mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 18:45:55 +00:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2a4707404a | |||
| cad0767a10 | |||
| 644a70960e | |||
| c2de6c636d | |||
| 6fbcb5f1ea | |||
| d94b0a1a7b | |||
| a29a45eca1 | |||
| f443a388cc | |||
| e4cceb3db1 | |||
| d8c2abdb2b | |||
| 81d3e4cb14 | |||
| 5c0e48ed2b | |||
| ce1c8db7fb | |||
| f0995dab1c | |||
| 600f12ea0e | |||
| d4ec5e6811 | |||
| 0b3e9a9e1e | |||
| 24c9d14663 | |||
| 934d4ddae2 | |||
| 7a81a7fc15 | |||
| 04acdc251f | |||
| 8cef71d2d2 | |||
| 47a1459060 | |||
| ee5ca2430b | |||
| ac08c3f106 | |||
| 6a197b6cf6 | |||
| e3d7cbe830 | |||
| e3c9f22ead | |||
| c6cbde3706 | |||
| b0d7572daf | |||
| 30449418ac | |||
| 95a56ca00e | |||
| fbf4a5458d | |||
| 2f8677a640 | |||
| bf69db04dd | |||
| e9f2f251e4 |
+8
-3
@@ -1,9 +1,14 @@
|
||||
# cargo-mutants (docs/mutation-testing.md). Mutants are judged by the black-box suites that will
|
||||
# outlive the kernel backend: the reference tests (tests/oracle, through the oracle_cases test).
|
||||
# The kernel's own unit tests are left out on purpose: a mutant only they catch is a behaviour the
|
||||
# outlive the kernel backend: the reference tests (tests/oracle, through the oracle_cases test)
|
||||
# and the pull-request sample of the robustness goldens (every 8th seed, as ci.yml). The
|
||||
# kernel's own unit tests are left out on purpose: a mutant only they catch is a behaviour the
|
||||
# backend-neutral suites would miss when the kernel is swapped.
|
||||
profile = "mutants"
|
||||
additional_cargo_test_args = [
|
||||
"--test", "oracle_cases", "--", "--include-ignored",
|
||||
"--test", "oracle_cases", "--lib", "--", "--include-ignored",
|
||||
"oracle_suite",
|
||||
"robustness_goldens::seed_00", "robustness_goldens::seed_08", "robustness_goldens::seed_16",
|
||||
"robustness_goldens::seed_24", "robustness_goldens::seed_32", "robustness_goldens::seed_40",
|
||||
"robustness_goldens::seed_48", "robustness_goldens::seed_56", "robustness_goldens::seed_64",
|
||||
"robustness_goldens::seed_72", "robustness_goldens::seed_80", "robustness_goldens::seed_88",
|
||||
]
|
||||
|
||||
+10
-8
@@ -1,6 +1,5 @@
|
||||
# cargo-nextest settings for CI (`cargo xtask test` does not use nextest).
|
||||
# CI builds the tests once and runs them in one job on a 32 vCPU runner (.github/workflows/ci.yml),
|
||||
# except the GPU path tracer's tests (the gpu-path group below), which run in four shards of their own.
|
||||
# CI builds the tests once and runs them in one job on a 32 vCPU runner (.github/workflows/ci.yml).
|
||||
|
||||
[profile.ci]
|
||||
# Heavy tests (one to twelve minutes each on a CI runner) that pull requests skip. They run on
|
||||
@@ -12,6 +11,7 @@ not (
|
||||
(package(solidcraft-engine) & (
|
||||
test(=tests::hostile_params_never_panic)
|
||||
| test(/^tests::robustness::/)
|
||||
| test(/^tests::robustness_goldens::seed_/)
|
||||
| test(=pattern_suppress_tests::enclosure_arc_extinction_keeps_a_real_sharp_inner_lip_after_resize_and_reopen)
|
||||
| test(=pattern_suppress_tests::enclosure_datums_follow_wall_height_and_board_depth_with_material_proofs)
|
||||
| test(=cmd::plastic::tests::enclosure_snap_fit_follows_resized_front_wall)
|
||||
@@ -42,13 +42,15 @@ filter = "binary(action_matrix) & test(=action_matrix_sample)"
|
||||
threads-required = 12
|
||||
priority = 100
|
||||
|
||||
|
||||
# The slowest tests start first, so a shard does not end with one of them running alone on a
|
||||
# core while the others sit idle. Medians from the CI test timings (the junit artifacts), slowest
|
||||
# first; a test over ~100 s belongs here.
|
||||
# core while the others sit idle (seed_81 alone takes about 20 minutes under CI load). Medians
|
||||
# from the CI test timings (the junit artifacts), slowest first; a test over ~100 s belongs here.
|
||||
[[profile.ci.overrides]]
|
||||
filter = """
|
||||
(package(solidcraft-engine) & (
|
||||
test(/^tests::robustness::/)
|
||||
test(/^tests::robustness_goldens::seed_(81|45|20|24|95|02|57|40|18|48|32|46|85|61)$/)
|
||||
| test(/^tests::robustness::/)
|
||||
| test(=tests::hostile_params_never_panic)
|
||||
| test(=cmd::clearance::tests::distances_are_exact_on_edges_and_curved_faces)
|
||||
| test(=cmd::clipboard::tests::a_pasted_coil_joins_the_pasted_body)
|
||||
@@ -71,7 +73,6 @@ priority = 100
|
||||
# Each nextest test has its own process/device. Concurrent cold lavapipe path
|
||||
# compiles contend for CPU and can exhaust the 600 s pipeline setup budget.
|
||||
# Serialize this module per CI shard; other tests retain their normal parallelism.
|
||||
# CI runs these in their own job (GPU path tests in ci.yml), split into count partitions.
|
||||
[test-groups.gpu-path]
|
||||
max-threads = 1
|
||||
|
||||
@@ -90,13 +91,14 @@ threads-required = "num-cpus"
|
||||
priority = 100
|
||||
|
||||
[[profile.local-ci.overrides]]
|
||||
filter = "package(solidcraft-kernel) & test(=tspline::tests::hostile_tsplines_never_panic)"
|
||||
filter = "(package(solidcraft-engine) & test(=tests::robustness_goldens::seed_81)) | (package(solidcraft-kernel) & test(=tspline::tests::hostile_tsplines_never_panic))"
|
||||
priority = 99
|
||||
|
||||
[[profile.local-ci.overrides]]
|
||||
filter = """
|
||||
(package(solidcraft-engine) & (
|
||||
test(/^tests::robustness::/)
|
||||
test(/^tests::robustness_goldens::seed_(81|45|20|24|95|02|57|40|18|48|32|46|85|61)$/)
|
||||
| test(/^tests::robustness::/)
|
||||
| test(=tests::hostile_params_never_panic)
|
||||
| test(=cmd::clearance::tests::distances_are_exact_on_edges_and_curved_faces)
|
||||
| test(=cmd::clipboard::tests::a_pasted_coil_joins_the_pasted_body)
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# Preserve canonical source receipts and the runtime icon-discovery snapshot.
|
||||
.gitattributes text eol=lf
|
||||
/assets/icons/ui/design/requested_icons.txt text eol=lf
|
||||
@@ -6,7 +6,6 @@ body:
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to report this! Inside the app, **Report an Issue** fills most of this in for you, with the command, its settings and your environment.
|
||||
Before reporting a boolean or modelling failure, please try to reproduce it on the latest nightly or a build from current `main`, and say which build you used.
|
||||
- type: textarea
|
||||
id: what
|
||||
attributes:
|
||||
@@ -20,7 +19,6 @@ body:
|
||||
label: Steps to reproduce
|
||||
description: |
|
||||
What you clicked or typed, in order. For a problem on a particular design, `solidcraft-cli script design.solidcraft --out repro.json` turns its history into a script that `solidcraft-cli eval repro.json` replays; attach it, trimmed to the failing step if you can.
|
||||
Using an AI agent with SolidCraft's MCP server? The `solidcraft-reporting` skill in [`skills/`](https://github.com/tangency-labs/solidcraft/tree/main/skills) has it reproduce the problem in a fresh design and write these steps for you.
|
||||
placeholder: |
|
||||
1. Sketch a circle on the XY plane
|
||||
2. Extrude it 10 mm
|
||||
|
||||
@@ -2,12 +2,6 @@
|
||||
|
||||
Describe the problem and resulting behavior. Link the issue when applicable.
|
||||
|
||||
Changes a default: no
|
||||
|
||||
<!-- Required line, replace "no" if this changes what a user gets out of the box (a default value, key binding, setting, tool behaviour):
|
||||
Changes a default: yes — <what>, old behaviour kept behind preference <name>
|
||||
Docs-only and bot PRs are exempt. -->
|
||||
|
||||
## Validation
|
||||
|
||||
What did you run, and what were the results? Include expected and measured values for geometry changes.
|
||||
|
||||
+72
-240
@@ -9,14 +9,12 @@
|
||||
# Documentation, packaging, the other workflows and the GitHub bots' scripts select nothing:
|
||||
# no native job runs for them, on pull requests or on main.
|
||||
# Tests are built and run in one job on a big runner, slowest first, starting beside the repository
|
||||
# checks (no archive to upload and download). The GPU path tracer's tests run one at a time
|
||||
# (.config/nextest.toml), so they have a job of their own in four shards (GPU path tests). A few
|
||||
# heavy tests (see .config/nextest.toml) are skipped on pull requests and run on pushes to main and
|
||||
# nightly. The reference tests and the doc tests run in the merge queue, on main and nightly, not
|
||||
# on pull requests (the fast lane, in Select checks); a break there is fixed forward on main. The
|
||||
# Windows jobs (D3D12 shaders and app startup) run nightly, and in the merge queue only for a group
|
||||
# that changes what they exercise. Only main saves the build caches; pull requests restore them.
|
||||
# sccache is on a Blacksmith sticky disk that only main commits.
|
||||
# checks (no archive to upload and download). A few heavy tests (see .config/nextest.toml) are
|
||||
# skipped on pull requests and run on pushes to main and nightly. The Windows jobs (D3D12 shaders
|
||||
# and app startup), the reference tests and the doc tests run on main and nightly only, not on
|
||||
# pull requests (the fast lane, in Select checks); a break there is fixed forward on main. Only
|
||||
# main saves the build caches; pull requests restore them. sccache is on a Blacksmith sticky disk that
|
||||
# only main commits.
|
||||
# A push to main runs only the repository checks when a passing merge-queue run tested that very
|
||||
# commit, or when nothing a build reads changed since the last push to main whose run passed.
|
||||
# Otherwise it runs everything.
|
||||
@@ -31,10 +29,8 @@
|
||||
# `ci` job still runs and reports the commit's real result (see the `ci` job): the merge queue
|
||||
# reads `ci` from the newest run of a commit, so a run without it would block a PR that passed.
|
||||
# Cross-compilation is opt-in through Run workflow; release builds remain in release.yml.
|
||||
# The release gate (release-gate.yml) calls this workflow (workflow_call) as a Run workflow with
|
||||
# cross-compilation on.
|
||||
# Every Linux job runs on Blacksmith's Ubuntu 24.04 runners (blacksmith-<N>vcpu-ubuntu-2404), sized
|
||||
# per job: 32 vCPU for the tests, 16 for Clippy, the GPU path tests, the archive build (nightly) and the merge queue's, 8 for compiles, doc and reference tests, 4 for the light checks, 2 for Select checks and ci.
|
||||
# per job: 32 vCPU for the tests, 16 for Clippy, the archive build (nightly) and the merge queue's, 8 for compiles, doc and reference tests, 4 for the light checks, 2 for Select checks and ci.
|
||||
# The Windows shader job runs on Blacksmith's Windows Server 2025 runners (blacksmith-8vcpu-windows-2025);
|
||||
# the Windows app startup smoke stays on windows-latest, which has a desktop session that presents frames.
|
||||
# Pull requests, forks included, the merge queue, pushes to main and the nightly run all use them:
|
||||
@@ -50,14 +46,10 @@ on:
|
||||
# only (other labels are skipped, see the top of this file).
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review, labeled]
|
||||
# The kernel-smlib branch (ADR 0002) runs no CI for now (owner, 2026-10-21). Drop this
|
||||
# line before the branch merges back into main.
|
||||
branches-ignore: [kernel-smlib]
|
||||
# The merge queue: each queued group runs the full suite before it lands on main.
|
||||
merge_group:
|
||||
push:
|
||||
# develop: the SMLib kernel's trunk until it merges into main (its PRs land there).
|
||||
branches: [main, develop]
|
||||
branches: [main]
|
||||
schedule:
|
||||
- cron: "17 6 * * *"
|
||||
workflow_dispatch:
|
||||
@@ -66,14 +58,6 @@ on:
|
||||
description: "Also check WebAssembly and RISC-V (before a release)"
|
||||
type: boolean
|
||||
default: false
|
||||
# The release gate (release-gate.yml): the event stays the caller's (workflow_dispatch), so a
|
||||
# call runs exactly what Run workflow runs.
|
||||
workflow_call:
|
||||
inputs:
|
||||
cross_compile:
|
||||
description: "Also check WebAssembly and RISC-V (before a release)"
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -117,10 +101,8 @@ jobs:
|
||||
outputs:
|
||||
native: ${{ steps.paths.outputs.native }}
|
||||
kernel: ${{ steps.paths.outputs.kernel }}
|
||||
goldens: ${{ steps.paths.outputs.goldens }}
|
||||
windows: ${{ steps.paths.outputs.windows }}
|
||||
# The GPU path tests run: native, and the whole workspace or solidcraft-gpu selected. The
|
||||
# job's `if` and the `ci` job's check both read this, so they cannot disagree.
|
||||
gpu: ${{ steps.paths.outputs.gpu }}
|
||||
full: ${{ steps.paths.outputs.full }}
|
||||
# Code changed on a pull request that has no approved-for-ci label yet: only Quick compile.
|
||||
quick: ${{ steps.paths.outputs.quick }}
|
||||
@@ -140,8 +122,6 @@ jobs:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
PUSH_SHA: ${{ github.sha }}
|
||||
GROUP_BASE_SHA: ${{ github.event.merge_group.base_sha }}
|
||||
GROUP_HEAD_SHA: ${{ github.event.merge_group.head_sha }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
HEAD_REF: ${{ github.event.pull_request.head.ref }}
|
||||
@@ -151,6 +131,7 @@ jobs:
|
||||
run: |
|
||||
native=false
|
||||
kernel=false
|
||||
goldens=false
|
||||
windows=false
|
||||
full=true
|
||||
packages=
|
||||
@@ -163,23 +144,6 @@ jobs:
|
||||
full=$(sed -n 's/^full=//p' "$RUNNER_TEMP/ci-affected")
|
||||
packages=$(sed -n 's/^packages=//p' "$RUNNER_TEMP/ci-affected")
|
||||
}
|
||||
# Whether the NUL-separated paths in $1 (the diff $2) reach what the Windows D3D12 jobs
|
||||
# exercise: the GPU crate and its shaders, the app and its startup, the viewport's wgpu
|
||||
# glue, the vendored egui-wgpu, their scripts, or a lockfile change that moves the
|
||||
# graphics stack (wgpu, naga, egui, winit, windows). Pull requests and merge groups share it.
|
||||
windows_paths() {
|
||||
while IFS= read -r -d '' path; do
|
||||
case "$path" in
|
||||
crates/gpu/*|apps/solidcraft/*|crates/ui-egui/src/gpu.rs|vendor/egui-wgpu/*) windows=true ;;
|
||||
tools/windows-*.ps1|.github/workflows/ci.yml|rust-toolchain*|.cargo/*) windows=true ;;
|
||||
esac
|
||||
done < "$1"
|
||||
if [ "$windows" = false ] && grep -qxz 'Cargo.lock' "$1" \
|
||||
&& git diff "$2" -- Cargo.lock \
|
||||
| grep -Eq '^[ +-]name = "(wgpu[a-z_-]*|naga|egui[a-z_-]*|eframe|winit|windows[a-z_-]*|d3d12)"'; then
|
||||
windows=true
|
||||
fi
|
||||
}
|
||||
if [ "$EVENT_NAME" = push ]; then
|
||||
# The queue lands the very commit its merge_group run tested. If that run passed,
|
||||
# testing the push again proves nothing new; otherwise (a merge that skipped the
|
||||
@@ -191,34 +155,22 @@ jobs:
|
||||
if [ "${tested:-0}" -gt 0 ]; then
|
||||
echo "The merge queue already tested $PUSH_SHA: repository checks only."
|
||||
elif tested=$(curl -fsS -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" \
|
||||
"https://api.github.com/repos/$REPO/actions/workflows/ci.yml/runs?event=push&branch=$GITHUB_REF_NAME&status=success&per_page=1" \
|
||||
"https://api.github.com/repos/$REPO/actions/workflows/ci.yml/runs?event=push&branch=main&status=success&per_page=1" \
|
||||
| grep -o '"head_sha": *"[0-9a-f]*"' | head -n 1 | grep -o '[0-9a-f]\{40\}') \
|
||||
&& [ -n "$tested" ] && git merge-base --is-ancestor "$tested" "$PUSH_SHA" \
|
||||
&& git diff --no-renames --name-only -z "$tested" "$PUSH_SHA" > "$RUNNER_TEMP/ci-paths" \
|
||||
&& select_packages "$RUNNER_TEMP/ci-paths" && [ "$full" != true ] && [ -z "$packages" ]; then
|
||||
# Nothing a build reads changed since the last push to this branch whose run passed (the
|
||||
# Nothing a build reads changed since the last push to main whose run passed (the
|
||||
# push before may never have run: a newer push cancels a waiting one).
|
||||
echo "$tested..$PUSH_SHA changes no code: repository checks only."
|
||||
else
|
||||
# Code changed (or the push can't be compared): main runs everything but Windows.
|
||||
# The merge queue ran Windows for the changes that reach it, and the nightly runs it
|
||||
# for everything else (it passed 129 of 129 non-PR runs before this).
|
||||
# Code changed (or the push can't be compared): main runs everything.
|
||||
full=true
|
||||
packages=
|
||||
native=true
|
||||
fi
|
||||
elif [ "$EVENT_NAME" = merge_group ]; then
|
||||
native=true
|
||||
# Windows (about 16 runner-minutes) only for a group that changes what it exercises;
|
||||
# the nightly runs it whatever changed. A group that can't be compared runs it.
|
||||
if git diff --no-renames --name-only -z "$GROUP_BASE_SHA...$GROUP_HEAD_SHA" > "$RUNNER_TEMP/ci-paths"; then
|
||||
windows_paths "$RUNNER_TEMP/ci-paths" "$GROUP_BASE_SHA...$GROUP_HEAD_SHA"
|
||||
else
|
||||
windows=true
|
||||
fi
|
||||
echo "windows=$windows for $GROUP_BASE_SHA...$GROUP_HEAD_SHA"
|
||||
elif [ "$EVENT_NAME" != pull_request ]; then
|
||||
# The nightly run and Run workflow: everything, Windows included.
|
||||
native=true
|
||||
windows=true
|
||||
else
|
||||
@@ -226,13 +178,34 @@ jobs:
|
||||
git diff --no-renames --name-only -z "$BASE_SHA...$HEAD_SHA" > "$RUNNER_TEMP/ci-paths"
|
||||
while IFS= read -r -d '' path; do
|
||||
case "$path" in crates/kernel/*|vendor/truck-*) kernel=true ;; esac
|
||||
# What a pull request re-checks the goldens for: the kernel, truck and the goldens
|
||||
# themselves (a re-record is checked before it is queued). The goldens are frozen, so
|
||||
# on any other change they would only re-check unchanged results; the merge queue,
|
||||
# main and the nightly run every seed before anything lands.
|
||||
case "$path" in
|
||||
crates/kernel/*|vendor/truck-*|tests/robustness/*) goldens=true ;;
|
||||
esac
|
||||
# What the Windows D3D12 jobs exercise: the GPU crate and its shaders, the app and
|
||||
# its startup, the viewport's wgpu glue, the vendored egui-wgpu, their scripts.
|
||||
case "$path" in
|
||||
crates/gpu/*|apps/solidcraft/*|crates/ui-egui/src/gpu.rs|vendor/egui-wgpu/*) windows=true ;;
|
||||
tools/windows-*.ps1|.github/workflows/ci.yml|rust-toolchain*|.cargo/*) windows=true ;;
|
||||
esac
|
||||
done < "$RUNNER_TEMP/ci-paths"
|
||||
# Computed as for a merge group, then turned off below (the fast lane).
|
||||
windows_paths "$RUNNER_TEMP/ci-paths" "$BASE_SHA...$HEAD_SHA"
|
||||
# A lockfile change that moves the graphics stack (wgpu, naga, egui, winit, windows).
|
||||
if [ "$windows" = false ] && grep -qxz 'Cargo.lock' "$RUNNER_TEMP/ci-paths" \
|
||||
&& git diff "$BASE_SHA...$HEAD_SHA" -- Cargo.lock \
|
||||
| grep -Eq '^[ +-]name = "(wgpu[a-z_-]*|naga|egui[a-z_-]*|eframe|winit|windows[a-z_-]*|d3d12)"'; then
|
||||
windows=true
|
||||
fi
|
||||
# Pull requests build and test the touched packages and their dependents; nothing
|
||||
# selected means no code changed.
|
||||
select_packages "$RUNNER_TEMP/ci-paths"
|
||||
if [ "$full" = true ] || [ -n "$packages" ]; then native=true; fi
|
||||
# The goldens are solidcraft-engine tests: its tests must be in the archive.
|
||||
if [ "$goldens" = true ] && [ "$full" != true ]; then
|
||||
case " $packages " in *" solidcraft-engine "*) ;; *) packages="$packages solidcraft-engine" ;; esac
|
||||
fi
|
||||
fi
|
||||
# The reference tests replay recipes through solidcraft-cli: needed when it, or anything
|
||||
# it depends on, is selected.
|
||||
@@ -247,12 +220,13 @@ jobs:
|
||||
doctests=$native
|
||||
if [ "$EVENT_NAME" = pull_request ]; then windows=false; oracle=false; doctests=false; fi
|
||||
# Until a maintainer adds approved-for-ci, a pull request gets a quick compile instead of
|
||||
# Clippy, the tests and the doc tests.
|
||||
# Clippy, the tests, the doc tests and the goldens.
|
||||
quick=false
|
||||
if [ "$EVENT_NAME" = pull_request ] && [ "$APPROVED" != true ]; then
|
||||
quick=$native
|
||||
native=false
|
||||
oracle=false
|
||||
goldens=false
|
||||
fi
|
||||
# A push that only merges main into the change (Merge branch 'main', Update branch) when
|
||||
# the change itself already passed: nothing new to test here, and main tests the merged
|
||||
@@ -265,19 +239,13 @@ jobs:
|
||||
if [ -n "$carried" ]; then
|
||||
echo "Only a merge of main into a change that passed in $carried: repository checks only."
|
||||
native=false
|
||||
goldens=false
|
||||
fi
|
||||
fi
|
||||
# The GPU path tests (their own job) run with the other tests, when solidcraft-gpu is
|
||||
# among the packages they test.
|
||||
gpu=false
|
||||
if [ "$native" = true ]; then
|
||||
case " $packages " in *" solidcraft-gpu "*) gpu=true ;; esac
|
||||
if [ "$full" = true ]; then gpu=true; fi
|
||||
fi
|
||||
{
|
||||
echo "native=$native"
|
||||
echo "gpu=$gpu"
|
||||
echo "kernel=$kernel"
|
||||
echo "goldens=$goldens"
|
||||
echo "windows=$windows"
|
||||
echo "oracle=$oracle"
|
||||
echo "doctests=$doctests"
|
||||
@@ -301,7 +269,7 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: repo-checks
|
||||
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: Format
|
||||
run: cargo fmt --all -- --check
|
||||
- name: Asset attribution
|
||||
@@ -318,14 +286,6 @@ jobs:
|
||||
cargo test -p xtask ui_containers --locked
|
||||
- name: Sorted lists
|
||||
run: cargo xtask lists
|
||||
- name: Model decisions use exact geometry, not meshes
|
||||
run: |
|
||||
cargo xtask mesh-decisions
|
||||
cargo test -p xtask mesh_decisions --locked
|
||||
- name: Agent router and skills cite real files
|
||||
run: |
|
||||
cargo xtask skills
|
||||
cargo test -p xtask skills --locked
|
||||
- name: Design format pending changes
|
||||
run: bash scripts/test-format-pending.sh
|
||||
- name: Web app offline use and updates (headless Chrome)
|
||||
@@ -335,7 +295,7 @@ jobs:
|
||||
- name: Merge-of-main detection self-test
|
||||
run: python3 -I scripts/ci/test_main_merge.py
|
||||
- name: Claim, triage and kernel-paused bot tests
|
||||
run: node --test scripts/claim/*.test.js scripts/triage/*.test.js scripts/kernel-paused/*.test.js scripts/pr-checks/*.test.js
|
||||
run: node --test scripts/claim/*.test.js scripts/triage/*.test.js scripts/kernel-paused/*.test.js
|
||||
|
||||
# Fast feedback for a pull request that has not been approved for the full CI yet.
|
||||
quick-compile:
|
||||
@@ -352,10 +312,7 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: quick-compile
|
||||
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
|
||||
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
|
||||
shell: bash
|
||||
run: scripts/fetch-smlib.sh
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: Cargo check
|
||||
run: cargo check --workspace --all-targets --locked
|
||||
|
||||
@@ -374,10 +331,7 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: native-clippy
|
||||
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
|
||||
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
|
||||
shell: bash
|
||||
run: scripts/fetch-smlib.sh
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: Clippy
|
||||
env:
|
||||
FULL: ${{ needs.changes.outputs.full }}
|
||||
@@ -420,10 +374,7 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: native-tests
|
||||
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
|
||||
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
|
||||
shell: bash
|
||||
run: scripts/fetch-smlib.sh
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: Set up compiler cache
|
||||
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
|
||||
with:
|
||||
@@ -494,10 +445,7 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: native-tests
|
||||
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
|
||||
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
|
||||
shell: bash
|
||||
run: scripts/fetch-smlib.sh
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: Set up compiler cache
|
||||
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
|
||||
with:
|
||||
@@ -508,8 +456,13 @@ jobs:
|
||||
- name: Run tests
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
GOLDENS: ${{ needs.changes.outputs.goldens }}
|
||||
FULL: ${{ needs.changes.outputs.full }}
|
||||
PACKAGES: ${{ needs.changes.outputs.packages }}
|
||||
# The robustness golden seeds a pull request skips: each takes one to twenty minutes under
|
||||
# CI load, together about four fifths of the goldens' time. Pushes to main, the merge
|
||||
# queue and the nightly run every seed.
|
||||
SLOW_SEEDS: 81|45|20|24|95|02|57|40|18|48|32|46|85|61|77|34|14
|
||||
run: |
|
||||
# Pull requests skip the heavy tests (default-filter in .config/nextest.toml); `default()`
|
||||
# below is that filter, so a pull request still runs only what it ran before.
|
||||
@@ -523,23 +476,27 @@ jobs:
|
||||
# The boolean property tests draw new random inputs every run; they run only in the
|
||||
# nightly property-tests job, so a known kernel bug cannot turn main red at random.
|
||||
base='not (package(solidcraft-kernel) & binary(boolean_props))'
|
||||
elif [ "$GOLDENS" = true ]; then
|
||||
# A pull request that can change a robustness golden (tests/robustness/goldens.json)
|
||||
# also checks every seed but the slow ones, in the same run so they share its cores.
|
||||
base="(default() & not (binary(action_matrix) & test(=action_matrix_sample))) | (package(solidcraft-engine) & test(/^tests::robustness_goldens::/) & not test(/^tests::robustness_goldens::seed_($SLOW_SEEDS)\$/))"
|
||||
fi
|
||||
junit=${CARGO_TARGET_DIR:-target}/nextest/ci/junit.xml
|
||||
rm -f "$junit"
|
||||
# Pull requests test the affected packages (scripts/ci/affected.py).
|
||||
scope=(--workspace)
|
||||
if [ "$FULL" != true ]; then scope=(); for p in $PACKAGES; do scope+=(-p "$p"); done; fi
|
||||
# The GPU path tracer's tests run in their own job (GPU path tests): one at a time, they
|
||||
# took about 36 of this job's 45 minutes.
|
||||
# A failed test does not fail the step by itself: the tests known to fail today
|
||||
# (tests/KNOWN_FAILING_TESTS) may fail, any other fails it, and so does a listed test
|
||||
# that passes. nextest's exit code 100 is "tests failed"; any other is its own error
|
||||
# (a build error, a bad filter) and fails the step at once.
|
||||
status=0
|
||||
cargo nextest run "${scope[@]}" --cargo-profile ci --locked --profile ci --no-tests=pass \
|
||||
--ignore-default-filter -E "($base) & not (package(=solidcraft-gpu) & test(/^path::/))" || status=$?
|
||||
if [ "$status" -ne 0 ] && [ "$status" -ne 100 ]; then exit "$status"; fi
|
||||
cargo xtask known-failing --junit "$junit"
|
||||
--ignore-default-filter -E "$base" || status=1
|
||||
if [ "$status" != 0 ]; then
|
||||
# A failed golden gets the hint to re-record it (junit.xml lists the failures).
|
||||
if [ -f "$junit" ] && tr '\n' ' ' < "$junit" | grep -q '<testcase name="tests::robustness_goldens::[^"]*"[^>]*>[[:space:]]*<failure'; then
|
||||
echo "::error::A robustness golden changed. If the new result is intended, re-record it in this pull request and say why:"
|
||||
echo "::error::ROBUSTNESS_GOLDENS_BLESS=\"why it changed\" cargo test --profile ci -p solidcraft-engine --lib robustness_goldens:: (see docs/robustness.md)"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
- name: Compiler cache statistics
|
||||
if: always()
|
||||
run: sccache --show-stats
|
||||
@@ -551,105 +508,6 @@ jobs:
|
||||
path: ${{ env.CARGO_TARGET_DIR || 'target' }}/nextest/ci/junit.xml
|
||||
retention-days: 7
|
||||
|
||||
gpu-path-tests:
|
||||
name: GPU path tests (${{ matrix.shard }}/4)
|
||||
# The GPU path tracer's tests (about 108) compile their pipelines on lavapipe, one test at a
|
||||
# time (the gpu-path test group in .config/nextest.toml keeps them within the 600 s pipeline
|
||||
# setup budget). Serial, they took about 36 minutes, the longest chain in Native Linux tests;
|
||||
# here four shards run beside it. Their filter is the one Native Linux tests leaves out, so
|
||||
# every test runs exactly once.
|
||||
# 16 vCPU, not 8: lavapipe compiles on every core, and on 8 vCPU one test went past the 600 s
|
||||
# budget (665 s, against 204 s for the same test in another run) and a shard took 35 minutes.
|
||||
needs: changes
|
||||
if: ${{ !cancelled() && needs.changes.result == 'success' && needs.changes.outputs.gpu == 'true' }}
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
timeout-minutes: 45
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2, 3, 4]
|
||||
env:
|
||||
# As in Native Linux tests: lavapipe draws, and a test without an adapter fails.
|
||||
SOLIDCRAFT_GPU: software
|
||||
SOLIDCRAFT_REQUIRE_GPU: "1"
|
||||
# lavapipe's own shader cache (below).
|
||||
MESA_SHADER_CACHE_DIR: /home/runner/.cache/mesa-gpu
|
||||
CARGO_INCREMENTAL: "0"
|
||||
RUSTC_WRAPPER: sccache
|
||||
SCCACHE_DIR: /home/runner/.cache/sccache
|
||||
SCCACHE_CACHE_SIZE: 20G
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Install a software Vulkan driver (lavapipe)
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends mesa-vulkan-drivers libvulkan1
|
||||
- name: Size parallel compiles to the job's memory
|
||||
run: scripts/ci/build-jobs.sh
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
# sccache lives on a Blacksmith sticky disk (an ext4 volume cloned from the last snapshot,
|
||||
# no 10 GB limit and no download). Only main commits it; pull requests, the merge queue and
|
||||
# forks mount a clone and never write back, so nothing they build can reach main's cache.
|
||||
# The shards build the same thing, so only the first one commits it (and saves the cache).
|
||||
- name: Mount the compiler cache
|
||||
uses: useblacksmith/stickydisk@3f6be1451e3cad893b778bc93495c80452a5da26 # v1.7.2
|
||||
continue-on-error: true
|
||||
with:
|
||||
key: ${{ github.repository }}-sccache-gpu
|
||||
path: /home/runner/.cache/sccache
|
||||
commit: ${{ github.ref == 'refs/heads/main' && matrix.shard == 1 && 'on-change' || 'false' }}
|
||||
# The first test in a shard to build the path pipeline compiles it cold on lavapipe (335 to
|
||||
# 665 s, against the 600 s setup budget); every later one reads Mesa's disk cache (about
|
||||
# 30 s). Keeping that cache from main's last run lets the first test read it too, until the
|
||||
# shaders change. Same sticky disk rules as the compiler cache.
|
||||
- name: Mount lavapipe's shader cache
|
||||
uses: useblacksmith/stickydisk@3f6be1451e3cad893b778bc93495c80452a5da26 # v1.7.2
|
||||
continue-on-error: true
|
||||
with:
|
||||
key: ${{ github.repository }}-mesa-gpu
|
||||
path: /home/runner/.cache/mesa-gpu
|
||||
commit: ${{ github.ref == 'refs/heads/main' && matrix.shard == 1 && 'on-change' || 'false' }}
|
||||
# Main saves the cache; every other run restores it, read-only.
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: native-gpu
|
||||
save-if: ${{ (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') && matrix.shard == 1 }}
|
||||
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
|
||||
shell: bash
|
||||
run: scripts/fetch-smlib.sh
|
||||
- name: Set up compiler cache
|
||||
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
|
||||
with:
|
||||
version: v0.16.0
|
||||
- uses: taiki-e/install-action@1168f6810a3cb30a3514bf5756a3b7f0422c6df8 # v2.87.28
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: Run one shard of the GPU path tests
|
||||
env:
|
||||
SHARD: ${{ matrix.shard }}
|
||||
run: |
|
||||
# None of these is in the default filter (the heavy tests), so pull requests, the merge
|
||||
# queue, main and nightly all run every one of them, as before. `count` deals the tests
|
||||
# out in turn (27 each); `hash` split them 41/40/27 across three shards.
|
||||
# Graded against tests/KNOWN_FAILING_TESTS, as in Native Linux tests.
|
||||
junit=${CARGO_TARGET_DIR:-target}/nextest/ci/junit.xml
|
||||
rm -f "$junit"
|
||||
status=0
|
||||
cargo nextest run -p solidcraft-gpu --cargo-profile ci --locked --profile ci --no-tests=pass \
|
||||
--ignore-default-filter -E 'test(/^path::/)' --partition "count:$SHARD/4" || status=$?
|
||||
if [ "$status" -ne 0 ] && [ "$status" -ne 100 ]; then exit "$status"; fi
|
||||
cargo xtask known-failing --junit "$junit"
|
||||
- name: Compiler cache statistics
|
||||
if: always()
|
||||
run: sccache --show-stats
|
||||
- name: Upload test timings
|
||||
if: always()
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: junit-gpu-${{ matrix.shard }}
|
||||
path: ${{ env.CARGO_TARGET_DIR || 'target' }}/nextest/ci/junit.xml
|
||||
retention-days: 7
|
||||
|
||||
action-matrix:
|
||||
# Every ordinary action on every face, edge, vertex and body of the design corpus, against
|
||||
# tests/action_matrix/baseline.json (docs/action-matrix.md). Every test run checks a sample;
|
||||
@@ -772,10 +630,7 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: doc-tests
|
||||
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
|
||||
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
|
||||
shell: bash
|
||||
run: scripts/fetch-smlib.sh
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: Set up compiler cache
|
||||
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
|
||||
with:
|
||||
@@ -833,10 +688,7 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: oracle
|
||||
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
|
||||
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
|
||||
shell: bash
|
||||
run: scripts/fetch-smlib.sh
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: Set up compiler cache
|
||||
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
|
||||
with:
|
||||
@@ -875,10 +727,7 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: windows-fxc
|
||||
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
|
||||
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
|
||||
shell: bash
|
||||
run: scripts/fetch-smlib.sh
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
# One test at a time: WARP's shader JIT crashes now and then (a null read in
|
||||
# d3d10warp.dll's JIT optimizer, about 1 run in 60) when several devices compile compute
|
||||
# pipelines at once, which the parallel tests do. Minidumps showed it in WARP itself, not
|
||||
@@ -902,10 +751,7 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: windows-startup
|
||||
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
|
||||
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
|
||||
shell: bash
|
||||
run: scripts/fetch-smlib.sh
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: Backend order and probing on Windows
|
||||
run: cargo test -p solidcraft --profile ci --locked -- startup::tests native_crash::tests native_frame::tests
|
||||
- name: Build the app
|
||||
@@ -923,7 +769,7 @@ jobs:
|
||||
# commit's real run instead: the merge queue reads `ci` from the newest run of a commit, so a
|
||||
# run without a passing `ci` would block a pull request that passed (#1850, #3182, #3252).
|
||||
name: ci
|
||||
needs: [changes, checks, quick-compile, clippy, build-tests, tests, gpu-path-tests, doc-tests, oracle, windows-fxc, windows-startup]
|
||||
needs: [changes, checks, quick-compile, clippy, build-tests, tests, doc-tests, oracle, windows-fxc, windows-startup]
|
||||
if: always() && (github.event_name != 'pull_request' || !github.event.pull_request.draft)
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
permissions:
|
||||
@@ -971,8 +817,6 @@ jobs:
|
||||
BUILD: ${{ needs.build-tests.result }}
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
TESTS: ${{ needs.tests.result }}
|
||||
GPU_RUN: ${{ needs.changes.outputs.gpu }}
|
||||
GPU: ${{ needs.gpu-path-tests.result }}
|
||||
DOCS: ${{ needs.doc-tests.result }}
|
||||
ORACLE: ${{ needs.oracle.result }}
|
||||
WINDOWS_FXC: ${{ needs.windows-fxc.result }}
|
||||
@@ -992,10 +836,6 @@ jobs:
|
||||
fi
|
||||
test "$TESTS" = success
|
||||
fi
|
||||
# The same output decides whether the GPU path tests run (their job's `if`).
|
||||
if [ "$GPU_RUN" = true ]; then
|
||||
test "$GPU" = success
|
||||
fi
|
||||
if [ "$DOCTESTS" = true ]; then
|
||||
test "$DOCS" = success
|
||||
fi
|
||||
@@ -1026,10 +866,7 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: wasm
|
||||
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
|
||||
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
|
||||
shell: bash
|
||||
run: scripts/fetch-smlib.sh wasm32-unknown-unknown
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- run: cargo xtask wasm
|
||||
|
||||
riscv64-gates:
|
||||
@@ -1051,10 +888,7 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: riscv64
|
||||
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
|
||||
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
|
||||
shell: bash
|
||||
run: scripts/fetch-smlib.sh riscv64gc-unknown-linux-gnu
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: Install cross-compilation dependencies
|
||||
run: |
|
||||
# Ubuntu's regular archive serves amd64; RISC-V packages live in ports.
|
||||
@@ -1070,13 +904,11 @@ jobs:
|
||||
EOF
|
||||
sudo dpkg --add-architecture riscv64
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gcc-riscv64-linux-gnu g++-riscv64-linux-gnu libasound2-dev:riscv64 libxkbcommon-dev:riscv64 \
|
||||
sudo apt-get install -y gcc-riscv64-linux-gnu libasound2-dev:riscv64 libxkbcommon-dev:riscv64 \
|
||||
libwayland-dev:riscv64 libx11-dev:riscv64 libxrandr-dev:riscv64 libxi-dev:riscv64 libgl1-mesa-dev:riscv64
|
||||
- name: Cargo check on RISC-V
|
||||
env:
|
||||
CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_GNU_LINKER: riscv64-linux-gnu-gcc
|
||||
# SMLib has no prebuilt library for RISC-V: its sources are compiled with this.
|
||||
CXX_riscv64gc_unknown_linux_gnu: riscv64-linux-gnu-g++
|
||||
PKG_CONFIG_SYSROOT_DIR: /
|
||||
PKG_CONFIG_PATH: /usr/lib/riscv64-linux-gnu/pkgconfig:/usr/share/pkgconfig
|
||||
PKG_CONFIG_ALLOW_CROSS: 1
|
||||
|
||||
@@ -53,9 +53,6 @@ jobs:
|
||||
with:
|
||||
key: docker-web
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
|
||||
shell: bash
|
||||
run: scripts/fetch-smlib.sh wasm32-unknown-unknown
|
||||
- name: Install trunk
|
||||
run: |
|
||||
mkdir -p "$HOME/.local/bin"
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
# Nightly import checks on real-world STEP files (#1397): the external corpus in
|
||||
# tests/external-corpus.toml is downloaded (hash-verified), imported headless, and checked
|
||||
# against tolerance-based expectations — body counts, total volume and area, time and memory
|
||||
# budgets. It is not part of `cargo xtask ci`: it downloads from the internet.
|
||||
#
|
||||
# Import results should improve over time (healing, edge merging, meshing). When a check fails
|
||||
# because the numbers moved for the better (or the source changed upstream), re-record with
|
||||
# `cargo xtask external-corpus --bless "why"` in a pull request, never by editing numbers.
|
||||
name: External corpus
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '40 3 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
only:
|
||||
description: "Run only this corpus entry name (empty = every entry)"
|
||||
type: string
|
||||
default: ""
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
corpus:
|
||||
name: Import the external STEP corpus
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
|
||||
shell: bash
|
||||
run: scripts/fetch-smlib.sh
|
||||
- name: Build the CLI
|
||||
run: cargo build --profile ci -p solidcraft-cli
|
||||
- name: Import and check every entry
|
||||
env:
|
||||
ONLY: ${{ inputs.only }}
|
||||
run: |
|
||||
if [ -z "$ONLY" ]; then
|
||||
cargo run -q -p xtask -- external-corpus --cli target/ci/solidcraft-cli
|
||||
else
|
||||
cargo run -q -p xtask -- external-corpus --cli target/ci/solidcraft-cli --only "$ONLY"
|
||||
fi
|
||||
@@ -1,109 +0,0 @@
|
||||
# The fillet rig as a regression gate: a pull request that touches fillet code (the paths below)
|
||||
# runs every case in tests/kernel-cases/fillets/ on SMLib, through one release build of
|
||||
# solidcraft-cli, and fails on a regression (a case that fails and
|
||||
# tests/kernel-cases/fillets/KNOWN_FAILING does not list) or on a listed case that now passes
|
||||
# (it is fixed: remove its line, so the list only shrinks). See tests/kernel-cases/README.md.
|
||||
#
|
||||
# GitHub filters a workflow by whole files, so the fillet parts of crates/doc/src/eval*.rs and
|
||||
# crates/engine/src/cmd/features.rs select it whenever those files change. Pushes to main and
|
||||
# develop that touch the same paths run it too and save the compiler cache (a Blacksmith sticky
|
||||
# disk) that pull requests then mount read-only.
|
||||
name: Fillet rig
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
paths:
|
||||
- "crates/kernel/src/blend*.rs"
|
||||
- "crates/kernel/src/fillet*.rs"
|
||||
- "crates/kernel/src/*corner*.rs"
|
||||
- "crates/kernel/src/continuity*.rs"
|
||||
- "crates/smlib/**"
|
||||
- "crates/doc/src/eval*.rs"
|
||||
- "crates/engine/src/cmd/features.rs"
|
||||
- "tests/kernel-cases/fillets/**"
|
||||
# The rig itself: the grading, the runner and this workflow.
|
||||
- "apps/solidcraft-cli/src/kernel_cases.rs"
|
||||
- "xtask/src/kernel_cases.rs"
|
||||
- "scripts/fetch-smlib.sh"
|
||||
- ".github/workflows/fillet-rig.yml"
|
||||
push:
|
||||
branches: [main, develop]
|
||||
# The same paths as pull_request.
|
||||
paths:
|
||||
- "crates/kernel/src/blend*.rs"
|
||||
- "crates/kernel/src/fillet*.rs"
|
||||
- "crates/kernel/src/*corner*.rs"
|
||||
- "crates/kernel/src/continuity*.rs"
|
||||
- "crates/smlib/**"
|
||||
- "crates/doc/src/eval*.rs"
|
||||
- "crates/engine/src/cmd/features.rs"
|
||||
- "tests/kernel-cases/fillets/**"
|
||||
# The rig itself: the grading, the runner and this workflow.
|
||||
- "apps/solidcraft-cli/src/kernel_cases.rs"
|
||||
- "xtask/src/kernel_cases.rs"
|
||||
- "scripts/fetch-smlib.sh"
|
||||
- ".github/workflows/fillet-rig.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: fillet-rig-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
fillets:
|
||||
name: Fillet rig
|
||||
# Draft pull requests run no CI, as in ci.yml.
|
||||
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
CARGO_INCREMENTAL: "0"
|
||||
RUSTC_WRAPPER: sccache
|
||||
SCCACHE_DIR: /home/runner/.cache/sccache
|
||||
SCCACHE_CACHE_SIZE: 10G
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Size parallel compiles to the job's memory
|
||||
run: scripts/ci/build-jobs.sh
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
# Only main and develop commit the cache; pull requests and forks mount a clone and never
|
||||
# write back.
|
||||
- name: Mount the compiler cache
|
||||
uses: useblacksmith/stickydisk@3f6be1451e3cad893b778bc93495c80452a5da26 # v1.7.2
|
||||
continue-on-error: true
|
||||
with:
|
||||
key: ${{ github.repository }}-sccache-fillet-rig
|
||||
path: /home/runner/.cache/sccache
|
||||
commit: ${{ (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') && 'on-change' || 'false' }}
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: fillet-rig
|
||||
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
|
||||
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
|
||||
shell: bash
|
||||
run: scripts/fetch-smlib.sh
|
||||
- name: Set up compiler cache
|
||||
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
|
||||
with:
|
||||
version: v0.16.0
|
||||
- name: Build the CLI (release, SMLib)
|
||||
run: cargo build --release --locked -p solidcraft-cli --features smlib
|
||||
- name: Run the fillet cases against the known-failing list
|
||||
run: >-
|
||||
cargo run -q --locked -p xtask -- kernel-cases --area fillets
|
||||
--cli target/release/solidcraft-cli --out "$RUNNER_TEMP/fillet-rig"
|
||||
--known-failing tests/kernel-cases/fillets/KNOWN_FAILING
|
||||
- name: Keep the results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: fillet-rig-results
|
||||
path: ${{ runner.temp }}/fillet-rig/results.json
|
||||
if-no-files-found: ignore
|
||||
@@ -1,8 +1,6 @@
|
||||
# Files generated from the code are rewritten here, on main, after each merge, so pull requests
|
||||
# never edit them (two pull requests that each regenerated one would always conflict):
|
||||
# THIRD-PARTY-LICENSES.txt (`cargo xtask licences`) and docs/features.md (`cargo xtask features`).
|
||||
# Pending files (assets/locales/pending, attribution/pending) are folded into the catalogs and
|
||||
# ATTRIBUTION.md by `cargo xtask fold-pending`.
|
||||
name: Generated files
|
||||
|
||||
on:
|
||||
@@ -19,8 +17,6 @@ on:
|
||||
- "crates/engine/**"
|
||||
- "apps/solidcraft-cli/**"
|
||||
- "xtask/**"
|
||||
- "assets/locales/pending/*.json"
|
||||
- "attribution/pending/*.md"
|
||||
- ".github/workflows/generated.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -48,22 +44,17 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: generated-files
|
||||
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
|
||||
shell: bash
|
||||
run: scripts/fetch-smlib.sh
|
||||
- name: THIRD-PARTY-LICENSES.txt
|
||||
run: cargo xtask licences
|
||||
- name: docs/features.md
|
||||
run: cargo xtask features
|
||||
- name: Pending locale strings and attribution rows
|
||||
run: cargo xtask fold-pending
|
||||
- name: Commit if changed
|
||||
run: |
|
||||
if [ -z "$(git status --porcelain)" ]; then echo "unchanged"; exit 0; fi
|
||||
if git diff --quiet -- THIRD-PARTY-LICENSES.txt docs/features.md; then echo "unchanged"; exit 0; fi
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git add -A THIRD-PARTY-LICENSES.txt docs/features.md assets/locales ATTRIBUTION.md attribution
|
||||
git commit -m "Regenerate generated files and fold pending files [skip ci]"
|
||||
git add THIRD-PARTY-LICENSES.txt docs/features.md
|
||||
git commit -m "Regenerate THIRD-PARTY-LICENSES.txt and docs/features.md [skip ci]"
|
||||
for _ in 1 2 3; do
|
||||
git pull --rebase origin main && git push origin HEAD:main && exit 0
|
||||
sleep 10
|
||||
|
||||
@@ -16,7 +16,7 @@ on:
|
||||
permissions:
|
||||
contents: read # to check out scripts/kernel-paused
|
||||
issues: write
|
||||
pull-requests: write # to list a PR's changed paths and add its label
|
||||
pull-requests: read # to list a PR's changed paths
|
||||
discussions: write
|
||||
|
||||
concurrency:
|
||||
|
||||
@@ -1,234 +0,0 @@
|
||||
# The kernel rig and the complexity ladder as a regression gate. A plan job maps a pull request's
|
||||
# changed files to the rig areas they can affect (scripts/ci/kernel_rig_plan.py): an area runs
|
||||
# when a file its tests/kernel-cases/<area>/PATHS lists changes, or one of its cases; every area
|
||||
# runs when the shared core in tests/kernel-cases/PATHS-ALL changes; the ladder runs whenever any
|
||||
# area does. One job then builds solidcraft-cli once (release, SMLib prebuilt), checks that every
|
||||
# registry command has a case or an exemption (tests/kernel-cases/EXEMPT), and hands the CLI to a
|
||||
# matrix of shards as an artifact, one per selected area plus the ladder, in parallel, so the
|
||||
# wall time is the build plus the slowest shard. See tests/kernel-cases/README.md.
|
||||
#
|
||||
# - An area fails on a regression (a case that fails and its tests/kernel-cases/<area>/KNOWN_FAILING
|
||||
# does not list) or on a listed case that passes (it is fixed: remove its line in the same pull
|
||||
# request, so the list only shrinks).
|
||||
# - The ladder fails on any case whose status or counts differ from
|
||||
# tests/kernel-cases/ladder/golden.json; a right change is re-recorded (`--record "why"`).
|
||||
#
|
||||
# The trigger paths are whole folders that hold every area's PATHS (a self-test,
|
||||
# scripts/ci/test_kernel_rig_plan.py, checks they do), so a pull request that touches none of
|
||||
# them (the UI, docs, packaging) starts no runner at all, and a new area needs no edit here: the
|
||||
# plan finds the areas from the folders. Pushes to main and develop on the same paths, merge groups
|
||||
# (once the queue is on), the release gate (release-gate.yml, workflow_call) and a run by hand run
|
||||
# every area; those pushes also commit the compiler cache (a Blacksmith sticky disk) that every
|
||||
# other run mounts read-only.
|
||||
name: Kernel rig
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
paths:
|
||||
- "crates/kernel/**"
|
||||
- "crates/geom/**"
|
||||
- "crates/smlib/**"
|
||||
- "crates/step/**"
|
||||
- "crates/io/src/**"
|
||||
- "crates/doc/src/**"
|
||||
- "crates/engine/src/**"
|
||||
- "tests/kernel-cases/**"
|
||||
# The rig itself: the grading, the runner, the selection and this workflow.
|
||||
- "apps/solidcraft-cli/src/kernel_cases.rs"
|
||||
- "xtask/src/kernel_cases.rs"
|
||||
- "xtask/src/kernel_ladder.rs"
|
||||
- "scripts/fetch-smlib.sh"
|
||||
- "scripts/ci/kernel_rig_plan.py"
|
||||
- "scripts/ci/test_kernel_rig_plan.py"
|
||||
- ".github/workflows/kernel-rig.yml"
|
||||
push:
|
||||
branches: [main, develop]
|
||||
# The same paths as pull_request.
|
||||
paths:
|
||||
- "crates/kernel/**"
|
||||
- "crates/geom/**"
|
||||
- "crates/smlib/**"
|
||||
- "crates/step/**"
|
||||
- "crates/io/src/**"
|
||||
- "crates/doc/src/**"
|
||||
- "crates/engine/src/**"
|
||||
- "tests/kernel-cases/**"
|
||||
# The rig itself: the grading, the runner, the selection and this workflow.
|
||||
- "apps/solidcraft-cli/src/kernel_cases.rs"
|
||||
- "xtask/src/kernel_cases.rs"
|
||||
- "xtask/src/kernel_ladder.rs"
|
||||
- "scripts/fetch-smlib.sh"
|
||||
- "scripts/ci/kernel_rig_plan.py"
|
||||
- "scripts/ci/test_kernel_rig_plan.py"
|
||||
- ".github/workflows/kernel-rig.yml"
|
||||
merge_group:
|
||||
workflow_call:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
# A new push to a pull request replaces its old run.
|
||||
group: kernel-rig-${{ github.event_name }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
plan:
|
||||
name: Select the rig areas
|
||||
# Draft pull requests run no CI, as in ci.yml.
|
||||
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
# The shard matrix (JSON list of areas and the ladder; [] when nothing is selected).
|
||||
shards: ${{ steps.plan.outputs.shards }}
|
||||
# Whether the build job, and with it the coverage check, runs.
|
||||
build: ${{ steps.plan.outputs.build }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
# A pull request's diff needs its base's history.
|
||||
fetch-depth: ${{ github.event_name == 'pull_request' && '0' || '1' }}
|
||||
- name: Selection self-test
|
||||
run: python3 -I scripts/ci/test_kernel_rig_plan.py
|
||||
- id: plan
|
||||
name: Map the changed files to areas
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
run: |
|
||||
# Pull requests run the areas their diff reaches; pushes, merge groups, the release
|
||||
# gate and a run by hand run every area.
|
||||
if [ "$EVENT_NAME" = pull_request ]; then
|
||||
git diff --no-renames --name-only -z "$BASE_SHA...$HEAD_SHA" > "$RUNNER_TEMP/rig-paths"
|
||||
python3 -I scripts/ci/kernel_rig_plan.py --paths "$RUNNER_TEMP/rig-paths" --github-output "$GITHUB_OUTPUT"
|
||||
else
|
||||
python3 -I scripts/ci/kernel_rig_plan.py --all --github-output "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
build:
|
||||
name: Build the CLI
|
||||
needs: plan
|
||||
if: needs.plan.outputs.build == 'true'
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
CARGO_INCREMENTAL: "0"
|
||||
RUSTC_WRAPPER: sccache
|
||||
SCCACHE_DIR: /home/runner/.cache/sccache
|
||||
SCCACHE_CACHE_SIZE: 10G
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Size parallel compiles to the job's memory
|
||||
run: scripts/ci/build-jobs.sh
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
# Only pushes to main and develop commit the cache; pull requests, merge groups and forks
|
||||
# mount a clone and never write back.
|
||||
- name: Mount the compiler cache
|
||||
uses: useblacksmith/stickydisk@3f6be1451e3cad893b778bc93495c80452a5da26 # v1.7.2
|
||||
continue-on-error: true
|
||||
with:
|
||||
key: ${{ github.repository }}-sccache-kernel-rig
|
||||
path: /home/runner/.cache/sccache
|
||||
commit: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') && 'on-change' || 'false' }}
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: kernel-rig
|
||||
save-if: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') }}
|
||||
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
|
||||
shell: bash
|
||||
run: scripts/fetch-smlib.sh
|
||||
- name: Set up compiler cache
|
||||
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
|
||||
with:
|
||||
version: v0.16.0
|
||||
- name: Build the CLI (release, SMLib) and the rig's runner
|
||||
run: |
|
||||
cargo build --release --locked -p solidcraft-cli --features smlib
|
||||
cargo build --locked -p xtask
|
||||
# A tar keeps the executable bits, which an artifact drops. xtask finds the repository from
|
||||
# the path it was built in (CARGO_MANIFEST_DIR), which is the same checkout path in every
|
||||
# job of this workflow.
|
||||
# Every registry command is used by a case or listed in tests/kernel-cases/EXEMPT.
|
||||
- name: Every command has a case
|
||||
run: target/debug/xtask kernel-cases --coverage --cli target/release/solidcraft-cli
|
||||
# A coverage-only run (the registry or the cases changed, no area selected) runs no shard.
|
||||
- name: Pack the binaries
|
||||
if: needs.plan.outputs.shards != '[]'
|
||||
run: tar -cf "$RUNNER_TEMP/kernel-rig-bin.tar" -C target release/solidcraft-cli debug/xtask
|
||||
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
if: needs.plan.outputs.shards != '[]'
|
||||
with:
|
||||
name: kernel-rig-bin
|
||||
path: ${{ runner.temp }}/kernel-rig-bin.tar
|
||||
retention-days: 1
|
||||
- name: Compiler cache statistics
|
||||
if: always()
|
||||
run: sccache --show-stats
|
||||
|
||||
shard:
|
||||
name: Kernel rig (${{ matrix.shard }})
|
||||
needs: [plan, build]
|
||||
if: needs.plan.outputs.shards != '[]'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# The areas of tests/kernel-cases/ the plan selected, and the ladder. On the build box
|
||||
# (aarch64, 4 cases at a time) the slowest areas took about 1 minute (booleans, 212 s of
|
||||
# cases with one of 84 s; threads, 161 s) and the ladder, one case at a time, 92 s: no area
|
||||
# needs splitting.
|
||||
shard: ${{ fromJSON(needs.plan.outputs.shards) }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
name: kernel-rig-bin
|
||||
path: ${{ runner.temp }}
|
||||
- name: Unpack the binaries
|
||||
run: mkdir -p target && tar -xf "$RUNNER_TEMP/kernel-rig-bin.tar" -C target
|
||||
- name: Run the cases against the known-failing list (the ladder against its golden)
|
||||
env:
|
||||
SHARD: ${{ matrix.shard }}
|
||||
run: |
|
||||
if [ "$SHARD" = ladder ]; then
|
||||
set -- --ladder --gate
|
||||
else
|
||||
set -- --area "$SHARD" --known-failing "tests/kernel-cases/$SHARD/KNOWN_FAILING"
|
||||
fi
|
||||
target/debug/xtask kernel-cases "$@" --cli target/release/solidcraft-cli --out "$RUNNER_TEMP/kernel-rig"
|
||||
- name: Keep the results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: kernel-rig-results-${{ matrix.shard }}
|
||||
path: ${{ runner.temp }}/kernel-rig/results.json
|
||||
if-no-files-found: ignore
|
||||
|
||||
# One check for the whole gate, for branch protection and the merge queue.
|
||||
kernel-rig:
|
||||
name: kernel-rig
|
||||
needs: [plan, build, shard]
|
||||
if: always() && (github.event_name != 'pull_request' || !github.event.pull_request.draft)
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
steps:
|
||||
# A skipped build or shard counts as passed only when the plan selected none.
|
||||
- name: Every selected job passed
|
||||
env:
|
||||
PLAN: ${{ needs.plan.result }}
|
||||
WANT_BUILD: ${{ needs.plan.outputs.build }}
|
||||
WANT_SHARDS: ${{ needs.plan.outputs.shards }}
|
||||
BUILD: ${{ needs.build.result }}
|
||||
SHARDS: ${{ needs.shard.result }}
|
||||
run: |
|
||||
echo "plan=$PLAN build=$BUILD shards=$SHARDS selected=$WANT_SHARDS"
|
||||
test "$PLAN" = success
|
||||
if [ "$WANT_BUILD" = true ]; then test "$BUILD" = success; else test "$BUILD" = skipped; fi
|
||||
if [ "$WANT_SHARDS" != '[]' ]; then test "$SHARDS" = success; else test "$SHARDS" = skipped; fi
|
||||
@@ -39,8 +39,6 @@ jobs:
|
||||
run: python3 packaging/tests/nightly_test.py
|
||||
- name: Windows executable icon validation regression
|
||||
run: python3 packaging/tests/windows_icon_test.py
|
||||
- name: AppImage CLI wrapper regression
|
||||
run: python3 packaging/tests/apprun_test.py
|
||||
- name: Flatpak desktop smoke validation regression
|
||||
run: python3 packaging/tests/flatpak_test.py
|
||||
- name: PowerShell
|
||||
|
||||
@@ -41,9 +41,6 @@ jobs:
|
||||
with:
|
||||
targets: wasm32-unknown-unknown
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
|
||||
shell: bash
|
||||
run: scripts/fetch-smlib.sh wasm32-unknown-unknown
|
||||
- name: Install mdbook and trunk
|
||||
run: |
|
||||
mkdir -p "$HOME/.local/bin"
|
||||
@@ -68,7 +65,6 @@ jobs:
|
||||
run: |
|
||||
mkdir -p site
|
||||
cp -r target/book/. site/
|
||||
bash docs/book/seo.sh site # canonical links, robots.txt, sitemap.xml
|
||||
cp -r dist/web site/app
|
||||
- uses: actions/configure-pages@v5
|
||||
id: pages
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
# Two quick checks on every pull request (CONTRIBUTING.md, "Sending a change"): no second open PR
|
||||
# for an issue a lower-numbered one already closes, and the "Changes a default:" line is filled in.
|
||||
# Neither is a required status check. The logic is in scripts/pr-checks/pr-checks.js (tested in ci.yml).
|
||||
name: PR checks
|
||||
|
||||
on:
|
||||
# pull_request, never pull_request_target: nothing here checks out or runs the PR's code, and the
|
||||
# token is read-only for fork PRs. Only scripts/pr-checks from the PR's own merge commit is run.
|
||||
pull_request:
|
||||
types: [opened, edited, synchronize, reopened, ready_for_review, labeled, unlabeled]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
concurrency:
|
||||
group: pr-checks-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
duplicate:
|
||||
name: Duplicate PR check
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
sparse-checkout: scripts/pr-checks
|
||||
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const checks = require('./scripts/pr-checks/pr-checks.js');
|
||||
const r = await checks.checkDuplicate({ github, context, core });
|
||||
core.info(JSON.stringify(r));
|
||||
|
||||
defaults:
|
||||
name: Defaults line check
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
sparse-checkout: scripts/pr-checks
|
||||
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const checks = require('./scripts/pr-checks/pr-checks.js');
|
||||
const r = await checks.checkDefaults({ github, context, core });
|
||||
core.info(JSON.stringify(r));
|
||||
|
||||
# Labelling needs write access, which a fork PR's token never has, so it is its own job and best
|
||||
# effort: a fork PR just doesn't get the `changes-default` label from here.
|
||||
label:
|
||||
name: Label changes-default
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
continue-on-error: true
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
sparse-checkout: scripts/pr-checks
|
||||
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const checks = require('./scripts/pr-checks/pr-checks.js');
|
||||
await checks.syncDefaultLabel({ github, context, core });
|
||||
@@ -1,74 +0,0 @@
|
||||
# The release gate: everything, in full and in parallel, on one branch's head, before that branch
|
||||
# replaces main as a release (develop, rebased onto main). Run it by hand:
|
||||
# gh workflow run release-gate.yml --ref develop
|
||||
# It calls the workflows that own each check instead of copying their jobs:
|
||||
# - ci.yml as Run workflow with cross-compilation: every native suite in full (not scoped), the
|
||||
# reference tests, the doc tests, the GPU path tests, the action matrix (all 16 shards) and its
|
||||
# determinism check, the property tests, the repository checks (web-offline among them),
|
||||
# WebAssembly and RISC-V, Windows D3D12 FXC and Windows app startup;
|
||||
# - kernel-rig.yml: every rig area against its KNOWN_FAILING and the ladder against its golden;
|
||||
# - release.yml as a dry run (draft false, packages_only): the Linux, Flatpak, Snap, FreeBSD,
|
||||
# macOS, Windows and web packages with their smoke tests and package checks, unsigned (no
|
||||
# secrets are passed, so no signing secret or SignPath quota is used) and without the release
|
||||
# job (checksums, update signing, publishing).
|
||||
# The gate passes only when all three pass. Before main is replaced, tag the old main
|
||||
# (main-before-develop-<date>) and push the tag.
|
||||
name: Release gate
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: release-gate-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
name: CI
|
||||
uses: ./.github/workflows/ci.yml
|
||||
permissions:
|
||||
contents: read
|
||||
# Select checks reads this commit's other runs.
|
||||
actions: read
|
||||
with:
|
||||
cross_compile: true
|
||||
|
||||
kernel-rig:
|
||||
name: Kernel rig
|
||||
uses: ./.github/workflows/kernel-rig.yml
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
packages:
|
||||
name: Packages
|
||||
uses: ./.github/workflows/release.yml
|
||||
# No secrets: the signing steps skip without them. GitHub checks a called workflow's job
|
||||
# permissions before the run, skipped jobs included, so the release job's contents: write must
|
||||
# be granted here; packages_only skips that job, and no step that runs writes to the
|
||||
# repository. actions: read is the Windows job's.
|
||||
permissions:
|
||||
contents: write
|
||||
actions: read
|
||||
with:
|
||||
draft: false
|
||||
packages_only: true
|
||||
|
||||
release-gate:
|
||||
name: release-gate
|
||||
needs: [ci, kernel-rig, packages]
|
||||
if: always()
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Every part passed
|
||||
env:
|
||||
CI: ${{ needs.ci.result }}
|
||||
KERNEL_RIG: ${{ needs.kernel-rig.result }}
|
||||
PACKAGES: ${{ needs.packages.result }}
|
||||
run: |
|
||||
echo "ci=$CI kernel-rig=$KERNEL_RIG packages=$PACKAGES"
|
||||
test "$CI" = success
|
||||
test "$KERNEL_RIG" = success
|
||||
test "$PACKAGES" = success
|
||||
+10
-226
@@ -4,14 +4,11 @@
|
||||
# comes from [workspace.package] in Cargo.toml. An agent verifies the artifacts and publishes
|
||||
# the draft separately; see docs/releasing.md. Tags and pushes to main do not trigger releases.
|
||||
# At midnight Eastern, changed main commits build and publish a nightly pre-release.
|
||||
# A version with a suffix (0.2.0-rc.1) is a pre-release.
|
||||
# The macOS app is signed and notarized when the APPLE_* secrets exist. The Windows binaries and
|
||||
# MSI are Authenticode-signed through SignPath Foundation when the SIGNPATH_API_TOKEN secret exists
|
||||
# (see the windows job). Nothing else is signed yet.
|
||||
# A version with a suffix (0.2.0-rc.1) is a pre-release. Nothing is signed yet.
|
||||
#
|
||||
# Linux x86_64, aarch64 AppImage (+ .zsync), .deb, .rpm, .flatpak, .snap, .tar.gz packaging/linux
|
||||
# FreeBSD x86_64 .tar.gz (built in a FreeBSD VM) packaging/freebsd
|
||||
# macOS universal .dmg, and the CLI as a .zip (both archs built in parallel) packaging/macos
|
||||
# macOS universal .dmg, and the CLI as a .zip packaging/macos
|
||||
# Windows x64, x86, arm64 .msi and a portable .zip; x64 also the plain .exe packaging/windows
|
||||
# Web a .zip of the static site packaging/web
|
||||
#
|
||||
@@ -20,10 +17,6 @@
|
||||
# releases/latest/download/<name> links in the README keep working across releases.
|
||||
#
|
||||
# A dry run: dispatch with draft=false (the default). Only the release branch may create drafts.
|
||||
# The release gate (release-gate.yml) calls it as such a dry run, without secrets (unsigned) and
|
||||
# without the release job (packages_only).
|
||||
# A dry run from any branch also test-signs the Windows files when SIGNPATH_API_TOKEN exists:
|
||||
# gh workflow run release.yml --ref <branch> -f draft=false [-f signpath_signing_policy=test-signing]
|
||||
name: Release
|
||||
|
||||
on:
|
||||
@@ -38,29 +31,6 @@ on:
|
||||
description: "Create/update a draft (release branch only; off: build artifacts only)"
|
||||
type: boolean
|
||||
default: false
|
||||
signpath_signing_policy:
|
||||
description: "SignPath signing policy for the Windows files (empty: the repository variable, else test-signing)"
|
||||
type: string
|
||||
default: ""
|
||||
# The release gate (release-gate.yml): a dry run (draft false) of every package and its checks.
|
||||
# The event stays the caller's (workflow_dispatch), so a call runs as a dispatched dry run does.
|
||||
# The gate passes no secrets, so nothing is signed (the signing steps skip without their
|
||||
# secrets, as on a fork), and packages_only skips the release job (checksums, update signing,
|
||||
# publishing).
|
||||
workflow_call:
|
||||
inputs:
|
||||
packages_only:
|
||||
description: "Build and check every package only: no release job"
|
||||
type: boolean
|
||||
default: false
|
||||
draft:
|
||||
description: "Create/update a draft (release branch only; off: build artifacts only)"
|
||||
type: boolean
|
||||
default: false
|
||||
signpath_signing_policy:
|
||||
description: "SignPath signing policy for the Windows files (empty: the repository variable, else test-signing)"
|
||||
type: string
|
||||
default: ""
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -125,9 +95,6 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: release-icons
|
||||
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
|
||||
shell: bash
|
||||
run: scripts/fetch-smlib.sh
|
||||
- run: cargo build --release --locked -p solidcraft-cli
|
||||
- run: |
|
||||
. packaging/env.sh
|
||||
@@ -167,9 +134,6 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: release-linux-${{ matrix.arch }}
|
||||
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
|
||||
shell: bash
|
||||
run: scripts/fetch-smlib.sh
|
||||
- name: Build and package (AppImage, deb, rpm, tar.gz)
|
||||
run: packaging/linux/package.sh
|
||||
- name: Check the packages
|
||||
@@ -331,19 +295,11 @@ jobs:
|
||||
path: dist/release/*
|
||||
if-no-files-found: error
|
||||
|
||||
# The two halves of the universal binary build in parallel on their own runners (each alone
|
||||
# took about half of the old single job's 45 minutes); `macos` below joins and packages them.
|
||||
macos-build:
|
||||
name: macOS build ${{ matrix.arch }}
|
||||
macos:
|
||||
name: macOS universal
|
||||
needs: version
|
||||
if: needs.version.outputs.build == 'true'
|
||||
runs-on: macos-15
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- { arch: aarch64, target: aarch64-apple-darwin }
|
||||
- { arch: x86_64, target: x86_64-apple-darwin }
|
||||
runs-on: macos-14
|
||||
env:
|
||||
SOLIDCRAFT_VERSION: ${{ needs.version.outputs.version }}
|
||||
SOLIDCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
|
||||
@@ -351,61 +307,12 @@ jobs:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
targets: aarch64-apple-darwin, x86_64-apple-darwin
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: release-macos-${{ matrix.arch }}
|
||||
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
|
||||
shell: bash
|
||||
run: scripts/fetch-smlib.sh ${{ matrix.target }}
|
||||
- name: Build
|
||||
run: packaging/macos/package.sh --build-only ${{ matrix.target }}
|
||||
# Not named release-*: the release job collects every release-* artifact as an asset.
|
||||
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: macos-bin-${{ matrix.arch }}
|
||||
path: |
|
||||
target/${{ matrix.target }}/release/solidcraft
|
||||
target/${{ matrix.target }}/release/solidcraft-cli
|
||||
target/${{ matrix.target }}/release/solvecraft-cli
|
||||
if-no-files-found: error
|
||||
|
||||
macos:
|
||||
name: macOS universal
|
||||
needs: [version, macos-build]
|
||||
if: needs.version.outputs.build == 'true'
|
||||
runs-on: macos-15
|
||||
env:
|
||||
SOLIDCRAFT_VERSION: ${{ needs.version.outputs.version }}
|
||||
SOLIDCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
name: macos-bin-aarch64
|
||||
path: target/aarch64-apple-darwin/release
|
||||
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
name: macos-bin-x86_64
|
||||
path: target/x86_64-apple-darwin/release
|
||||
- name: Set up Developer ID signing and notarization
|
||||
# Optional until the APPLE_* secrets exist (docs/releasing.md, "macOS signing"). Without
|
||||
# them (forks, dry runs) the app keeps its ad-hoc signature and is not notarized.
|
||||
env:
|
||||
APPLE_CERTIFICATE_P12: ${{ secrets.APPLE_CERTIFICATE_P12 }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
APPLE_API_KEY_P8: ${{ secrets.APPLE_API_KEY_P8 }}
|
||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
||||
run: packaging/macos/signing.sh --setup
|
||||
- name: Join, sign, package and check (dmg, CLI zip; the dmg is mounted and checked)
|
||||
run: |
|
||||
# Artifacts don't keep the executable bit.
|
||||
chmod +x target/*-apple-darwin/release/solidcraft target/*-apple-darwin/release/solidcraft-cli target/*-apple-darwin/release/solvecraft-cli
|
||||
packaging/macos/package.sh --arch universal --skip-build ${MACOS_SIGN_IDENTITY:+--sign}
|
||||
- name: Remove the signing keychain and API key
|
||||
if: always()
|
||||
run: packaging/macos/signing.sh --cleanup
|
||||
key: release-macos-universal
|
||||
- name: Build and package (dmg, CLI zip; the dmg is mounted and checked)
|
||||
run: packaging/macos/package.sh --arch universal
|
||||
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: release-macos
|
||||
@@ -416,14 +323,7 @@ jobs:
|
||||
name: Windows ${{ matrix.arch }}
|
||||
needs: [version, icons]
|
||||
if: needs.version.outputs.build == 'true'
|
||||
# Must stay a GitHub-hosted runner, like every job before it: SignPath's open-source policy
|
||||
# only signs builds whose jobs all ran on GitHub-hosted runners.
|
||||
runs-on: windows-latest
|
||||
# actions: read lets the SignPath action read this job's details and fetch the uploaded
|
||||
# artifact with GITHUB_TOKEN (the workflow default above grants contents: read only).
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -434,16 +334,6 @@ jobs:
|
||||
- { arch: arm64, target: aarch64-pc-windows-msvc }
|
||||
env:
|
||||
SOLIDCRAFT_VERSION: ${{ needs.version.outputs.version }}
|
||||
# Authenticode signing through SignPath Foundation (docs/releasing.md, "Windows signing").
|
||||
# Only when the API token secret exists: forks and repositories without it build unsigned,
|
||||
# exactly as before. `if:` can't read secrets, so the check goes through this variable.
|
||||
SIGN_WINDOWS: ${{ secrets.SIGNPATH_API_TOKEN != '' }}
|
||||
# The one switch between the self-signed test certificate (test-signing) and the real one
|
||||
# (release-signing): a dispatch input, else the SIGNPATH_SIGNING_POLICY repository variable,
|
||||
# else test-signing.
|
||||
SIGNPATH_SIGNING_POLICY: ${{ inputs.signpath_signing_policy || vars.SIGNPATH_SIGNING_POLICY || 'test-signing' }}
|
||||
SIGNPATH_ORGANIZATION_ID: fa603ff4-c702-404b-8588-a524371c7a4e
|
||||
SIGNPATH_PROJECT_SLUG: solidcraft
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
@@ -456,118 +346,16 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: release-windows-${{ matrix.arch }}
|
||||
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
|
||||
shell: bash
|
||||
run: scripts/fetch-smlib.sh ${{ matrix.target }}
|
||||
- name: Install WiX
|
||||
shell: pwsh
|
||||
run: |
|
||||
dotnet tool install --global wix --version 5.0.2
|
||||
wix extension add -g WixToolset.UI.wixext/5.0.2 WixToolset.Util.wixext/5.0.2
|
||||
- name: Build and package (msi, portable zip)
|
||||
if: env.SIGN_WINDOWS != 'true'
|
||||
shell: pwsh
|
||||
env:
|
||||
SOLIDCRAFT_ICONS: ${{ github.workspace }}/icons
|
||||
run: ./packaging/windows/package.ps1 -Arch ${{ matrix.arch }}
|
||||
|
||||
# ---- signed path ---------------------------------------------------------------------------
|
||||
# SignPath's GitHub integration (docs.signpath.io/trusted-build-systems/github): the files go
|
||||
# up as a workflow artifact, SignPath fetches that artifact itself (so it can verify it came
|
||||
# from this workflow run), signs it, and the action downloads the signed copy. GitHub stores
|
||||
# artifacts as zips, so packaging/windows/signpath-artifact-configuration.xml (pasted into the
|
||||
# SignPath project) describes a zip holding the files at its top level.
|
||||
# Two rounds: the .exe files first, so the MSI, the portable zip and the plain .exe all carry
|
||||
# signed binaries; then the MSI built from them.
|
||||
- name: Build and stage the binaries for signing
|
||||
if: env.SIGN_WINDOWS == 'true'
|
||||
shell: pwsh
|
||||
env:
|
||||
SOLIDCRAFT_ICONS: ${{ github.workspace }}/icons
|
||||
run: ./packaging/windows/package.ps1 -Arch ${{ matrix.arch }} -Phase stage
|
||||
- name: Upload the unsigned binaries
|
||||
id: unsigned-exes
|
||||
if: env.SIGN_WINDOWS == 'true'
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: signpath-unsigned-exes-windows-${{ matrix.arch }}
|
||||
path: target/windows-package/${{ matrix.arch }}/*.exe
|
||||
if-no-files-found: error
|
||||
retention-days: 3
|
||||
- name: Sign the binaries (SignPath, ${{ env.SIGNPATH_SIGNING_POLICY }})
|
||||
if: env.SIGN_WINDOWS == 'true'
|
||||
uses: signpath/github-action-submit-signing-request@f6d04783b4569d051e0c80105fe66e82819d0092 # v3.0
|
||||
with:
|
||||
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
|
||||
organization-id: ${{ env.SIGNPATH_ORGANIZATION_ID }}
|
||||
project-slug: ${{ env.SIGNPATH_PROJECT_SLUG }}
|
||||
signing-policy-slug: ${{ env.SIGNPATH_SIGNING_POLICY }}
|
||||
github-artifact-id: ${{ steps.unsigned-exes.outputs.artifact-id }}
|
||||
wait-for-completion: true
|
||||
# release-signing requests wait for a person to approve them in SignPath; give that an
|
||||
# hour instead of the default 10 minutes. test-signing requests finish in seconds.
|
||||
wait-for-completion-timeout-in-seconds: 3600
|
||||
output-artifact-directory: target/windows-package/signed-exes-${{ matrix.arch }}
|
||||
- name: Put the signed binaries in place
|
||||
if: env.SIGN_WINDOWS == 'true'
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$stage = "target/windows-package/${{ matrix.arch }}"
|
||||
foreach ($exe in 'solidcraft.exe', 'solidcraft-cli.exe', 'solvecraft-cli.exe') {
|
||||
$signed = "target/windows-package/signed-exes-${{ matrix.arch }}/$exe"
|
||||
# The artifact configuration matches each file optionally, so check that every file
|
||||
# actually came back signed rather than trusting the request's success.
|
||||
$sig = Get-AuthenticodeSignature $signed
|
||||
if (-not $sig.SignerCertificate) { throw "$exe came back from SignPath without a signature ($($sig.Status))" }
|
||||
# The test certificate is self-signed, so Status is not Valid until release-signing.
|
||||
Write-Output "$exe signed by $($sig.SignerCertificate.Subject) (status: $($sig.Status))"
|
||||
Copy-Item -Force $signed "$stage/$exe"
|
||||
}
|
||||
- name: Package the signed binaries (msi, portable zip)
|
||||
if: env.SIGN_WINDOWS == 'true'
|
||||
shell: pwsh
|
||||
env:
|
||||
SOLIDCRAFT_ICONS: ${{ github.workspace }}/icons
|
||||
run: ./packaging/windows/package.ps1 -Arch ${{ matrix.arch }} -Phase package
|
||||
- name: Upload the unsigned MSI
|
||||
id: unsigned-msi
|
||||
if: env.SIGN_WINDOWS == 'true'
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: signpath-unsigned-msi-windows-${{ matrix.arch }}
|
||||
path: dist/release/*.msi
|
||||
if-no-files-found: error
|
||||
retention-days: 3
|
||||
- name: Sign the MSI (SignPath, ${{ env.SIGNPATH_SIGNING_POLICY }})
|
||||
if: env.SIGN_WINDOWS == 'true'
|
||||
uses: signpath/github-action-submit-signing-request@f6d04783b4569d051e0c80105fe66e82819d0092 # v3.0
|
||||
with:
|
||||
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
|
||||
organization-id: ${{ env.SIGNPATH_ORGANIZATION_ID }}
|
||||
project-slug: ${{ env.SIGNPATH_PROJECT_SLUG }}
|
||||
signing-policy-slug: ${{ env.SIGNPATH_SIGNING_POLICY }}
|
||||
github-artifact-id: ${{ steps.unsigned-msi.outputs.artifact-id }}
|
||||
wait-for-completion: true
|
||||
wait-for-completion-timeout-in-seconds: 3600
|
||||
output-artifact-directory: target/windows-package/signed-msi-${{ matrix.arch }}
|
||||
- name: Put the signed MSI in place
|
||||
if: env.SIGN_WINDOWS == 'true'
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$msi = Get-Item "dist/release/*.msi"
|
||||
$signed = "target/windows-package/signed-msi-${{ matrix.arch }}/$($msi.Name)"
|
||||
$sig = Get-AuthenticodeSignature $signed
|
||||
if (-not $sig.SignerCertificate) { throw "$($msi.Name) came back from SignPath without a signature ($($sig.Status))" }
|
||||
Write-Output "$($msi.Name) signed by $($sig.SignerCertificate.Subject) (status: $($sig.Status))"
|
||||
Copy-Item -Force $signed $msi.FullName
|
||||
"Windows ${{ matrix.arch }}: Authenticode-signed through SignPath (policy $env:SIGNPATH_SIGNING_POLICY, $($sig.SignerCertificate.Subject))." >> $env:GITHUB_STEP_SUMMARY
|
||||
- name: Note the unsigned build
|
||||
if: env.SIGN_WINDOWS != 'true'
|
||||
shell: pwsh
|
||||
run: '"Windows ${{ matrix.arch }}: unsigned (no SIGNPATH_API_TOKEN secret)." >> $env:GITHUB_STEP_SUMMARY'
|
||||
|
||||
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: release-windows-${{ matrix.arch }}
|
||||
@@ -590,9 +378,6 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: release-web
|
||||
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
|
||||
shell: bash
|
||||
run: scripts/fetch-smlib.sh wasm32-unknown-unknown
|
||||
- name: Install trunk
|
||||
run: |
|
||||
curl -fsSL "https://github.com/trunk-rs/trunk/releases/download/v${TRUNK_VERSION}/trunk-x86_64-unknown-linux-gnu.tar.gz" \
|
||||
@@ -608,8 +393,7 @@ jobs:
|
||||
release:
|
||||
name: GitHub Release or dry-run artifacts
|
||||
needs: [version, linux, flatpak, snap, freebsd, macos, windows, web]
|
||||
# Not for the release gate (packages_only): it builds and checks the packages, nothing more.
|
||||
if: needs.version.outputs.build == 'true' && !inputs.packages_only
|
||||
if: needs.version.outputs.build == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
@@ -11,7 +11,3 @@ __pycache__/
|
||||
|
||||
# cargo-mutants output (docs/mutation-testing.md)
|
||||
mutants.out*/
|
||||
|
||||
# external corpus downloads (cargo xtask external-corpus, #1397): fetched at test time,
|
||||
# hash-pinned in tests/external-corpus.toml; never commit case data
|
||||
/corpus/
|
||||
|
||||
@@ -1,48 +1,216 @@
|
||||
# SolidCraft: instructions for agents
|
||||
# SolidCraft — instructions for agents
|
||||
|
||||
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.
|
||||
SolidCraft is a clean-room, open-source, pure-Rust parametric 3D CAD application:
|
||||
sketches with a constraint solver, a parametric timeline of features, a B-rep
|
||||
kernel, and a desktop UI that agents can drive. Projects published by
|
||||
[storytold](https://github.com/storytold) can serve as references for implementation conventions.
|
||||
Any code adapted from other projects (MIT OR Apache-2.0) is noted in ATTRIBUTION.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.
|
||||
## Product name
|
||||
SolidCraft everywhere: crates `solidcraft-*`, binaries `solidcraft` and `solidcraft-cli`,
|
||||
`.solidcraft` designs, the `solidcraft/N` format tag, `SOLIDCRAFT_*` environment variables,
|
||||
settings and recovery folders, the MCP server name, docs, CI and packaging. The old name
|
||||
survives only in the explicit compatibility shims listed in `docs/compatibility.md`; don't add
|
||||
new uses of it anywhere else. Public docs/web/Flatpak deployments use project Pages
|
||||
`/solidcraft/`; do not add permanent old-name Pages aliases.
|
||||
|
||||
## 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`.
|
||||
- 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`.
|
||||
- Everything is a command and programmatic calls never open dialogs: `skills/solidcraft-commands/SKILL.md`.
|
||||
- Layering (geom, sketch/kernel/render, doc, io, engine, ui-egui, apps); only `crates/kernel` uses truck: `cargo xtask layers`.
|
||||
- One outer UI container, one camera path (`navigation::handle`), colours from theme tokens: `cargo xtask ui-containers`.
|
||||
- Application code is Rust only and builds for wasm: `cargo xtask wasm`, `cargo xtask web-offline-test`.
|
||||
- The product name is SolidCraft; the old name lives only in `docs/compatibility.md`.
|
||||
- Sorted lists (message catalogs, `mod` lines, languages) stay sorted: `cargo xtask lists`. Never edit generated files (`docs/features.md`, `THIRD-PARTY-LICENSES.txt`) in a PR.
|
||||
- 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 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`.
|
||||
- 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.
|
||||
## Start every session here
|
||||
1. Read `plan/STATUS.md` (current milestone, next task), then the task in `plan/execution-plan.md`
|
||||
and the relevant `plan/architecture.md` section.
|
||||
2. Kernel and library decisions: `plan/adr/0001-geometry-kernel.md` (what we may depend on, what
|
||||
is rejected for licence reasons).
|
||||
3. Follow the autonomous operation protocol (`plan/execution-plan.md` §6). Don't stop to ask
|
||||
unless it lists the decision as the owner's.
|
||||
|
||||
## Working on X: read the skill first
|
||||
| Working on | Read |
|
||||
|---|---|
|
||||
| A PR, a claim, review, CI or evidence | `skills/solidcraft-contributing/SKILL.md` |
|
||||
| Licences, assets, clean-room, the name | `skills/solidcraft-ground-rules/SKILL.md` |
|
||||
| Non-test Rust, hostile input, a crash report | `skills/solidcraft-crash-safety/SKILL.md` |
|
||||
| A command, a crate boundary | `skills/solidcraft-commands/SKILL.md` |
|
||||
| A dialog, panel, camera, icon, colour | `skills/solidcraft-ui/SKILL.md` |
|
||||
| Labels, messages, translations | `skills/solidcraft-ui-text/SKILL.md` |
|
||||
| Saving something new in a design, import, export | `skills/solidcraft-file-formats/SKILL.md` |
|
||||
| Preferences, a changed default | `skills/solidcraft-preferences/SKILL.md` |
|
||||
| Writing or running tests | `skills/solidcraft-tests/SKILL.md` |
|
||||
| Slow work, the window thread, wasm | `skills/solidcraft-performance/SKILL.md` |
|
||||
| Releases, installers, packages | `skills/solidcraft-packaging/SKILL.md` |
|
||||
| Running the app, control channel, MCP tools | `skills/solidcraft-agent-tools/SKILL.md` |
|
||||
| Building a part over MCP | `skills/solidcraft-modeling/SKILL.md` |
|
||||
| Changing an existing design | `skills/solidcraft-editing/SKILL.md` |
|
||||
| A bug report, a crash, a wrong result | `skills/solidcraft-reporting/SKILL.md` |
|
||||
| Checking, porting, designs in git | `skills/solidcraft-checking/SKILL.md` |
|
||||
| Assemblies, joints, motion | `skills/solidcraft-mechanisms/SKILL.md` |
|
||||
`plan/` is gitignored (local only).
|
||||
|
||||
## Never crash
|
||||
People trust SolidCraft with their designs; a crash loses their work. **This outranks feature work.**
|
||||
- **No panics in non-test code:** no `unwrap()`, `expect()`, `panic!`, `unreachable!`, `todo!`,
|
||||
`unimplemented!`; no `unsafe` (`unsafe_code = "forbid"`). The exceptions, in the app crate and in
|
||||
`crates/threedx-client` (which deny it instead), have no safe API: the macOS-only
|
||||
`crates/threedx-client/src/macos.rs` (loads the 3Dconnexion driver's client framework at run time
|
||||
and calls through its function pointers), two functions in
|
||||
`apps/solidcraft/src/open_documents.rs` (macOS hands Finder files only to an Objective-C
|
||||
method), the Windows-only `apps/solidcraft/src/native_crash.rs` (the exception filter that
|
||||
records crashes inside native code such as graphics drivers), the Windows-only
|
||||
`apps/solidcraft/src/native_frame.rs` (the window procedure that answers the resize-border hit
|
||||
test of the window without OS decorations) and the Windows-only
|
||||
`apps/solidcraft/src/space_mouse/windows_hid.rs` (asks Plug and Play which HID interfaces are 3D
|
||||
mice, so no other device is sent anything).
|
||||
- **Errors are `Result<T, E>`** through the crate's error type and `?`. An unfinished feature
|
||||
returns an error ("not supported yet: …"); it never panics.
|
||||
- **Input-derived numbers are hostile** (command parameters, design files, control-channel
|
||||
input): `get()` not `[i]`, finite checks, caps on sizes and loop counts.
|
||||
- **Third-party kernel calls are guarded:** everything that calls `truck` goes through
|
||||
`solidcraft_kernel::guard`, which turns a panic into `KernelError::Internal`.
|
||||
- **Last-resort guard:** `Session::execute` runs every command under `catch_unwind`; an escaped
|
||||
panic restores the design and reports `EngineError::Internal`.
|
||||
- **Crash reports:** the desktop app writes every panic and every unclean exit to a local crash folder,
|
||||
with the design and the commands that reproduce it (`solidcraft_engine::crash`, docs/crash-reports.md).
|
||||
When someone says the app crashed, look there first: `solidcraft-cli crashes`, `crashes show <id>`,
|
||||
`crashes replay <id>` (or the `diagnostics.crash_reports` command / MCP `crash_reports` tool).
|
||||
- **Prove it:** every crash fix lands with a regression test (see `hostile_params_never_panic`); a
|
||||
crash report's `design.solidcraft` + `replay.json` is the test's starting point.
|
||||
- **Where tests go:** a regression test goes into the existing test file for its area (booleans, blends,
|
||||
shell, sweep, STEP, naming…), never a new file named after the bug (#3226).
|
||||
- Every production crate root carries
|
||||
`#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]`.
|
||||
|
||||
## Non-negotiables
|
||||
- **Independent.** SolidCraft is independent. Behaviour is designed from public documentation,
|
||||
general CAD conventions and user feedback. Reference tests use analytic values and open-source
|
||||
tools. Never copy another product's code, icons, artwork, help text, tooltips, templates or
|
||||
materials. File formats come from public specifications.
|
||||
- **No commercial CAD products.** Agents must not drive, script, automate or extract data from
|
||||
commercial CAD products (Fusion, SolidWorks, Onshape, …) for this project: no test data,
|
||||
measurements, command lists, ids or files produced with them.
|
||||
- **Never read, copy or link GPL/LGPL/AGPL code**: OpenCASCADE, FreeCAD (incl. planegcs),
|
||||
SolveSpace, CadQuery/OCP, LibreCAD, QCAD — don't even read their sources. (`tools/freecad-oracle/`
|
||||
runs FreeCAD as a separate program through its public scripting API to compute reference values;
|
||||
nothing from FreeCAD is linked, vendored or shipped.) The sketch solver and the blend
|
||||
operations are our own.
|
||||
- **Assets — absolutely essential.** SolidCraft contains **no iconography, images, fonts or
|
||||
artwork from commercial CAD products — ever.** Command icons are the original SVG masters in
|
||||
`assets/icons/ui/design/svg/`, compiled through `crates/ui-egui/src/icon_svg.rs`.
|
||||
**Before any icon, icon assignment or media edit, read `assets/AGENTS.md`,
|
||||
`assets/icons/ui/AGENTS.md` and `assets/icons/ui/STYLE_GUIDE.md`.** Use the canonical
|
||||
bases and named finished references there; never invent a parallel icon style. Any file asset must be original or openly licensed and **must have a row in ATTRIBUTION.md**
|
||||
(`cargo xtask assets` enforces it). Screenshots of commercial CAD software are never committed.
|
||||
Breaking this rule is the most serious mistake you can make in this repo.
|
||||
- **Everything is a command.** User-visible behaviour = a command in `crates/engine/src/cmd/*`
|
||||
(`CommandSpec`: id = our own dotted, lower-case id `<area>.<thing>[.<variant>]` such as
|
||||
`solid.extrude` or `sketch.rectangle.two_point`, never another product's command id; label;
|
||||
toolbar tab and panel; icon; shortcut; params doc; `enabled`; JSON `run`) + tests. The toolbar,
|
||||
dialogs, sketch tools, palette, scripts, CLI and control channel all reach the same commands.
|
||||
- **Programmatic calls never open dialogs.** `engine.execute` runs the JSON form; only toolbar
|
||||
clicks (`ui.start`) start interactive tools and dialogs, which then run commands.
|
||||
- **Layering** is enforced by `cargo xtask layers`: L0 `geom` → L1 `sketch`, `kernel`, `render`
|
||||
→ L2 `doc` → L3 `io` → L4 `engine` → L5 `ui-egui` → apps. Nothing below L5 depends on
|
||||
egui/eframe/winit/wgpu/rfd. **The UI crate is swappable.** Only `crates/kernel` may use `truck`.
|
||||
- **The UI is thin**: panels read engine state and act through `app.run(id, params)` /
|
||||
`app.start(id)`. Colours come from `theme::Tokens`.
|
||||
- **One outer UI container**: all panels, floating sheets, menus, overlays and platform UI
|
||||
windows use `ui_kit::Container` (directly or through `Panel`, `Window`, `Confirm`, `Menu`).
|
||||
Declare the semantic `Surface`; workspace panels use its shared layout policy and chrome.
|
||||
System/report windows use `ui_kit::Window`; its low-level host builder is kit-private.
|
||||
Do not construct raw egui hosts, outer popup/window frames or shadows in feature modules.
|
||||
`cargo xtask ui-containers` recursively checks source/examples in all current and future crates
|
||||
and apps. It runs in the fast repository CI job and `cargo xtask ci`, with no caller allowances.
|
||||
See `docs/ui-container-audit.md` for the inventory and legitimate content/native exceptions.
|
||||
- **One set of camera controls**: every page or canvas with a camera (viewport, Render
|
||||
preview, future flat pattern or drawing pages) calls `navigation::handle` with its `NavCaps`
|
||||
(orbit off for flat 2D pages). New canvases must use `navigation::handle`; never hand-roll
|
||||
camera input (a test rejects it).
|
||||
- **Application code is Rust only.** Units are millimetres and radians internally; Z is up.
|
||||
Local asset authoring, lint and review tools use Python 3.11+ as documented in
|
||||
`assets/AGENTS.md`; Python is not an application runtime dependency. The application exception is
|
||||
JavaScript the browser requires: the service worker `apps/solidcraft-web/sw.js` (browsers only
|
||||
run service workers as JavaScript) and the small inline scripts in `apps/solidcraft-web/index.html`.
|
||||
Keep them minimal; `cargo xtask web-offline-test` covers the service worker.
|
||||
- **Quality gates** before every commit: `cargo xtask ci` (fmt, clippy -D warnings, tests,
|
||||
assets, layers). Commit after every feature arc that builds.
|
||||
- **Publishing**: the repository is github.com/tangency-labs/solidcraft. Changes land through
|
||||
**pull requests** and the **merge queue**: push a branch and open a PR. The heavy CI jobs start
|
||||
when a maintainer adds `approved-for-ci` after reviewing the code. Approved PRs go into the queue
|
||||
with `gh pr merge N -R tangency-labs/solidcraft --squash --auto`; the queue runs the full suite
|
||||
on each group before it lands on `main`. Keep each PR to one topic and one issue, and never
|
||||
force-push `main`. Never commit `plan/`, build dirs (`target*/`) or generated reference data;
|
||||
check `git ls-files` before pushing.
|
||||
- **Never break wasm**: everything below the apps must build for `wasm32-unknown-unknown`
|
||||
(`cargo xtask wasm`, part of `cargo xtask ci`).
|
||||
|
||||
## Showing and following through on changes
|
||||
|
||||
- Help reviewers see what is broken or changed. For observable CAD, UI or model behavior,
|
||||
issues and PRs must include actual descriptive screenshots or a short recording from a
|
||||
small reproduction. This includes kernel, geometry, assembly and backend changes whose
|
||||
effects can be shown in the app. Coding agents should run the app and capture this evidence
|
||||
themselves; do not ask a human for evidence the agent can produce. Show the actual failure
|
||||
or result, preferably before and after, with brief reproduction steps, expected/actual
|
||||
behavior and annotations that point out the relevant detail.
|
||||
- Flowcharts and Mermaid diagrams cannot substitute for screenshots or recordings of visible
|
||||
behavior. Use diagrams for architectural reasoning when actual screenshots cannot illustrate
|
||||
the point. Changes with no useful observable behavior do not need screenshot or video evidence.
|
||||
- Use a small public-safe example and check for private designs, paths, account details or
|
||||
other sensitive content before sharing. Attach media to the issue/PR or link to an accessible
|
||||
recording; do not commit generated screenshots or videos just to provide review evidence.
|
||||
- Throughout the work and before handoff, check the relevant PR's merge status. Don't push to
|
||||
an open PR only to catch up with `main` (no merge of `main`, no Update branch, no rebase for
|
||||
that alone): every push restarts the whole CI run, and `main`'s own run catches what lands in
|
||||
between, to be fixed forward. When you push a change anyway, or GitHub reports a conflict,
|
||||
rebase onto the latest `main` (push your own branch with `--force-with-lease`); never merge
|
||||
`main` into a PR. Resolve conflicts without discarding others' work; never force-push `main`.
|
||||
Don't re-run CI on an unchanged commit: a real failure fails again. Re-run only a job that
|
||||
runner trouble cancelled, with "Re-run failed jobs". Report the PR state you checked and any
|
||||
conflicts resolved. If a PR remains open, keep monitoring it while the task is active and
|
||||
make its pending status clear at handoff; do not claim it merged without checking.
|
||||
|
||||
## Before you start: look at what is already open
|
||||
- Work with an authenticated GitHub CLI (`gh auth status`) and `git`; the checks below need both.
|
||||
- Pull the whole list, not one search: `gh pr list -R tangency-labs/solidcraft --state open --limit 500`
|
||||
(drafts included) and `gh issue list -R tangency-labs/solidcraft --state open --limit 500`. Look for
|
||||
work on the same issue, the same feature under another number, and PRs that change the files
|
||||
you are about to change (`gh pr list ... --json number,title,files`).
|
||||
- If someone already has it, comment on their PR or issue instead of opening a second one. Two
|
||||
PRs for one change can't both land, and one of them is wasted work and CI.
|
||||
|
||||
## Claim = a draft PR
|
||||
- Before working on issue #N, check nobody has it: `gh pr list -R tangency-labs/solidcraft --state open --search "#N"`.
|
||||
- If none, branch from the latest `origin/main`, `git commit --allow-empty -m "claim: #N"`, push,
|
||||
and open a **draft** PR titled `<summary> (#N)` right away. **The lowest-numbered open PR for an
|
||||
issue wins**; if yours lost, close it. Opening the PR is the claim: the bot assigns you and
|
||||
labels the issue `in progress` (or says on your PR who holds it). Reference `#N` in the PR title
|
||||
so the bot can find it. Don't claim by editing files; comment `/claim` on the issue only to stake
|
||||
a claim before you have a branch.
|
||||
- Push work in progress at least daily. A draft with no pushes for 72 hours may be taken over:
|
||||
comment on it first, then open your own claim (you may start from its branch).
|
||||
This takeover policy is separate from the bot's automatic cleanup deadlines in CONTRIBUTING.md.
|
||||
- On a shared build machine, check the load before a big build (`cut -d' ' -f1 /proc/loadavg`)
|
||||
and wait while it is over 24; build with `nice`.
|
||||
|
||||
## Design file format
|
||||
A change that saves something new in a design adds ONE file, `crates/doc/src/format/pending/<name>.rs`
|
||||
(a `//!` note, and `pub fn step` if older designs must be rewritten). Never edit `FORMAT`,
|
||||
`JSON_STEPS`, the history in `format.rs`, another change's file or a `v<N>/` folder: the build
|
||||
works out the number (the next one while anything is pending), and the release folds pending
|
||||
changes into `v<N>/` with `scripts/fold-format-pending.sh`. See CONTRIBUTING.md.
|
||||
|
||||
## Application releases
|
||||
Development pushes are not application releases. Follow `docs/releasing.md` when asked to
|
||||
prepare or ship a release: choose the version from the actual changes, update Cargo.toml and
|
||||
Cargo.lock, pass the quality gates, advance `release` without force-pushing, and verify the
|
||||
resulting draft and its artifacts. When shipping is authorized, publish that verified draft
|
||||
and confirm its tag and downstream deployment. Do not auto-release every push to `main`, infer
|
||||
version bumps solely from commit prefixes, overwrite published versions, or push release tags
|
||||
before the draft is verified.
|
||||
|
||||
## Running and looking at the app
|
||||
- `cargo run --release -p solidcraft -- --sample --control PORT` (sample design + control channel).
|
||||
Pick a free port. Without a display, run it under `Xvfb :99` with `DISPLAY=:99`.
|
||||
- Drive it with JSON lines on `127.0.0.1:PORT` (see `docs/control-protocol.md`). The first
|
||||
request on a connection carries the launch's token as `"token"`: read it from
|
||||
`~/.local/share/solidcraft/control/PORT.token`, or start the app with
|
||||
`SOLIDCRAFT_CONTROL_TOKEN=$(openssl rand -hex 32)` and send that.
|
||||
- `{"id":1,"method":"engine.execute","params":{"command":"solid.extrude","params":{"distance":20}}}`
|
||||
- `{"id":2,"method":"ui.start","params":{"command":"sketch.create"}}` then `ui.click {x, y}`
|
||||
- `{"id":3,"method":"ui.screenshot","params":{"path":"/tmp/shot.png"}}` then read the PNG.
|
||||
- **For UI work, look at the result** (screenshot, read the PNG). Without a window use
|
||||
`ui.render` or `solidcraft-cli snapshot`.
|
||||
- Headless: `solidcraft-cli run script.json --out part.step`, `solidcraft-cli eval design.solidcraft`,
|
||||
`solidcraft-cli snapshot script.json --out shot.png`, `solidcraft-cli commands`.
|
||||
- Reference tests: `cargo xtask oracle` replays `tests/oracle/*/recipe.json` and compares with
|
||||
each case's `expected.json` (analytic or FreeCAD values; writes `docs/oracle.md`). Expected
|
||||
values never come from a commercial CAD product.
|
||||
- Build with `CARGO_TARGET_DIR=<repo>/target`. Windows: `cargo xwin build --release --target x86_64-pc-windows-msvc -p solidcraft`.
|
||||
|
||||
## Roadmap
|
||||
`ROADMAP.md` (committed) tracks status, milestones and estimates. Update it whenever a
|
||||
milestone task lands. `docs/features.md` (SolidCraft's commands by area, from the registry) and
|
||||
`THIRD-PARTY-LICENSES.txt` are regenerated on `main` after each merge by the Generated files
|
||||
workflow; never regenerate them in a PR.
|
||||
|
||||
## Lists many PRs add to
|
||||
Keep them conflict-free (CONTRIBUTING.md, "Lists many changes add to"): message catalogs sorted
|
||||
(`cargo xtask lists --fix`), `mod` lines in their sorted group, the language list English first
|
||||
then by id, generated files left to `main`, and a design-format change as one new file.
|
||||
|
||||
+20
-433
@@ -14,12 +14,6 @@ Editable drawing code, native projects and shipped exports are collected under `
|
||||
|
||||
| File | Author | Source | Licence |
|
||||
|---|---|---|---|
|
||||
| assets/render/catalog/capabilities.json | SolidCraft contributors | Byte-identical runtime export of docs/render-materials/capabilities.json; generated and checked by tools/render-library/generate.py | MIT OR Apache-2.0 |
|
||||
| assets/render/catalog/environments.json | SolidCraft contributors | Byte-identical runtime export of docs/render-materials/environments.json; generated and checked by tools/render-library/generate.py | MIT OR Apache-2.0 |
|
||||
| assets/render/catalog/lighting.json | SolidCraft contributors | Byte-identical runtime export of docs/render-materials/lighting.json; generated and checked by tools/render-library/generate.py | MIT OR Apache-2.0 |
|
||||
| assets/render/catalog/materials.json | SolidCraft contributors | Byte-identical runtime export of docs/render-materials/materials.json; generated and checked by tools/render-library/generate.py | MIT OR Apache-2.0 |
|
||||
| assets/render/catalog/quality-presets.json | SolidCraft contributors | Byte-identical runtime export of docs/render-materials/quality-presets.json; generated and checked by tools/render-library/generate.py | MIT OR Apache-2.0 |
|
||||
| assets/render/catalog/scene-presets.json | SolidCraft contributors | Byte-identical runtime export of docs/render-materials/scene-presets.json; generated and checked by tools/render-library/generate.py | MIT OR Apache-2.0 |
|
||||
| examples/chebyshev-motion-trace.json | SolidCraft contributors | Original parametric Chebyshev lambda mechanism and associative-trace demo, constructed from analytic circle intersections; classical link proportions documented at https://doi.org/10.1002/rcs.70095; no external CAD geometry or artwork | MIT OR Apache-2.0 |
|
||||
| examples/material-card.json | SolidCraft contributors | original analytic box and explicitly synthetic engineering card; invented illustrative constants, no external material dataset | MIT OR Apache-2.0 |
|
||||
| examples/studio/author_sweep.py | SolidCraft contributors | original editable CAD scene and authoring metadata; no external assets | MIT OR Apache-2.0 |
|
||||
@@ -33,12 +27,6 @@ Editable drawing code, native projects and shipped exports are collected under `
|
||||
| examples/studio/stage1-render.json | SolidCraft contributors | original studio camera and lighting parameters | MIT OR Apache-2.0 |
|
||||
| examples/render-studio.json | SolidCraft contributors | original procedural box/cylinder enclosure script for the integrated Render workspace | MIT OR Apache-2.0 |
|
||||
| examples/render-opaque-studio.json | SolidCraft contributors | original procedural sphere study varying opaque metal fraction and roughness independently; no external assets or engineering data | MIT OR Apache-2.0 |
|
||||
| crates/gpu/src/path/denoise.wgsl | SolidCraft contributors | original bounded edge-aware a-trous spatial filter; GPU2 donor d710e31, no external shader source | MIT OR Apache-2.0 |
|
||||
| crates/gpu/src/path/denoise_guides.wgsl | SolidCraft contributors | original deterministic centre-ray material and geometry guide pass; GPU2 donor d710e31, no external shader source | MIT OR Apache-2.0 |
|
||||
| crates/gpu/src/path/shadows.wgsl | SolidCraft contributors | original bounded area, sun and environment visibility transport; GPU2 donor d710e31, no external shader source | MIT OR Apache-2.0 |
|
||||
| crates/gpu/src/path/textures.wgsl | SolidCraft contributors | original planar colour and normal map sampling; GPU2 donor d710e31, no external shader source | MIT OR Apache-2.0 |
|
||||
| assets/appearances/starter_pack.rs | SolidCraft contributors | original illustrative optical recipes, authored scalar values and future procedural resource descriptors; no measured or external material dataset | MIT OR Apache-2.0 |
|
||||
| examples/material-finishes.json | SolidCraft contributors | original procedural sphere study of six illustrative opaque finishes with generic material densities; no external assets or measured optical data | MIT OR Apache-2.0 |
|
||||
| examples/render-path-studio.json | SolidCraft contributors | original procedural opaque enclosure, teal insert and metal fasteners for progressive rendering; no imported models | MIT OR Apache-2.0 |
|
||||
| crates/gpu/src/path/path.wgsl | SolidCraft contributors | original software BVH traversal and path integration using the existing SolidCraft surface/studio equations; no external shader source | MIT OR Apache-2.0 |
|
||||
| crates/gpu/src/pbr.rs | SolidCraft contributors | original WGSL translation of SolidCraft's opaque surface and fixed studio reference, embedded as Rust source; no external shader code | MIT OR Apache-2.0 |
|
||||
@@ -51,10 +39,10 @@ Editable drawing code, native projects and shipped exports are collected under `
|
||||
| assets/icons/app/macos/exports/icon.icns | SolidCraft contributors | macOS fallback compiled by Apple's actool from the approved assets/icons/app/macos/source/SolidCraft.icon composition | MIT OR Apache-2.0 |
|
||||
| assets/models/test_boat.bin | 3DBenchy.com | 3DBenchy.com | Public Domain |
|
||||
| assets/models/make_test_boat.py | SolidCraft contributors | original script that packs a model into the compact file above | MIT OR Apache-2.0 |
|
||||
| docs/screenshots/class-a-blend.png | SolidCraft contributors | screenshot of SolidCraft itself: a 24 mm and a 10 mm box bridged by a `surface.blend` sheet (G1 at both edges), rendered by the app's `ui.render` in the dark theme with examples/demo/class_a_shots.py | MIT OR Apache-2.0 |
|
||||
| docs/screenshots/class-a-crown.png | SolidCraft contributors | screenshot of SolidCraft itself: a patch crowned with `surface.patch crown=0.3` over a skew four-line rim, rendered by the app's `ui.render` in the dark theme with examples/demo/class_a_shots.py | MIT OR Apache-2.0 |
|
||||
| docs/screenshots/class-a-boundary.png | SolidCraft contributors | screenshot of SolidCraft itself: a surface through two 3D sketch splines built by `surface.boundary`, rendered by the app's `ui.render` in the dark theme with examples/demo/class_a_shots.py | MIT OR Apache-2.0 |
|
||||
| docs/screenshots/sample-plate.png | SolidCraft contributors | screenshot of SolidCraft 0.22.1 itself (`solidcraft --sample`, default dark theme, 1600x1000) | MIT OR Apache-2.0 |
|
||||
| docs/screenshots/class-a-blend.png | SolidCraft contributors | screenshot of SolidCraft itself: two boxes bridged by `surface.blend` (a 40x10 sheet, `solidcraft-cli snapshot` of plan/evidence script ev_blend) | MIT OR Apache-2.0 |
|
||||
| docs/screenshots/class-a-crown.png | SolidCraft contributors | screenshot of SolidCraft itself: a patch crowned with `surface.patch crown=0.3` over a saddle rim (`solidcraft-cli snapshot`) | MIT OR Apache-2.0 |
|
||||
| docs/screenshots/class-a-boundary.png | SolidCraft contributors | screenshot of SolidCraft itself: a patch through two sketch lines built by `surface.boundary` with `crown: 0.4` (`solidcraft-cli snapshot`) | MIT OR Apache-2.0 |
|
||||
| docs/screenshots/sample-plate.png | SolidCraft contributors | screenshot of SolidCraft itself (`solidcraft --sample`) | MIT OR Apache-2.0 |
|
||||
| assets/samples/exports/0.png | SolidCraft contributors | original Sample Plate from `crates/engine/src/sample.rs`, exported by the SolidCraft thumbnail renderer | MIT OR Apache-2.0 |
|
||||
| assets/samples/exports/1.png | SolidCraft contributors | original Sheet Metal Bracket from `crates/engine/src/sample.rs`, exported by the SolidCraft thumbnail renderer | MIT OR Apache-2.0 |
|
||||
| assets/samples/exports/2.png | SolidCraft contributors | original Hinge Assembly from `crates/engine/src/sample.rs`, exported by the SolidCraft thumbnail renderer | MIT OR Apache-2.0 |
|
||||
@@ -62,10 +50,10 @@ Editable drawing code, native projects and shipped exports are collected under `
|
||||
| assets/samples/exports/4.png | SolidCraft contributors | original Bolted Flange from `crates/engine/src/sample.rs`, exported by the SolidCraft thumbnail renderer | MIT OR Apache-2.0 |
|
||||
| assets/samples/exports/5.png | SolidCraft contributors | original Vent Plate from `crates/engine/src/sample.rs`, exported by the SolidCraft thumbnail renderer | MIT OR Apache-2.0 |
|
||||
| assets/samples/exports/scripts.json | SolidCraft contributors | SHA-256 fingerprints of the original sample scripts, generated with the shipped thumbnails | MIT OR Apache-2.0 |
|
||||
| docs/screenshots/demo.gif | SolidCraft contributors | original screen recording of the actual SolidCraft 0.22.1 English desktop UI (default dark theme, software renderer) modelling an original Raspberry Pi 4 case (no logos or branding; the engraved text is set in the system's DejaVu Sans Bold), generated with examples/demo/record_ui.sh and its original animated pointer overlay (6× playback, idle pauses cut) | MIT OR Apache-2.0 |
|
||||
| docs/screenshots/demo.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.22.1 English desktop UI (default dark theme, software renderer) modelling an original Raspberry Pi 4 case (no logos or branding; the engraved text is set in the system's DejaVu Sans Bold), generated with examples/demo/record_ui.sh and its original animated pointer overlay (6× playback, idle pauses cut) | MIT OR Apache-2.0 |
|
||||
| docs/screenshots/rover-build.gif | 0xlillith (model); SolidCraft contributors (recording) | screen recording of the actual SolidCraft 0.22.1 desktop UI (default dark theme, software renderer) replaying the timeline of the SELENE rover (`Rover-recorded.solidcraft` from https://github.com/0xlillith/solidcraft-rover, an original model by 0xlillith built through SolidCraft MCP); the model's lettering follows Ubuntu Light outlines (Canonical Ltd., Ubuntu Font Licence 1.0, as noted in that repository); the emblem is the SolidCraft project icon (15× playback, idle pauses cut, constant-rate camera orbit) | MIT OR Apache-2.0 |
|
||||
| docs/screenshots/rover-build.mp4 | 0xlillith (model); SolidCraft contributors (recording) | screen recording of the actual SolidCraft 0.22.1 desktop UI (default dark theme, software renderer) replaying the timeline of the SELENE rover (`Rover-recorded.solidcraft` from https://github.com/0xlillith/solidcraft-rover, an original model by 0xlillith built through SolidCraft MCP); the model's lettering follows Ubuntu Light outlines (Canonical Ltd., Ubuntu Font Licence 1.0, as noted in that repository); the emblem is the SolidCraft project icon | MIT OR Apache-2.0 |
|
||||
| docs/screenshots/demo-0.8.gif | SolidCraft contributors | original screen recording of the actual SolidCraft 0.8 English desktop UI modelling an original Raspberry Pi 4 case (no logos or branding; the engraved text is set in the system's DejaVu Sans Bold), generated with examples/demo/record_ui.sh and its original animated pointer overlay (9× playback) | MIT OR Apache-2.0 |
|
||||
| docs/screenshots/demo-0.8.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.8 English desktop UI modelling an original Raspberry Pi 4 case (no logos or branding; the engraved text is set in the system's DejaVu Sans Bold), generated with examples/demo/record_ui.sh and its original animated pointer overlay (9× playback) | MIT OR Apache-2.0 |
|
||||
| docs/screenshots/rover-build.gif | 0xlillith (model); SolidCraft contributors (recording) | screen recording of the actual SolidCraft desktop UI replaying the timeline of the SELENE rover (`Rover-recorded.solidcraft` from https://github.com/0xlillith/solidcraft-rover, an original model by 0xlillith built through SolidCraft MCP); the model's lettering follows Ubuntu Light outlines (Canonical Ltd., Ubuntu Font Licence 1.0, as noted in that repository); the emblem is the SolidCraft project icon (2× playback) | MIT OR Apache-2.0 |
|
||||
| docs/screenshots/rover-build.mp4 | 0xlillith (model); SolidCraft contributors (recording) | screen recording of the actual SolidCraft desktop UI replaying the timeline of the SELENE rover (`Rover-recorded.solidcraft` from https://github.com/0xlillith/solidcraft-rover, an original model by 0xlillith built through SolidCraft MCP); the model's lettering follows Ubuntu Light outlines (Canonical Ltd., Ubuntu Font Licence 1.0, as noted in that repository); the emblem is the SolidCraft project icon | MIT OR Apache-2.0 |
|
||||
| docs/design/ux-redesign/model-floating.png | SolidCraft contributors | original UX mockup (Model tab, floating layout) rendered from an HTML mockup that uses the original Technical Accent icon set; no third-party artwork, and not a screenshot of any other CAD product | MIT OR Apache-2.0 |
|
||||
| docs/design/ux-redesign/model-compact.png | SolidCraft contributors | original UX mockup (Model tab, compact docked layout) rendered from an HTML mockup that uses the original Technical Accent icon set; no third-party artwork, and not a screenshot of any other CAD product | MIT OR Apache-2.0 |
|
||||
| docs/design/ux-redesign/sketch.png | SolidCraft contributors | original UX mockup (Sketch tab) rendered from an HTML mockup that uses the original Technical Accent icon set; no third-party artwork, and not a screenshot of any other CAD product | MIT OR Apache-2.0 |
|
||||
@@ -101,23 +89,6 @@ Editable drawing code, native projects and shipped exports are collected under `
|
||||
| tests/step-conformance/tiny_pin.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
|
||||
| tests/step-conformance/torus_ring_nurbs.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
|
||||
| tests/step-conformance/washer.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
|
||||
| tests/kernel-cases/import/1721-closed-bspline-tube-face/input.step | step-conformance contributors | copy of tests/step-conformance/cylinder_nurbs.step (generated from formulas by https://github.com/dan335/step-conformance, corpus/generated, 96f30bb); kernel regression case for tangency-labs/solidcraft#1721 | CC0-1.0 |
|
||||
| tests/kernel-cases/import/2043-outer-bound-opposing-surface-sense/input.step | SolidCraft contributors | synthetic STEP file written by hand from ISO 10303-21 for the kernel regression case of tangency-labs/solidcraft#2043 | MIT OR Apache-2.0 |
|
||||
| tests/kernel-cases/import/258-cone-point-vertex/input.step | step-conformance contributors | copy of tests/step-conformance/cone_point_vertex.step (generated from formulas by https://github.com/dan335/step-conformance, corpus/generated, 96f30bb); kernel regression case for tangency-labs/solidcraft#258 | CC0-1.0 |
|
||||
| tests/kernel-cases/import/258-cone-to-point/input.step | step-conformance contributors | copy of tests/step-conformance/cone_point.step (generated from formulas by https://github.com/dan335/step-conformance, corpus/generated, 96f30bb); kernel regression case for tangency-labs/solidcraft#258 | CC0-1.0 |
|
||||
| tests/kernel-cases/import/258-tiny-pin/input.step | step-conformance contributors | copy of tests/step-conformance/tiny_pin.step (generated from formulas by https://github.com/dan335/step-conformance, corpus/generated, 96f30bb); kernel regression case for tangency-labs/solidcraft#258 | CC0-1.0 |
|
||||
| tests/kernel-cases/import/3098-bspline-edges-over-tiny-knot-span/input.step | SolidCraft contributors | synthetic STEP file written by hand from ISO 10303-21 for the kernel regression case of tangency-labs/solidcraft#3098 | MIT OR Apache-2.0 |
|
||||
| tests/kernel-cases/import/3208-extruded-ellipse-face-over-parameter-seam/input.step | SolidCraft contributors | synthetic STEP file written by hand from ISO 10303-21 for the kernel regression case of tangency-labs/solidcraft#3208 | MIT OR Apache-2.0 |
|
||||
| tests/kernel-cases/import/3352-vertex-off-its-edge-curve/input.step | SolidCraft contributors | synthetic STEP file written by hand from ISO 10303-21 for the kernel regression case of tangency-labs/solidcraft#3352 | MIT OR Apache-2.0 |
|
||||
| tests/kernel-cases/import/3730-void-oriented-shell-false/input.step | SolidCraft contributors | synthetic STEP file written by hand from ISO 10303-21 for the kernel regression case of tangency-labs/solidcraft#3730 | MIT OR Apache-2.0 |
|
||||
| tests/kernel-cases/import/3880-cylinder-seam-edge/input.step | step-conformance contributors | copy of tests/step-conformance/cylinder_seam.step (generated from formulas by https://github.com/dan335/step-conformance, corpus/generated, 96f30bb); kernel regression case for tangency-labs/solidcraft#3880 | CC0-1.0 |
|
||||
| tests/kernel-cases/import/3880-nurbs-hemisphere-dome/input.step | step-conformance contributors | copy of tests/step-conformance/dome_nurbs.step (generated from formulas by https://github.com/dan335/step-conformance, corpus/generated, 96f30bb); kernel regression case for tangency-labs/solidcraft#3880 | CC0-1.0 |
|
||||
| tests/kernel-cases/import/3963-closed-spline-edge-vertex-past-start/input.step | SolidCraft contributors | synthetic STEP file written by hand from ISO 10303-21 for the kernel regression case of tangency-labs/solidcraft#3963 | MIT OR Apache-2.0 |
|
||||
| tests/kernel-cases/import/4005-cylinder-written-as-near-zero-cone/input.step | SolidCraft contributors | synthetic STEP file written by hand from ISO 10303-21 for the kernel regression case of tangency-labs/solidcraft#4005 | MIT OR Apache-2.0 |
|
||||
| tests/kernel-cases/import/4006-plane-written-as-cone-of-nearly-right-angle/input.step | SolidCraft contributors | synthetic STEP file written by hand from ISO 10303-21 for the kernel regression case of tangency-labs/solidcraft#4006 | MIT OR Apache-2.0 |
|
||||
| tests/ssi-corpus/cases/*.json.gz, tests/ssi-corpus/LICENSE-MIT, tests/ssi-corpus/LICENSE-APACHE | ssi-corpus contributors (jyviko) | 119 surface/surface intersection cases with exact ground truth from https://github.com/jyviko/ssi-corpus (cases/data, b04c8a6), gzip-compressed, unchanged; SSI scorer fixtures (tests/ssi-corpus/README.md) | MIT OR Apache-2.0 |
|
||||
| tests/ssi-corpus/cases/y23_*.json.gz | Yang, Jia and Yan (paper authors: published surfaces, figures and topology); ssi-corpus contributors (reconstructions and truth) | 25 cases built from Yang, Jia & Yan 2023, "Topology Guaranteed B-Spline Surface/Surface Intersection", ACM Transactions on Graphics 42(6), Article 211; each cites the paper in its `source` field | CC-BY 4.0 (paper); MIT OR Apache-2.0 (case files) |
|
||||
| tests/step-regressions/rod_doubled_edge.step | SolidCraft contributors | written by SolidCraft from the commands of #4598 (a bored rod moved 100 mm and exported); import regression fixture | MIT OR Apache-2.0 |
|
||||
| tests/oracle/17-loft/reference.stl | SolidCraft contributors | triangulation of our own recipe (tests/oracle/17-loft/recipe.json) built with FreeCAD as an external tool by tools/freecad-oracle | MIT OR Apache-2.0 |
|
||||
| tests/oracle/23-bracket-multifeature/reference.stl | SolidCraft contributors | triangulation of our own recipe (tests/oracle/23-bracket-multifeature/recipe.json) built with FreeCAD as an external tool by tools/freecad-oracle | MIT OR Apache-2.0 |
|
||||
| tests/oracle/28-blind-pocket-fillets/reference.stl | SolidCraft contributors | triangulation of our own recipe (tests/oracle/28-blind-pocket-fillets/recipe.json) built with FreeCAD as an external tool by tools/freecad-oracle | MIT OR Apache-2.0 |
|
||||
@@ -139,7 +110,7 @@ Editable drawing code, native projects and shipped exports are collected under `
|
||||
| tests/oracle/63-hinge-knuckle/reference.stl | SolidCraft contributors | triangulation of our own recipe (tests/oracle/63-hinge-knuckle/recipe.json) built with FreeCAD as an external tool by tools/freecad-oracle | MIT OR Apache-2.0 |
|
||||
| tests/oracle/65-sketch-gear-tooth/reference.stl | SolidCraft contributors | triangulation of our own recipe (tests/oracle/65-sketch-gear-tooth/recipe.json) built with FreeCAD as an external tool by tools/freecad-oracle | MIT OR Apache-2.0 |
|
||||
| assets/appearances/library.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
| assets/appearances/opaque_finishes.rs | SolidCraft contributors | original illustrative opaque finish values; no external dataset, textures or measured engineering data | MIT OR Apache-2.0 |
|
||||
| assets/appearances/starter_pack.rs | SolidCraft contributors | original illustrative optical recipes, authored scalar values and future procedural resource descriptors; no measured or external material dataset | MIT OR Apache-2.0 |
|
||||
| assets/fonts/biz-udp-gothic/OFL.txt | The BIZ UDGothic Project Authors | Unmodified accompanying font licence | SIL OFL 1.1 |
|
||||
| assets/fonts/ibm-plex-mono/OFL.txt | IBM Corp. | Unmodified accompanying font licence | SIL OFL 1.1 |
|
||||
| assets/fonts/ibm-plex-sans/OFL.txt | IBM Corp. | Unmodified accompanying font licence | SIL OFL 1.1 |
|
||||
@@ -174,7 +145,6 @@ Editable drawing code, native projects and shipped exports are collected under `
|
||||
| assets/locales/ja.json | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
| assets/locales/ko.json | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
| assets/locales/ms.json | A.Hakim (a-hakim) | Malay translation of the English message catalog | MIT OR Apache-2.0 |
|
||||
| assets/locales/nb.json | SolidCraft contributors | Original Norwegian Bokmål translation of SolidCraft's English catalog; no commercial CAD text used | MIT OR Apache-2.0 |
|
||||
| assets/locales/nl.json | SolidCraft contributors | Original Dutch translation of SolidCraft's English catalog; no commercial CAD text used | MIT OR Apache-2.0 |
|
||||
| assets/locales/pl.json | cyryllo | Polish translation of the English catalog, contributed upstream | MIT OR Apache-2.0 |
|
||||
| assets/locales/ru.json | Alex Pro (del000) | Russian translation of the English catalog, contributed upstream | MIT OR Apache-2.0 |
|
||||
@@ -190,10 +160,6 @@ Editable drawing code, native projects and shipped exports are collected under `
|
||||
| assets/platforms/linux/ai.storyteller.solidcraft.desktop | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
|
||||
| assets/platforms/linux/ai.storyteller.solidcraft.metainfo.xml.in | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
|
||||
| assets/platforms/linux/ai.storyteller.solidcraft.mime.xml | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
|
||||
| assets/platforms/macos/dmg/background.tiff | SolidCraft contributors | original disk image window background (blueprint sheet: grid, double border, circles, centre lines, leader arrow, title block) drawn by assets/platforms/macos/dmg/make_background.py in IBM Plex Sans/Mono; 1x and 2x representations | MIT OR Apache-2.0 |
|
||||
| assets/platforms/macos/dmg/make_background.py | SolidCraft contributors | authoring script that draws the disk image background (Pillow); not part of any build | MIT OR Apache-2.0 |
|
||||
| assets/platforms/macos/dmg/layout.json | SolidCraft contributors | disk image window size, icon size and icon positions shared by the background script and settings.py | MIT OR Apache-2.0 |
|
||||
| assets/platforms/macos/dmg/settings.py | SolidCraft contributors | dmgbuild settings (MIT, https://github.com/dmgbuild/dmgbuild) for the SolidCraft disk image; original settings, no dmgbuild code copied | MIT OR Apache-2.0 |
|
||||
| assets/platforms/macos/Info.plist.in | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
|
||||
| assets/platforms/macos/README.txt | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
|
||||
| assets/platforms/windows/solidcraft.en-us.wxl | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
|
||||
@@ -220,8 +186,6 @@ Editable drawing code, native projects and shipped exports are collected under `
|
||||
| assets/styles/ui/themes/graphite.json | SolidCraft contributors | original SolidCraft theme file | MIT OR Apache-2.0 |
|
||||
| assets/styles/ui/themes/nosferatu.json | SolidCraft contributors | original SolidCraft theme file adapting the colours of a published palette (Dracula by Zeno Rocha and the Dracula Theme contributors, https://github.com/dracula/dracula-theme, MIT) | MIT OR Apache-2.0 |
|
||||
| assets/styles/ui/themes/catppuccin-mocha.json | SolidCraft contributors | original SolidCraft theme file adapting the colours of a published palette (Catppuccin by the Catppuccin organisation, https://github.com/catppuccin/catppuccin, MIT) | MIT OR Apache-2.0 |
|
||||
| assets/styles/ui/themes/okibi.json | SolidCraft contributors (Kai Evans) | original SolidCraft theme file mapping SolidCraft's UI colours to the Okibi palette (dark) of the Okibi design system by Kai Evans (https://kaievns.github.io/okibi-akiri-design-system-spec/) | MIT OR Apache-2.0 |
|
||||
| assets/styles/ui/themes/akari.json | SolidCraft contributors (Kai Evans) | original SolidCraft theme file mapping SolidCraft's UI colours to the Akari palette (light) of the Okibi design system by Kai Evans (https://kaievns.github.io/okibi-akiri-design-system-spec/) | MIT OR Apache-2.0 |
|
||||
| assets/styles/web/loading-logo.svg | SolidCraft contributors | Animation of the SolidCraft mark, built from the paths and gradients of assets/icons/app/macos/source/SolidCraft.icon/Assets/03-solid-blue-cutout.svg; original animation | MIT OR Apache-2.0 |
|
||||
| assets/styles/web/loading.css | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
| crates/io/src/native_cad/fixtures/block.FCStd | SolidCraft contributors | original 10 × 20 × 30 mm test box saved by FreeCAD 1.1.4 run as an external program (`make_block_fcstd.py` beside it); no third-party geometry | MIT OR Apache-2.0 |
|
||||
@@ -304,8 +268,6 @@ Editable drawing code, native projects and shipped exports are collected under `
|
||||
| assets/icons/ui/design/svg/model/circle_tt.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/circle_ttt.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/coil.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/colored_components.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/colored_components_off.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/combine.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/component.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/compute.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
@@ -538,14 +500,6 @@ Editable drawing code, native projects and shipped exports are collected under `
|
||||
|
||||
| docs/book/src/og.png | SolidCraft contributors | copy of assets/branding/web/source/og.png staged by cargo xtask book (gitignored) | MIT OR Apache-2.0 |
|
||||
| docs/sponsors/blacksmith-ci-powered.png | Blacksmith (blacksmith.sh) | "CI powered by Blacksmith" sponsor banner supplied by Blacksmith for the sponsorship credit; used only to credit the sponsor, not part of the application | © Blacksmith, used with permission |
|
||||
| assets/icons/ui/design/svg/model/path_flange.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| docs/book/src/generated/hem.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/hem.json`; gitignored), original sheet metal demonstration | MIT OR Apache-2.0 |
|
||||
| docs/book/scripts/hem.json | SolidCraft contributors | original parametric recipe for an 80 × 50 mm sheet with an 8 mm returned edge, authored for the command guide | MIT OR Apache-2.0 |
|
||||
| assets/styles/ui/themes/clay.json | SolidCraft contributors | original SolidCraft theme file (Clay, light): warm paper palette chosen for SolidCraft | MIT OR Apache-2.0 |
|
||||
| assets/styles/ui/themes/clay-dark.json | SolidCraft contributors | original SolidCraft theme file (Clay Dark): warm charcoal palette chosen for SolidCraft | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/cone.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/locales/ar.json | Abdullah (A-AL-ANAZI) | Original Arabic translation of SolidCraft's English catalog; no commercial CAD text used | MIT OR Apache-2.0 |
|
||||
| assets/locales/he.json | SolidCraft contributors | Original Hebrew translation of SolidCraft's English catalog; no commercial CAD text used | MIT OR Apache-2.0 |
|
||||
|
||||
## Code adapted from sibling projects
|
||||
|
||||
@@ -605,6 +559,17 @@ copied into SolidCraft.
|
||||
|
||||
| Directory | Source | Licence | Changes |
|
||||
|---|---|---|---|
|
||||
| examples/studio/author_sweep.py | SolidCraft contributors | original editable CAD scene and authoring metadata; no external assets | MIT OR Apache-2.0 |
|
||||
| examples/studio/author_scene.py | SolidCraft contributors | original editable CAD scene and authoring metadata; no external assets | MIT OR Apache-2.0 |
|
||||
| examples/studio/materials.json | SolidCraft contributors | original editable CAD scene and authoring metadata; no external assets | MIT OR Apache-2.0 |
|
||||
| examples/studio/coffee-exploded-sweep.json | SolidCraft contributors | original editable CAD scene and authoring metadata; no external assets | MIT OR Apache-2.0 |
|
||||
| examples/studio/coffee-exploded-render.json | SolidCraft contributors | original editable CAD scene and authoring metadata; no external assets | MIT OR Apache-2.0 |
|
||||
| examples/studio/coffee-exploded.json | SolidCraft contributors | original editable CAD scene and authoring metadata; no external assets | MIT OR Apache-2.0 |
|
||||
| vendor/truck-meshalgo | truck-meshalgo 0.4.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | meshing: bounded parameter searches, collapsed-boundary projection, subdivision and provisional fixed-grid previews (vendor/README.md) |
|
||||
| vendor/truck-stepio | truck-stepio 0.3.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | STEP writer: intersection-curve surfaces written at unique ids (vendor/README.md) |
|
||||
| vendor/truck-shapeops | truck-shapeops 0.4.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | robust meshing and nearest-parameter fallback in booleans; degenerate intersection-curve rejection; face and edge history of booleans; trace macro; corner choice at vertices several intersection curves end on; reuse of an operand's meshes (vendor/README.md) |
|
||||
| vendor/truck-geometry | truck-geometry 0.5.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | intersection curves divided from their neighbours' surface parameters; surface second derivatives (vendor/README.md) |
|
||||
| vendor/matext4cgmath | matext4cgmath 0.1.0, https://github.com/ricosjp/matext4cgmath (RICOS Co. Ltd.) | Apache-2.0 | root refinement stops after 100 Newton steps (vendor/README.md) |
|
||||
|
||||
## Vendored renderer compatibility fix
|
||||
|
||||
@@ -690,381 +655,3 @@ The original studio generators produce local ignored PNG maps and an RGBE gradie
|
||||
Generated image bytes are not source assets; their authorship, licences and expected
|
||||
hashes are documented under `examples/studio/textures`. Rendered evidence belongs
|
||||
on the results branch. No photographs, scans or external image assets are included.
|
||||
|
||||
## Additional render and showcase resources
|
||||
|
||||
| File | Author | Source | Licence |
|
||||
|---|---|---|---|
|
||||
| assets/render/starter/tools/check_all.py | SolidCraft contributors | original read-only authored/native resource validation orchestration and evidence receipts | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/tools/test_check_all.py | SolidCraft contributors | original validation-runner regression fixtures and subprocess tests | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/tools/requirements.txt | SolidCraft contributors | pinned offline authoring tool requirements; no application dependency changes | MIT OR Apache-2.0 |
|
||||
| crates/render/examples/validate_starter.rs | SolidCraft contributors | original native resource conformance checker using public SolidCraft decoding, normalization, sampling and mip APIs | MIT OR Apache-2.0 |
|
||||
| .gitattributes | SolidCraft contributors | original canonical line-ending policy for the existing runtime icon-discovery manifest; no artwork or icon-assignment changes | MIT OR Apache-2.0 |
|
||||
| assets/render/.gitattributes | SolidCraft contributors | original canonical line-ending policy for SHA-256-pinned renderer authoring resources | MIT OR Apache-2.0 |
|
||||
| docs/render-materials/.gitattributes | SolidCraft contributors | original canonical line-ending policy for SHA-256-pinned renderer authoring resources | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/assembly-bench-soft.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/automotive-turntable.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/black-metal-strips.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/cnc-overhead.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/esd-bench-neutral.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/fdm-neutral-inspection.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/garage-door.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/hangar-skylight.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/home-workbench.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/makerspace-worklights.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/mechanical-bay.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/neutral-comparison.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/neutral-side-softbox.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/paint-inspection-bands.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/printed-part-photo.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/rework-offset-strips.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/robot-led-editorial.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/showroom-ribbons.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/spacecraft-clean-bay.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/sun-free-overcast.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/vacuum-black.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/warm-table-window.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/white-enveloping-studio.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/woodshop-window.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/manifest.json | SolidCraft contributors | original clean-room renderer resource recipe or manifest; authored/unbound scope and source receipts in assets/render/starter/README.md | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/source/recipes.json | SolidCraft contributors | original clean-room renderer resource recipe or manifest; authored/unbound scope and source receipts in assets/render/starter/README.md | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/lighting/exports/rigs.json | SolidCraft contributors | original clean-room renderer resource recipe or manifest; authored/unbound scope and source receipts in assets/render/starter/README.md | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/lighting/recipe.json | SolidCraft contributors | original clean-room renderer resource recipe or manifest; authored/unbound scope and source receipts in assets/render/starter/README.md | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-001/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-001/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-001/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-001/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-001/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-002/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-002/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-002/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-002/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-002/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-003/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-003/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-003/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-003/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-003/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-004/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-004/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-004/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-004/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-004/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-005/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-005/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-005/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-005/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-005/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-006/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-006/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-006/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-006/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-006/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-007/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-007/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-007/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-007/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-007/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-008/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-008/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-008/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-008/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-008/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-009/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-009/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-009/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-009/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-009/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-010/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-010/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-010/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-010/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-010/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-011/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-011/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-011/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-011/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-011/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-012/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-012/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-012/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-012/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-012/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-013/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-013/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-013/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-013/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-013/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-014/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-014/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-014/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-014/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-014/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-015/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-015/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-015/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-015/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-015/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-016/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-016/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-016/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-016/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-016/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-017/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-017/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-017/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-017/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-017/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-018/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-018/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-018/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-018/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-018/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-019/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-019/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-019/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-019/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-019/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-020/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-020/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-020/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-020/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-020/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-021/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-021/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-021/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-021/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-021/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-022/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-022/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-022/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-022/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-022/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-023/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-023/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-023/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-023/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-023/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-024/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-024/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-024/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-024/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-024/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-025/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-025/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-025/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-025/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-025/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-026/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-026/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-026/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-026/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-026/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-027/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-027/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-027/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-027/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-027/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-028/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-028/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-028/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-028/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-028/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-029/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-029/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-029/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-029/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-029/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-030/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-030/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-030/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-030/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-030/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-031/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-031/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-031/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-031/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-031/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-032/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-032/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-032/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-032/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-032/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-033/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-033/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-033/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-033/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-033/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-034/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-034/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-034/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-034/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-034/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-035/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-035/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-035/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-035/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-035/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-036/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-036/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-036/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-036/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-036/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-037/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-037/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-037/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-037/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-037/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-038/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-038/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-038/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-038/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-038/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-039/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-039/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-039/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-039/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-039/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-040/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-040/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-040/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-040/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-040/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-041/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-041/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-041/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-041/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-041/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-042/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-042/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-042/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-042/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-042/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-043/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-043/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-043/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-043/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-043/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-044/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-044/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-044/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-044/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-044/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-045/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-045/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-045/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-045/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-045/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-046/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-046/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-046/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-046/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-046/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-047/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-047/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-047/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-047/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-047/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-048/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-048/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-048/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-048/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-048/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/manifest.json | SolidCraft contributors | original clean-room renderer resource recipe or manifest; authored/unbound scope and source receipts in assets/render/starter/README.md | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/source/recipes.json | SolidCraft contributors | original clean-room renderer resource recipe or manifest; authored/unbound scope and source receipts in assets/render/starter/README.md | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/pack.json | SolidCraft contributors | original clean-room renderer resource recipe or manifest; authored/unbound scope and source receipts in assets/render/starter/README.md | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/tools/environments.py | SolidCraft contributors | original clean-room offline resource authoring, numerical verification or validation tool; no application runtime dependency | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/tools/lighting.py | SolidCraft contributors | original clean-room offline resource authoring, numerical verification or validation tool; no application runtime dependency | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/tools/materials.py | SolidCraft contributors | original clean-room offline resource authoring, numerical verification or validation tool; no application runtime dependency | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/tools/test_environments.py | SolidCraft contributors | original clean-room offline resource authoring, numerical verification or validation tool; no application runtime dependency | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/tools/test_lighting.py | SolidCraft contributors | original clean-room offline resource authoring, numerical verification or validation tool; no application runtime dependency | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/tools/test_materials.py | SolidCraft contributors | original clean-room offline resource authoring, numerical verification or validation tool; no application runtime dependency | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/tools/validate_pack.py | SolidCraft contributors | original clean-room offline resource authoring, numerical verification or validation tool; no application runtime dependency | MIT OR Apache-2.0 |
|
||||
| assets/render/selene/licenses/UFL.txt | Canonical Ltd. | exact Ubuntu Font Licence 1.0 notice retained from https://github.com/0xlillith/solidcraft-rover/blob/b32437915372d32b71419dc8f5143fe22ff1e869/UFL.txt for the modelled Ubuntu Light lettering | Ubuntu Font Licence 1.0 |
|
||||
| assets/render/selene/materials/appearance-pack.json | SolidCraft contributors | original clean-room English appearance authoring data: 48 illustrative opaque PBR projections with separate advanced target specifications; no measured dataset or external assets | MIT OR Apache-2.0 |
|
||||
| assets/render/selene/materials/appearance-swatches.json | SolidCraft contributors | original analytic command script for 48 opaque box swatches and a support base, derived only from the adjacent original appearance pack; no external artwork or engineering properties | MIT OR Apache-2.0 |
|
||||
| assets/render/selene/models/SELENE-functional.solidcraft | SolidCraft contributors (native enhancements); 0xlillith (original rover); SychPL / Mateusz Sych (supplied showcase assembly); Canonical Ltd. (lettering outlines) | native-command derivative of the unchanged author-supplied source, with original bearing, suspension, gripper, electronics and presentation work; source and final hashes in assets/render/selene/SOURCE.md and VALIDATION.md | Model and showcase contributions: MIT OR Apache-2.0 (explicit SychPL grant https://github.com/tangency-labs/solidcraft/pull/5494#issuecomment-6375788360); lettering: Ubuntu Font Licence 1.0, notice retained in assets/render/selene/licenses/UFL.txt |
|
||||
| assets/render/selene/motion/interference-pairs.json | SolidCraft contributors | original authored exact-body pair selection for bounded native clearance checks on the functional SELENE derivative; not a generated collision result | MIT OR Apache-2.0 |
|
||||
| assets/render/selene/motion/mechanism-acceptance.json | SolidCraft contributors | original kinematic acceptance criteria, inspected source-container bindings and analytic planning checks from cited public SolidCraft documentation and the admitted native source; derivative runtime acceptance is recorded separately | MIT OR Apache-2.0 |
|
||||
| assets/render/selene/motion/preflight-commands.json | SolidCraft contributors | original read-only inventory script using existing project commands; no source-model selectors or state mutations | MIT OR Apache-2.0 |
|
||||
| assets/render/selene/motion/sample-sequence.json | SolidCraft contributors | original future lunar sampling and sealed-vial dynamics state specification; no simulation or physical calibration result | MIT OR Apache-2.0 |
|
||||
| assets/render/selene/scenes/roles.json | SolidCraft contributors | original semantic role and candidate-material authoring specification for the prepared SELENE demo; no native geometry or external assets bundled | MIT OR Apache-2.0 |
|
||||
| assets/render/selene/scenes/shots.json | SolidCraft contributors | original shot, framing and staged-sequence authoring specification for the prepared SELENE demo; no render or simulation result claimed | MIT OR Apache-2.0 |
|
||||
| assets/render/selene/scenes/stages.json | SolidCraft contributors | original stage composition and light-ownership authoring specification derived from the project render catalog; scientific context linked in scenes/README.md | MIT OR Apache-2.0 |
|
||||
| assets/render/selene/source-lock.json | SolidCraft contributors | original source-admission metadata and public-reference inventory pinning the supplied improved SELENE native file; original-model and font notices preserved in SOURCE.md | MIT OR Apache-2.0 |
|
||||
| assets/render/selene/source/SELENE-showcase.solidcraft | 0xlillith (original rover); SychPL / Mateusz Sych (showcase assembly and helpers); SolidCraft contributors (emblem); Canonical Ltd. (lettering outlines) | unchanged author-published native file from https://github.com/SychPL/solidcraft/blob/32ec1720447e3fcc6bb6cc53a63fb5b39cfa21b9/SELENE-showcase.solidcraft; source hash and license evidence in assets/render/selene/SOURCE.md | Model and showcase contributions: MIT OR Apache-2.0 (explicit SychPL grant https://github.com/tangency-labs/solidcraft/pull/5494#issuecomment-6375788360); lettering: Ubuntu Font Licence 1.0, notice retained in assets/render/selene/licenses/UFL.txt |
|
||||
| assets/render/selene/tools/bearings.py | SolidCraft contributors | original native-command authoring of grooved bearings, rolling-element kinematics, clamp hubs and arm/head clearances; analytic geometry, no copied CAD source | MIT OR Apache-2.0 |
|
||||
| assets/render/selene/tools/build.py | SolidCraft contributors | original Python development tool for pinned-source native command replay, staged checkpoints, command-trace and geometry health checks; no application runtime dependency | MIT OR Apache-2.0 |
|
||||
| assets/render/selene/tools/electronics.py | SolidCraft contributors | original native-command Raspberry Pi 5 visual reconstruction using public manufacturer mechanical dimensions; original package envelopes and connector internals, no manufacturer CAD or artwork copied | MIT OR Apache-2.0 |
|
||||
| assets/render/selene/tools/export_views.py | SolidCraft contributors | original native MCP preview export tool for three reusable showcase views; changes only transient visibility and cameras and verifies the source master hash | MIT OR Apache-2.0 |
|
||||
| assets/render/selene/tools/finishes.py | SolidCraft contributors | original native physical-material proxy and opaque PBR assignment generator preserving the supplied rover palette | MIT OR Apache-2.0 |
|
||||
| assets/render/selene/tools/mechanisms.py | SolidCraft contributors | original native-command gripper sleeves, pads, wheel ownership, joint limits and inspection studies with independent analytic suspension relation | MIT OR Apache-2.0 |
|
||||
| assets/render/selene/tools/prepare.py | SolidCraft contributors | original Python development tool for bounded native-container inspection and reviewed opaque body-assignment preparation; no application runtime dependency | MIT OR Apache-2.0 |
|
||||
| assets/render/selene/tools/presentation.py | SolidCraft contributors | original native-command browser organization, camera/render setups and closed sample-vial glass preparation | MIT OR Apache-2.0 |
|
||||
| assets/render/selene/tools/suspension.py | SolidCraft contributors | original native-command trailing-arm suspension, telescoping coilover loops, plain bushings, carrier gussets and local motion clearances derived from the admitted rover geometry; independent analytic pin-distance relation, no copied external CAD | MIT OR Apache-2.0 |
|
||||
| assets/render/selene/tools/validate_native.py | SolidCraft contributors | original bounded native MCP verification tool for sampled SELENE kinematics, independent analytic follower and world-coordinate checks, optional exact-name interference pairs and unchanged input hashes; no application runtime dependency | MIT OR Apache-2.0 |
|
||||
| assets/render/selene/tools/validate_prepared.py | SolidCraft contributors | original Python development validator for prepared SELENE resources, source status, references, hashes and command projections; no application runtime dependency | MIT OR Apache-2.0 |
|
||||
| assets/render/selene/tools/yaw_supports.py | SolidCraft contributors | original native-command plain yaw journals, flanged brass thrust bushings, recessed axial retaining pins and mounting-head clearances derived from the admitted rover geometry; no copied external CAD | MIT OR Apache-2.0 |
|
||||
| assets/render/selene/ui/demo-flow.json | SolidCraft contributors | original progressive-disclosure UI integration specification for the existing Render workspace; no copied UI artwork or runtime registration | MIT OR Apache-2.0 |
|
||||
| docs/render-materials/baseline-setup-commands.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
|
||||
| docs/render-materials/capabilities.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
|
||||
| docs/render-materials/environment-presets.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
|
||||
| docs/render-materials/environments.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
|
||||
| docs/render-materials/lighting.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
|
||||
| docs/render-materials/lunar-physics-fixtures.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
|
||||
| docs/render-materials/materials.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
|
||||
| docs/render-materials/provenance.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
|
||||
| docs/render-materials/quality-presets.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
|
||||
| docs/render-materials/roadmap.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
|
||||
| docs/render-materials/scene-presets.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
|
||||
| docs/render-materials/selene-bindings.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
|
||||
| docs/render-materials/simulation-requirements.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
|
||||
| docs/render-materials/studio-presets.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
|
||||
| docs/render-materials/ui-integration.json | SolidCraft contributors | original English UI integration specification and exact source-revision mappings from SolidCraft's own implemented shell, UI kit and accepted icons; no copied artwork or runtime registration | MIT OR Apache-2.0 |
|
||||
| docs/render-materials/ux.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
|
||||
| docs/render-materials/validate_catalog.py | SolidCraft contributors | original Python development validator for catalog references, preset composition and analytic specification fixtures; no application runtime dependency | MIT OR Apache-2.0 |
|
||||
|
||||
### Built-in render resource adapters (personal integration batch, 2026-10-22)
|
||||
|
||||
The original adapters in `crates/engine/src/cmd/render/library.rs`, the generated lookup `library_data.rs` and its generator `tools/render-library/generate.py` project the already attributed starter and SELENE resources without replacing their bytes or authoring manifests. The added scalar texture channels, explicit color/V conventions and immutable-source/GPU-payload sharing are original contributor work under MIT OR Apache-2.0. No external code, commercial CAD data or new third-party imagery was imported. Source metadata and the original authoring attribution remain authoritative; pending runtime acceptance is not a photometric or engineering qualification.
|
||||
|
||||
## Physical reference resource additions
|
||||
|
||||
| File | Author | Source | Licence |
|
||||
|---|---|---|---|
|
||||
| assets/render/physical/bindings.json | SolidCraft contributors | original physical-reference curation, associations or derived schema-1 export; per-fact public sources retained without relicensing their publications | MIT OR Apache-2.0 |
|
||||
| assets/render/physical/exports/coverage.json | SolidCraft contributors | original physical-reference curation, associations or derived schema-1 export; per-fact public sources retained without relicensing their publications | MIT OR Apache-2.0 |
|
||||
| assets/render/physical/exports/native-cards.json | SolidCraft contributors | original physical-reference curation, associations or derived schema-1 export; per-fact public sources retained without relicensing their publications | MIT OR Apache-2.0 |
|
||||
| assets/render/physical/profiles.json | SolidCraft contributors | original physical-reference curation, associations or derived schema-1 export; per-fact public sources retained without relicensing their publications | MIT OR Apache-2.0 |
|
||||
| assets/render/physical/quantities.json | SolidCraft contributors | original physical-reference curation, associations or derived schema-1 export; per-fact public sources retained without relicensing their publications | MIT OR Apache-2.0 |
|
||||
| assets/render/physical/sources.json | SolidCraft contributors | original physical-reference curation, associations or derived schema-1 export; per-fact public sources retained without relicensing their publications | MIT OR Apache-2.0 |
|
||||
| assets/render/physical/tools/check_physical.py | SolidCraft contributors | original offline physical-reference validation and projection tool | MIT OR Apache-2.0 |
|
||||
| assets/render/physical/tools/physical.py | SolidCraft contributors | original offline physical-reference validation and projection tool | MIT OR Apache-2.0 |
|
||||
| assets/render/physical/tools/test_physical.py | SolidCraft contributors | original offline physical-reference validation and projection tool | MIT OR Apache-2.0 |
|
||||
| assets/render/physical/tools/export_library.py | SolidCraft contributors | original explicit reference-library planner and native no-loss export verifier; no external code | MIT OR Apache-2.0 |
|
||||
| assets/render/physical/tools/test_export_library.py | SolidCraft contributors | original regressions for native capability refusal, source identity and physical-card preservation | MIT OR Apache-2.0 |
|
||||
|
||||
| tools/render-library/selene_studio.py | SolidCraft contributors | original bounded native MCP studio orchestration with pinned editable master, explicit GPU policy and request/export receipts; no generated model or image substitutes | MIT OR Apache-2.0 |
|
||||
|
||||
| tools/render-library/selene_recipe.py | SolidCraft contributors | original full-detail SELENE material/light composition and analytic camera fit; unchanged pinned native master, no external imagery or measured optical dataset | MIT OR Apache-2.0 |
|
||||
| tools/render-library/test_selene_recipe.py | SolidCraft contributors | original independent projection, scaling, nonmutation and input-refusal fixtures for the SELENE studio recipe | MIT OR Apache-2.0 |
|
||||
|
||||
| assets/render/selene/scenes/studio-hero.json | SolidCraft contributors | original executable render intent and verified provenance for the selected native SELENE studio showcase; authored RGB appearance and relative light units, not measured optics or simulation results | MIT OR Apache-2.0 |
|
||||
| tools/render-library/publish_selene.py | SolidCraft contributors | original bounded native-frame publication checks for explicit visual review, source identity, resource hashes, PNG CRCs and scanline limits; copies completed native bytes without image synthesis or resampling | MIT OR Apache-2.0 |
|
||||
| tools/render-library/test_publish_selene.py | SolidCraft contributors | original tiny analytic PNG fixtures for source-size, CRC, expansion, dimensions and explicit-publication-consent regressions | MIT OR Apache-2.0 |
|
||||
|
||||
| tools/render-library/package_selene.py | SolidCraft contributors | original byte-preserving packaging of the reviewed native SELENE PNG, public provenance and checksums; bounded resource/PNG verification, no fonts, executables or new render execution | MIT OR Apache-2.0 |
|
||||
| .github/workflows/selene-showcase.yml | SolidCraft contributors | original read-only artifact-delivery workflow for the reviewed native SELENE export; existing pinned repository actions, no compilation, package installation, secret use or renderer claims | MIT OR Apache-2.0 |
|
||||
|
||||
| tools/render-library/verify_linear_master.py | SolidCraft contributors | original independent validation driver for native linear EXR output using the official OpenEXR Python reader as an external test oracle; no codec implementation copied or production dependency added | MIT OR Apache-2.0 |
|
||||
|
||||
## Aging, effects and backgrounds showcase, 2026-10-23
|
||||
|
||||
| File | Author | Source | Licence |
|
||||
|---|---|---|---|
|
||||
| tools/render-showcase/instrument.py | SolidCraft contributors | original native-scene authoring, checksum-verified editorial assembly or bounded delivery tool; no third-party source or font binary copied | MIT OR Apache-2.0 |
|
||||
| tools/render-showcase/compose.py | SolidCraft contributors | original native-scene authoring, checksum-verified editorial assembly or bounded delivery tool; no third-party source or font binary copied | MIT OR Apache-2.0 |
|
||||
| tools/render-showcase/package.py | SolidCraft contributors | original native-scene authoring, checksum-verified editorial assembly or bounded delivery tool; no third-party source or font binary copied | MIT OR Apache-2.0 |
|
||||
|
||||
| distribution/SolidCraft-Render-Showcase.zip | SolidCraft contributors and the explicitly credited original model/UI/resource authors | byte-verified selected collaborator package of reviewed native media, editable sample, recipes and public provenance; contained attribution and existing SELENE lettering notices retained; no fonts or executable binaries | MIT OR Apache-2.0 for new contributions; referenced original notices retained |
|
||||
| distribution/SolidCraft-Render-Showcase.json | SolidCraft contributors | original verified delivery archive manifest with exact SHA-256, member count and bounded scope | MIT OR Apache-2.0 |
|
||||
|
||||
+12
-46
@@ -1,10 +1,8 @@
|
||||
# Contributing to SolidCraft
|
||||
|
||||
Agents: start from the router in [AGENTS.md](AGENTS.md), which points to a short skill in [`skills/`](skills/README.md) for each kind of work. This page stays the full human guide.
|
||||
|
||||
SolidCraft is an open-source parametric 3D CAD app written in pure Rust.
|
||||
You sketch on a plane, constrain the sketch, turn profiles into solids, and
|
||||
change a parameter to watch the timeline rebuild. It runs as a desktop app (Windows, macOS, Linux), in
|
||||
change a parameter to watch the timeline rebuild. It runs as a desktop app (Windows, Linux), in
|
||||
the browser (wasm + WebGPU/WebGL2), headless from a CLI, and as an MCP server that AI agents can
|
||||
model with.
|
||||
|
||||
@@ -13,7 +11,7 @@ build it, how the code is laid out, the few rules that aren't negotiable, and wh
|
||||
needed most.
|
||||
|
||||
Before choosing a task, check the [command inventory](docs/features.md) and
|
||||
[roadmap](docs/roadmap-detail.md) for implemented capabilities and current priorities.
|
||||
[roadmap](ROADMAP.md) for implemented capabilities and current priorities.
|
||||
|
||||
Questions, ideas or want to say what you're working on? Join the [Discord](https://discord.gg/FtU4NQNVDH).
|
||||
|
||||
@@ -132,9 +130,7 @@ The [architecture page](docs/architecture.md) has the layer table. The short ver
|
||||
- Every call into truck goes through `solidcraft_kernel::guard`.
|
||||
- Every crash fix comes with a regression test.
|
||||
5. **Everything is a command, with a test.** A new feature means a `CommandSpec` in
|
||||
`crates/engine/src/cmd/` with input and output schemas (see
|
||||
[Adding a command](docs/book/src/dev/commands.md#adding-a-command); a test warns about a
|
||||
command without them, and will fail it once required), engine tests that measure the result (volume, area,
|
||||
`crates/engine/src/cmd/`, engine tests that measure the result (volume, area,
|
||||
face/edge/vertex counts against a value you can derive), and for UI work a scenario (below).
|
||||
6. **One set of camera controls.** Every page or canvas that shows a view (the modelling
|
||||
viewport, the Render preview, and any new page such as a flat pattern or drawing) calls
|
||||
@@ -158,11 +154,9 @@ the most complete description of the house style.
|
||||
| UI scenarios | `crates/ui-egui/tests/scenarios/*.json` | part of `cargo test` |
|
||||
| Reference tests | `cargo xtask oracle` | `tests/oracle/` (see below) |
|
||||
|
||||
The reference for what the kernel builds is the kernel-cases ladder (`tests/kernel-cases/ladder/`,
|
||||
`cargo xtask kernel-cases --ladder`, [docs/kernel-ladder.md](docs/kernel-ladder.md)).
|
||||
The naming corpus checks persistent names: every name a corpus design stores still finds a face or
|
||||
an edge, and two builds name its faces and edges alike
|
||||
(`cargo test --profile ci -p solidcraft-cli --test naming_corpus`).
|
||||
A geometry change that changes the robustness goldens re-records them in the same pull request:
|
||||
`ROBUSTNESS_GOLDENS_BLESS="why it changed" cargo test --profile ci -p solidcraft-engine --lib robustness_goldens::`
|
||||
(see [docs/robustness.md](docs/robustness.md)); CI runs every seed on pull requests that touch geometry.
|
||||
[docs/mutation-testing.md](docs/mutation-testing.md) shows how to find kernel behaviour these
|
||||
black-box suites miss.
|
||||
|
||||
@@ -192,16 +186,8 @@ no LTO, and debug assertions off (truck has debug-only assertions that reject va
|
||||
builds several times faster than `--release`. If a test fails only in a plain debug build, say so
|
||||
in the PR. `cargo xtask test` builds the tests with it and then runs every test binary at once
|
||||
(`cargo test` runs them one after another); arguments after `--` go to each binary, e.g.
|
||||
`cargo xtask test -- shell`. Set `SOLIDCRAFT_TEST_JOBS` to a positive integer to opt into a
|
||||
shared bound on top-level test/side commands for `cargo xtask test` and `cargo xtask ci`.
|
||||
For example, `SOLIDCRAFT_TEST_JOBS=2` allows two commands at once; the sequential wasm/doc
|
||||
worker consumes one of those slots. Every binary and side check still runs after failures.
|
||||
Without the variable the existing concurrency is unchanged. With it set, each test binary
|
||||
inherits `RUST_TEST_THREADS` and `ACTION_MATRIX_JOBS` of `max(1, cpus / jobs)` — the machine's
|
||||
parallelism shared between the binaries in flight — unless you set either one explicitly;
|
||||
explicit caller overrides always win. Cargo's build-job setting remains an independent caller
|
||||
override; this limit does not change test filters, timeouts or baselines.
|
||||
Tests run beside each other on a busy machine, so a test must not depend on wall-clock timing.
|
||||
`cargo xtask test -- shell`. Tests run beside each other on a busy machine, so a test must not
|
||||
depend on wall-clock timing.
|
||||
|
||||
**Ready pull requests test what they touch.** CI maps changed files to workspace packages and
|
||||
runs Clippy, the test build and tests for those and their dependents. The
|
||||
@@ -221,7 +207,8 @@ Pull requests skip a few heavy tests (one to twelve minutes each), which run in
|
||||
Check that validation after merging. They are listed in
|
||||
`.config/nextest.toml` and the [testing page](docs/book/src/dev/testing.md):
|
||||
|
||||
- `solidcraft-engine`: `tests::hostile_params_never_panic`, everything in `tests::robustness`,
|
||||
- `solidcraft-engine`: `tests::hostile_params_never_panic`, everything in `tests::robustness` and
|
||||
the seeds of `tests::robustness_goldens` (`seed_00` … `seed_95`; pull requests that change the kernel, truck or the goldens run all but the slowest, `SLOW_SEEDS` in `ci.yml`),
|
||||
`pattern_suppress_tests::enclosure_arc_extinction_keeps_a_real_sharp_inner_lip_after_resize_and_reopen`,
|
||||
`pattern_suppress_tests::enclosure_datums_follow_wall_height_and_board_depth_with_material_proofs`,
|
||||
`cmd::plastic::tests::enclosure_snap_fit_follows_resized_front_wall`, the `atomic_commands` contract tests
|
||||
@@ -241,8 +228,7 @@ the surface itself). Expected values are either worked out in closed form
|
||||
(`tools/oracle-analytic/`) or computed by building the same recipe in FreeCAD, run as a separate
|
||||
program (`tools/freecad-oracle/`); each file says which. The generated
|
||||
[reference test report](docs/oracle.md) has the current case count and results. The reference-test
|
||||
CI job runs `cargo xtask oracle` during full validation, and on pull requests that reach
|
||||
`solidcraft-cli`. A case that
|
||||
CI job runs `cargo xtask oracle` during full validation; pull-request runs skip it. A case that
|
||||
fails because of a known bug names its issue in `expected.json` (`"known_failure": "#123"`);
|
||||
never change an expected value to match SolidCraft's output.
|
||||
|
||||
@@ -282,18 +268,12 @@ and a fresh CI run:
|
||||
and the Lists workflow) fails on an unsorted catalog or an id that appears twice.
|
||||
- **`mod` lines**: one per line, in the sorted group they belong to (`cargo fmt` keeps each group
|
||||
sorted). Add a new module into the existing group, not as a new group at the end.
|
||||
- **Commands without schemas** (`crates/engine/src/schema/unconverted.txt`): sorted, one id per
|
||||
line, and it only shrinks. Converting a command removes its line; a new command is never added
|
||||
to it.
|
||||
- **Languages** (`crates/ui-egui/src/i18n.rs`): one per line, English first, then by id (a test
|
||||
checks it).
|
||||
- **Generated files**: `THIRD-PARTY-LICENSES.txt` and `docs/features.md` are rewritten on `main`
|
||||
after each merge by the Generated files workflow (`cargo xtask licences`, `cargo xtask
|
||||
features`). Don't regenerate or edit them in a pull request.
|
||||
- **Design format**: one file per change, never an edit to a shared number (above).
|
||||
- **New UI strings and attribution rows**: one new file per change, `assets/locales/pending/<slug>.json`
|
||||
and `attribution/pending/<slug>.md`, instead of editing the catalogs or the ATTRIBUTION.md table;
|
||||
`cargo xtask fold-pending` folds them on `main` (see the README in each folder).
|
||||
|
||||
## Sending a change
|
||||
|
||||
@@ -345,16 +325,6 @@ Changes with no useful observable behavior do not need screenshot or video evide
|
||||
small example without private designs or sensitive details; attach the media or link to an
|
||||
accessible recording rather than committing generated review artifacts.
|
||||
|
||||
To show a picture without committing it, attach the media to the PR (it goes to the PR's own
|
||||
attachment storage and renders inline in the comment). A gist is the better home for a log or
|
||||
another text file, and you link it; a gist does not render a picture inline. The review artifacts
|
||||
stay out of the diff that way, and nothing has to be pushed to a branch of its own.
|
||||
|
||||
Attaching works in the browser for anyone who can comment: drag the file into a comment on the
|
||||
pull request. A command-line agent without a browser session usually cannot attach a file (the
|
||||
upload needs write access to this repository). Say so in the pull request, and the person running
|
||||
the agent or a maintainer can attach it.
|
||||
|
||||
Keep an eye on your PR's status, but don't push to it only to catch up with `main` (no merge of
|
||||
`main`, no Update branch button, no rebase for that alone): every push restarts the whole CI
|
||||
run for everyone waiting behind it, and `main`'s own run catches what lands in between. When you
|
||||
@@ -370,10 +340,6 @@ owning one area, with a human setting direction and reporting bugs. Their workin
|
||||
[AGENTS.md](AGENTS.md). Human contributors are just as welcome. Use whatever tools you like, as
|
||||
long as the change meets the rules above.
|
||||
|
||||
Two quick checks run on every pull request (`.github/workflows/pr-checks.yml`, not required): it
|
||||
fails when a lower-numbered open PR by someone else already closes the same issue (the label
|
||||
`not-a-duplicate` skips it), or when the description lacks the `Changes a default: no` / `Changes a default: yes — <what>, old behaviour kept behind preference <name>` line (docs-only and bot PRs are exempt).
|
||||
|
||||
## Labels and triage
|
||||
|
||||
The triage bot labels every new issue and PR (`.github/workflows/triage.yml`). It only adds labels;
|
||||
@@ -410,7 +376,7 @@ In order of how much they'd help users:
|
||||
2. **Boolean robustness.** A random-design fuzzer currently gets about half of its designs
|
||||
through without a kernel error. Each failing seed is a reproducible bug.
|
||||
3. **Drawings, CAM and mesh tools.** These are out of scope for now (see
|
||||
[ROADMAP.md](docs/roadmap-detail.md)), but anyone who wants to own one is welcome.
|
||||
[ROADMAP.md](ROADMAP.md)), but anyone who wants to own one is welcome.
|
||||
|
||||
## Updating the generated feature list
|
||||
|
||||
|
||||
Generated
+326
-74
@@ -188,6 +188,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "approx"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f2a05fd1bd10b2527e20a2cd32d8873d115b8b39fe219ee25f42a8aca6ba278"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arboard"
|
||||
version = "3.6.1"
|
||||
@@ -208,6 +217,12 @@ dependencies = [
|
||||
"x11rb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "array-macro"
|
||||
version = "2.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "220a2c618ab466efe41d0eace94dfeff1c35e3aa47891bdb95e1c0fefffd3c99"
|
||||
|
||||
[[package]]
|
||||
name = "arrayref"
|
||||
version = "0.3.9"
|
||||
@@ -520,6 +535,15 @@ dependencies = [
|
||||
"piper",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "branches"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f11502672c5570f77f6bdf573332483f8475bab6a7fda00f1fae8ddb5a6245c0"
|
||||
dependencies = [
|
||||
"rustc_version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.3"
|
||||
@@ -767,6 +791,17 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cgmath"
|
||||
version = "0.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a98d30140e3296250832bbaaff83b27dcd6fa3cc70fb6f1f3e5c9c0023b5317"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"num-traits",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chacha20"
|
||||
version = "0.10.2"
|
||||
@@ -789,6 +824,19 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.4.4"
|
||||
@@ -859,6 +907,12 @@ version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
@@ -1036,6 +1090,19 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "0.99.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustc_version",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
@@ -1182,7 +1249,7 @@ dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"emath",
|
||||
"epaint",
|
||||
"itertools",
|
||||
"itertools 0.15.0",
|
||||
"log",
|
||||
"nohash-hasher",
|
||||
"profiling",
|
||||
@@ -1600,8 +1667,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1914,6 +1983,30 @@ dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.65"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_collections"
|
||||
version = "2.3.0"
|
||||
@@ -2073,6 +2166,15 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.15.0"
|
||||
@@ -2167,6 +2269,17 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "katexit"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccfb0b7ce7938f84a5ecbdca5d0a991e46bc9d6d078934ad5e92c5270fe547db"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "keyboard-types"
|
||||
version = "0.8.3"
|
||||
@@ -2317,6 +2430,15 @@ version = "0.4.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
||||
|
||||
[[package]]
|
||||
name = "matext4cgmath"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cgmath",
|
||||
"katexit",
|
||||
"num-complex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
@@ -3192,6 +3314,30 @@ dependencies = [
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
|
||||
dependencies = [
|
||||
"proc-macro-error-attr",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error-attr"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.107"
|
||||
@@ -3364,6 +3510,15 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rclite"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e09665c494a9de2bd230e1aedc0c2fb20481a51f28f7db84d0c9bcfe2fe537b2"
|
||||
dependencies = [
|
||||
"branches",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "read-fonts"
|
||||
version = "0.41.0"
|
||||
@@ -4053,7 +4208,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft"
|
||||
version = "0.24.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"block2 0.6.2",
|
||||
"eframe",
|
||||
@@ -4062,7 +4217,6 @@ dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
"hidapi",
|
||||
"keyring",
|
||||
"libc",
|
||||
"log",
|
||||
"muda",
|
||||
"objc2 0.6.5",
|
||||
@@ -4074,7 +4228,6 @@ dependencies = [
|
||||
"rfd",
|
||||
"ron",
|
||||
"rustls",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"solidcraft-3dx-client",
|
||||
"solidcraft-engine",
|
||||
@@ -4087,24 +4240,23 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-3dx-client"
|
||||
version = "0.24.0"
|
||||
version = "0.21.0"
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-cli"
|
||||
version = "0.24.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"log",
|
||||
"serde_json",
|
||||
"solidcraft-engine",
|
||||
"solidcraft-gpu",
|
||||
"solidcraft-kernel",
|
||||
"solidcraft-mcp",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-derive"
|
||||
version = "0.24.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -4113,7 +4265,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-doc"
|
||||
version = "0.24.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
@@ -4122,7 +4274,6 @@ dependencies = [
|
||||
"solidcraft-derive",
|
||||
"solidcraft-geom",
|
||||
"solidcraft-kernel",
|
||||
"solidcraft-profile",
|
||||
"solidcraft-render",
|
||||
"solidcraft-sketch",
|
||||
"thiserror 2.0.21",
|
||||
@@ -4131,9 +4282,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-engine"
|
||||
version = "0.24.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"log",
|
||||
"schemars",
|
||||
"serde",
|
||||
@@ -4143,17 +4293,15 @@ dependencies = [
|
||||
"solidcraft-geom",
|
||||
"solidcraft-io",
|
||||
"solidcraft-kernel",
|
||||
"solidcraft-profile",
|
||||
"solidcraft-render",
|
||||
"solidcraft-sketch",
|
||||
"thiserror 2.0.21",
|
||||
"toml_edit",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-geom"
|
||||
version = "0.24.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -4162,7 +4310,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-gpu"
|
||||
version = "0.24.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"log",
|
||||
@@ -4173,7 +4321,6 @@ dependencies = [
|
||||
"serde_json",
|
||||
"solidcraft-engine",
|
||||
"solidcraft-geom",
|
||||
"solidcraft-io",
|
||||
"solidcraft-kernel",
|
||||
"solidcraft-render",
|
||||
"thiserror 2.0.21",
|
||||
@@ -4182,14 +4329,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-io"
|
||||
version = "0.24.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"cadmpeg-core",
|
||||
"cadmpeg-ir",
|
||||
"cadmpeg-registry",
|
||||
"chardetng",
|
||||
"encoding_rs",
|
||||
"gethostname",
|
||||
"gltf",
|
||||
"log",
|
||||
"quick-xml",
|
||||
@@ -4200,14 +4346,13 @@ dependencies = [
|
||||
"solidcraft-doc",
|
||||
"solidcraft-geom",
|
||||
"solidcraft-kernel",
|
||||
"solidcraft-profile",
|
||||
"thiserror 2.0.21",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-kernel"
|
||||
version = "0.24.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"proptest",
|
||||
@@ -4215,34 +4360,28 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"solidcraft-geom",
|
||||
"solidcraft-profile",
|
||||
"solidcraft-smlib",
|
||||
"solidcraft-step",
|
||||
"spade",
|
||||
"thiserror 2.0.21",
|
||||
"truck-meshalgo",
|
||||
"truck-modeling",
|
||||
"truck-shapeops",
|
||||
"truck-stepio",
|
||||
"truck-topology",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-mcp"
|
||||
version = "0.24.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"solidcraft-engine",
|
||||
"solidcraft-gpu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-profile"
|
||||
version = "0.24.0"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-render"
|
||||
version = "0.24.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"moxcms",
|
||||
@@ -4256,7 +4395,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-sketch"
|
||||
version = "0.24.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -4265,37 +4404,16 @@ dependencies = [
|
||||
"ttf-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-smlib"
|
||||
version = "0.24.0"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"flate2",
|
||||
"log",
|
||||
"serde_json",
|
||||
"solidcraft-geom",
|
||||
"solidcraft-profile",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-space-mouse"
|
||||
version = "0.24.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-step"
|
||||
version = "0.24.0"
|
||||
dependencies = [
|
||||
"solidcraft-geom",
|
||||
"solidcraft-profile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-ui-egui"
|
||||
version = "0.24.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"egui",
|
||||
@@ -4317,7 +4435,6 @@ dependencies = [
|
||||
"tiny-skia",
|
||||
"toml",
|
||||
"ttf-parser",
|
||||
"unicode-bidi",
|
||||
"web-time",
|
||||
"winreg",
|
||||
"zip",
|
||||
@@ -4325,17 +4442,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-web"
|
||||
version = "0.24.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"egui",
|
||||
"js-sys",
|
||||
"log",
|
||||
"percent-encoding",
|
||||
"serde_json",
|
||||
"solidcraft-engine",
|
||||
"solidcraft-ui-egui",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
@@ -4386,6 +4501,16 @@ version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.119"
|
||||
@@ -4572,7 +4697,6 @@ dependencies = [
|
||||
"indexmap",
|
||||
"toml_datetime",
|
||||
"toml_parser",
|
||||
"toml_writer",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
@@ -4623,6 +4747,144 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "truck-base"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c279de9e92e5dc20a188deb0bb9a4bd421a6a185e57e03e19025e84a36c5a05"
|
||||
dependencies = [
|
||||
"cgmath",
|
||||
"matext4cgmath",
|
||||
"rustc-hash 2.1.3",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "truck-derivers"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "421ff1c5a303aed64e295d9b30e8424ee7a2fa251617a59ae4acfd078496009f"
|
||||
dependencies = [
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "truck-geometry"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"thiserror 1.0.69",
|
||||
"truck-base",
|
||||
"truck-geotrait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "truck-geotrait"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97a31fe98d5a0589ad20576260e214af23dea21a8d125da81ad3bbcb666f411f"
|
||||
dependencies = [
|
||||
"getrandom 0.2.17",
|
||||
"rand 0.8.8",
|
||||
"thiserror 1.0.69",
|
||||
"truck-base",
|
||||
"truck-derivers",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "truck-meshalgo"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"array-macro",
|
||||
"derive_more",
|
||||
"itertools 0.13.0",
|
||||
"rayon",
|
||||
"rustc-hash 2.1.3",
|
||||
"spade",
|
||||
"truck-base",
|
||||
"truck-geometry",
|
||||
"truck-modeling",
|
||||
"truck-polymesh",
|
||||
"truck-topology",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "truck-modeling"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8082c2dfac8c2732a014d81807fb5d24dbac0d72ee4de2a705b7fbc05fa05b18"
|
||||
dependencies = [
|
||||
"derive_more",
|
||||
"rustc-hash 2.1.3",
|
||||
"serde",
|
||||
"thiserror 1.0.69",
|
||||
"truck-base",
|
||||
"truck-geometry",
|
||||
"truck-geotrait",
|
||||
"truck-polymesh",
|
||||
"truck-topology",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "truck-polymesh"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8f22def310af4a89c37beb4839bbf702b302d2c112ea29553759f0e16622fbf"
|
||||
dependencies = [
|
||||
"array-macro",
|
||||
"bytemuck",
|
||||
"itertools 0.13.0",
|
||||
"rustc-hash 2.1.3",
|
||||
"serde",
|
||||
"thiserror 1.0.69",
|
||||
"truck-base",
|
||||
"truck-geotrait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "truck-shapeops"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"derive_more",
|
||||
"itertools 0.13.0",
|
||||
"rustc-hash 2.1.3",
|
||||
"truck-base",
|
||||
"truck-geometry",
|
||||
"truck-geotrait",
|
||||
"truck-meshalgo",
|
||||
"truck-topology",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "truck-stepio"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"truck-geometry",
|
||||
"truck-modeling",
|
||||
"truck-polymesh",
|
||||
"truck-topology",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "truck-topology"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf62a1610405c5e39ab5eb6948f326fc1641689734a6b8207a80792133fe0e78"
|
||||
dependencies = [
|
||||
"parking_lot",
|
||||
"rayon",
|
||||
"rclite",
|
||||
"rustc-hash 2.1.3",
|
||||
"serde",
|
||||
"thiserror 1.0.69",
|
||||
"truck-base",
|
||||
"truck-geotrait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ttf-parser"
|
||||
version = "0.25.1"
|
||||
@@ -4679,12 +4941,6 @@ version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-general-category"
|
||||
version = "1.1.0"
|
||||
@@ -5722,16 +5978,12 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.24.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quick-xml",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.10.9",
|
||||
"syn 2.0.119",
|
||||
"toml",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
+19
-12
@@ -5,7 +5,7 @@ members = ["crates/*", "apps/*", "xtask"]
|
||||
exclude = ["vendor"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.24.0"
|
||||
version = "0.21.0"
|
||||
edition = "2024"
|
||||
license = "MIT OR Apache-2.0"
|
||||
rust-version = "1.95"
|
||||
@@ -13,14 +13,10 @@ authors = ["SolidCraft contributors"]
|
||||
repository = "https://github.com/bherbruck/solidcraft"
|
||||
|
||||
[workspace.dependencies]
|
||||
robust = "1.2"
|
||||
# internal
|
||||
solidcraft-geom = { path = "crates/geom" }
|
||||
solidcraft-profile = { path = "crates/profile" }
|
||||
solidcraft-sketch = { path = "crates/sketch" }
|
||||
solidcraft-kernel = { path = "crates/kernel" }
|
||||
solidcraft-smlib = { path = "crates/smlib" }
|
||||
solidcraft-step = { path = "crates/step" }
|
||||
solidcraft-render = { path = "crates/render" }
|
||||
solidcraft-gpu = { path = "crates/gpu" }
|
||||
solidcraft-doc = { path = "crates/doc" }
|
||||
@@ -48,7 +44,14 @@ sha2 = "0.10"
|
||||
moxcms = { version = "0.8.1", default-features = false, features = ["extended_range"] }
|
||||
semver = "1"
|
||||
|
||||
# constrained Delaunay + Ruppert refinement for trimmed faces (MIT OR Apache-2.0).
|
||||
# geometry kernel (Apache-2.0; see plan/adr/0001-geometry-kernel.md). Only crates/kernel uses it.
|
||||
truck-modeling = "0.6"
|
||||
truck-shapeops = { version = "0.4", default-features = false }
|
||||
truck-meshalgo = { version = "0.4", default-features = false, features = ["tessellation", "analyzers"] }
|
||||
truck-stepio = { version = "0.3", default-features = false }
|
||||
truck-topology = "0.6"
|
||||
# constrained Delaunay + Ruppert refinement for trimmed faces (MIT OR Apache-2.0); already in the
|
||||
# tree through truck-meshalgo.
|
||||
spade = { version = "2.15", default-features = false, features = ["std"] }
|
||||
rayon = "1.12"
|
||||
|
||||
@@ -63,9 +66,6 @@ encoding_rs = "0.8"
|
||||
# glTF 2.0 import (MIT OR Apache-2.0): the document model and GLB container only; buffers are
|
||||
# read by crates/io itself, so no image decoding or file access comes with it.
|
||||
gltf = { version = "1.4", default-features = false, features = ["names"] }
|
||||
# The computer's name in a design's "open elsewhere" lock file (MIT OR Apache-2.0; already in the
|
||||
# tree through x11rb).
|
||||
gethostname = "1"
|
||||
|
||||
# FreeCAD and Rhino import (Apache-2.0, pure Rust; native and browser builds).
|
||||
# Pinned: its IR and codec APIs change between minor versions. Open formats only: no default
|
||||
@@ -121,7 +121,9 @@ opt-level = 2
|
||||
opt-level = 1
|
||||
|
||||
# Tests and lints (`cargo xtask ci`, GitHub CI): quick to build, quick enough to run. Light
|
||||
# optimisation, line tables only (file:line in backtraces), no LTO, no debug assertions.
|
||||
# optimisation, line tables only (file:line in backtraces), no LTO. Debug assertions are off because
|
||||
# truck's debug-only checks reject valid geometry (seam edges, closed edges); that was the only
|
||||
# reason the tests used to build with --release, which cost most of CI's time.
|
||||
[profile.ci]
|
||||
inherits = "dev"
|
||||
opt-level = 1
|
||||
@@ -144,9 +146,14 @@ codegen-units = 16
|
||||
# binary). See docs/crash-reports.md.
|
||||
debug = "line-tables-only"
|
||||
|
||||
|
||||
# Vendored crates with SolidCraft fixes (see vendor/README.md and ATTRIBUTION.md).
|
||||
# truck-shapeops with SolidCraft fixes (Apache-2.0, see vendor/truck-shapeops and ATTRIBUTION.md).
|
||||
[patch.crates-io]
|
||||
truck-geometry = { path = "vendor/truck-geometry" }
|
||||
truck-shapeops = { path = "vendor/truck-shapeops" }
|
||||
truck-stepio = { path = "vendor/truck-stepio" }
|
||||
truck-meshalgo = { path = "vendor/truck-meshalgo" }
|
||||
matext4cgmath = { path = "vendor/matext4cgmath" }
|
||||
|
||||
# Keep OpenGL pipelines, resolve textures and CAD callbacks at the same safe sample count.
|
||||
egui-wgpu = { path = "vendor/egui-wgpu" }
|
||||
|
||||
|
||||
@@ -46,13 +46,13 @@
|
||||
<a href="https://trendshift.io/repositories/296178?utm_source=trendshift-badge&utm_medium=badge&utm_campaign=badge-trendshift-296178" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/trendshift/repositories/296178/daily?language=Rust" alt="tangency-labs%2Fsolidcraft | Trendshift (Rust)" width="250" height="55"/></a>
|
||||
</p>
|
||||
|
||||

|
||||

|
||||
|
||||
*A Raspberry Pi 4 case from an empty design: a sketch dimensioned with the board's parameters, extrude, fillet, shell, the rim rounded in one pick, an M2.5 standoff patterned on the 58 × 49 mm hole rectangle, port cut-outs, vent slots, "Pi 4" engraved in DejaVu Sans Bold and a PETG material defined in the design, with its mass in Properties, then height 24 → 32 mm in Parameters and it all rebuilds. 6× playback with the idle pauses cut. [Full video](https://github.com/tangency-labs/solidcraft/raw/refs/heads/main/docs/screenshots/demo.mp4), recorded with the [demo script](examples/demo/record_ui.sh).*
|
||||
*A Raspberry Pi 4 case from an empty design: a sketch dimensioned with the board's parameters, extrude, fillet, shell, the rim rounded in one pick, an M2.5 standoff patterned on the 58 × 49 mm hole rectangle, port cut-outs, vent slots, "Pi 4" engraved in DejaVu Sans Bold and a PETG material defined in the design, with its mass in Properties, then height 24 → 32 mm in Parameters and it all rebuilds. 9× playback. [Full video](https://github.com/tangency-labs/solidcraft/raw/refs/heads/main/docs/screenshots/demo-0.8.mp4), recorded with the [demo script](examples/demo/record_ui.sh).*
|
||||
|
||||
## Download
|
||||
|
||||
- **Windows:** the [installer][windows-msi].
|
||||
- **Windows:** the [installer][windows-msi], or `winget install bherbruck.SolidCraft`.
|
||||
- **macOS:** the [disk image][macos-dmg] (Apple silicon and Intel).
|
||||
- **Linux:** the [AppImage][linux-appimage], or `sudo snap install solidcraft` ([Snap Store][snap-store]).
|
||||
- **Web:** [try it in your browser][web-app], no install.
|
||||
@@ -96,10 +96,8 @@ More commands, the Windows cross-build and the browser build are in [docs/buildi
|
||||
|
||||
`solidcraft-cli mcp` is a [Model Context Protocol](https://modelcontextprotocol.io) server. Headless it
|
||||
runs its own design session, or with `--connect` it drives the running app so you can watch.
|
||||
Claude Code: `claude mcp add solidcraft -- solidcraft-cli mcp`. If a terminal doesn't find
|
||||
`solidcraft-cli` (older Windows installers don't put it on `PATH`), give its full path (by default
|
||||
`"C:\Program Files\SolidCraft\solidcraft-cli.exe"`). Tools, access rules and an example session:
|
||||
[docs/mcp.md](docs/mcp.md).
|
||||
Claude Code: `claude mcp add solidcraft -- solidcraft-cli mcp`. Tools, access rules and an
|
||||
example session: [docs/mcp.md](docs/mcp.md).
|
||||
|
||||
## Scripts
|
||||
|
||||
|
||||
+667
-63
@@ -1,82 +1,686 @@
|
||||
# SolidCraft roadmap
|
||||
|
||||
The short version, for people who use SolidCraft. The detailed developer status (milestone
|
||||
tables, limitations, measurements) is in [docs/roadmap-detail.md](docs/roadmap-detail.md).
|
||||
Status as of **2026-10-14**. SolidCraft supports parametric modelling (sketch →
|
||||
features → parametric timeline) as an open, pure-Rust, agent-drivable application.
|
||||
|
||||
## What SolidCraft is
|
||||
## At a glance
|
||||
|
||||
SolidCraft is open-source parametric 3D CAD written in Rust. You sketch, constrain, extrude, and
|
||||
change a parameter to watch the timeline rebuild. It runs as a desktop app and in the browser
|
||||
([try it](https://solidcraft.io/app)), and it has a command-line tool and an MCP server so that
|
||||
scripts and AI agents can drive the same commands you use.
|
||||
| Question | Answer |
|
||||
|---|---|
|
||||
| Where are we? | **M0 and M13 done; M1, M2, M5–M7, M9, M10, M14 in progress.** Sketch with solver and the full SKETCH create/modify set; extrude, revolve (two sides, symmetric, to object), fillet/chamfer (variable radius, chord length, a radius per edge, corner patches), holes, threads (cosmetic and modelled), patterns, mirror, shell (inside/outside/both), draft, loft, sweep, coil, split body and split face, delete face, construction planes, axes and points; persistent naming of faces and edges; parameters and configurations; components, joints, motion studies, exploded views; sheet metal and a plastic enclosure subset; STEP/IGES/3MF/STL/OBJ/DXF; FreeCAD/Rhino import; autosave and crash recovery; desktop and browser (wasm) app, CLI, MCP server. |
|
||||
| Features | Every registered command by area — [docs/features.md](docs/features.md) (`cargo xtask features`); registry breadth does not establish workflow or geometric depth |
|
||||
| Reference tests | [Current case count and results](docs/oracle.md): public cases in `tests/oracle/` with analytic or FreeCAD-computed expected values, checked by the reference-test job in [CI](.github/workflows/ci.yml). |
|
||||
| Tests | Solver, profiles, kernel booleans/blends/measures, expressions, timeline, persistent naming, file formats and migrations, assemblies, sheet metal, engine end-to-end, hostile-input fuzz over every command and headless UI scenarios. `cargo xtask ci` reports the current passing counts. |
|
||||
| Gates | `cargo xtask ci`: fmt, clippy -D warnings, tests, asset attribution, layering, wasm32 build — green |
|
||||
| Time to a useful alpha (M0–M6) | ≈ **82 agent hours** remain (sum of the M1–M6 estimates below) |
|
||||
|
||||
## Works today
|
||||
Optional GPU viewport ambient occlusion is available in View settings and Graphics preferences,
|
||||
with persistent settings, Performance/Quality graphics presets (Quality combines AO, Fine tessellation and adaptive 2× supersampling; manual combinations show Custom), and the
|
||||
`view.ambient_occlusion` and `view.graphics_preset` commands. See [display settings](docs/ambient-occlusion.md).
|
||||
|
||||
- Sketching with a constraint solver, and the usual create and modify tools.
|
||||
- Extrude, revolve, loft, sweep, coil, fillet, chamfer, shell, draft, holes, threads,
|
||||
patterns, mirror and split.
|
||||
- Parameters with units and expressions, and configurations.
|
||||
- Faces and edges keep their names through edits, so references survive changes.
|
||||
- Components, assemblies, joints, motion studies and exploded views.
|
||||
- Sheet metal with flat patterns and DXF export, and a plastic-enclosure subset.
|
||||
- A Render workspace with GPU rendering, studio lighting, materials and image export
|
||||
(first release in 0.22.0).
|
||||
- Import and export of STEP, IGES, 3MF, STL, OBJ and DXF, and FreeCAD and Rhino files.
|
||||
- Autosave and crash recovery, and the browser build of the same app.
|
||||
## Scope (owner decision, 2026-10-07)
|
||||
|
||||
The full list of commands is in [docs/features.md](docs/features.md). Release notes for each
|
||||
version are on the [releases page](https://github.com/tangency-labs/solidcraft/releases).
|
||||
The issue batch adds preservation of blind pockets in through cuts, associative projected
|
||||
offsets and plastic face placements, XZ (+Y normal) with a v6 migration preserving older
|
||||
designs, interactive angle placement, file-picker sketch imports, Home/recent controls,
|
||||
toolbar overflow, and the remaining component-frame and assembly WIP work. The enclosure
|
||||
acceptance scenarios now include board resizes; STEP reopening is automated. See [WIP reconciliation](docs/wip-reconciliation.md).
|
||||
Face-based Extrude Join (including outward Press Pull) now defaults to the picked body,
|
||||
so edge-touching assembly neighbours stay separate regardless of body order (#226).
|
||||
|
||||
## In progress now
|
||||
In scope, in priority order:
|
||||
1. SOLID and SKETCH (now).
|
||||
2. Parameters / functional constraints (done: see `parameters.*`, `expr.evaluate`; unit-aware,
|
||||
cycles reported with their path, rename follows references, deletes list their users):
|
||||
user parameters with units and expressions (+-*/, ^,
|
||||
parentheses, sin/cos/sqrt/min/max/floor/ceil/round, unit conversion), model parameters
|
||||
d1, d2… for every dimension and feature input, references between parameters with a
|
||||
dependency graph and cycle detection, re-evaluation on change, comments, favourites,
|
||||
CSV/JSON import and export.
|
||||
3. Components and assemblies with joints (M9).
|
||||
4. SHEET METAL: flange, contour flange, bend, unfold/refold, flat pattern with DXF export, sheet
|
||||
metal rules (thickness, K-factor, bend radius) (M10).
|
||||
5. A small PLASTIC subset for enclosures, after sheet metal: Boss, Rib, Web, Lip/Groove, Snap Fit.
|
||||
|
||||
Each feature below is a [milestone](https://github.com/tangency-labs/solidcraft/milestones) holding
|
||||
the issues for it, so the count shows how far along it is. Counts are as of 2026-10-25, one per
|
||||
feature or request. Work merged on a feature branch (`drawings`, `render`) counts as built but
|
||||
reaches the app only when that branch merges. They are rough, not a promise.
|
||||
Deferred until further notice: MESH, FORM (T-splines), SURFACE beyond what solids need (scope proposed in #772), PCB / 3D
|
||||
PCB / Package, Render, Animation, Simulation, Manufacture/CAM, Drawings.
|
||||
|
||||
- **[Rendering & materials](https://github.com/tangency-labs/solidcraft/milestone/17)**: in progress, 9 of 34 pieces done, 3 of them built only on the `render` branch and in the app when it merges. The basic Render workspace is already in releases (0.22.0); the interface rework (#6281) and the material and light libraries are what is left.
|
||||
- **[Drawings](https://github.com/tangency-labs/solidcraft/milestone/8)**: in progress, 3 of 21 pieces built (2 in releases, 1 on the `drawings` branch), and 3 more partly built there (sheets and borders, bill-of-materials types, the paper scene). Not in the app yet: it lands when the `drawings` branch merges. Today you can export the current view as a DXF.
|
||||
- **[Sheet metal](https://github.com/tangency-labs/solidcraft/milestone/15)**: in progress, 1 of 30 requests done. Flanges, hems, unfold and flat patterns already work; the 30 are extensions, mostly the per-edge flange options (segments, miters, partial flanges, reliefs) and bend allowance.
|
||||
- **[Sketching](https://github.com/tangency-labs/solidcraft/milestone/14)**: in progress, 12 of 50 pieces done. Dimension types, selection and snapping options.
|
||||
- **[Solid modeling](https://github.com/tangency-labs/solidcraft/milestone/2)**: in progress, 9 of 50 pieces done. Blends, holes, threads and patterns.
|
||||
- **[Assemblies & joints](https://github.com/tangency-labs/solidcraft/milestone/3)**: in progress, 4 of 19 pieces done. Joints, motion studies and exploded views already work; placement rules, linked sub-assemblies, fastener stacks and weldment corners are the pieces left.
|
||||
- **[Import & export](https://github.com/tangency-labs/solidcraft/milestone/5)**: in progress, 7 of 39 pieces done. FreeCAD, SolidWorks and point-cloud import, and mesh export options.
|
||||
- **[AI agents & MCP](https://github.com/tangency-labs/solidcraft/milestone/18)**: in progress, 2 of 8 pieces done. Agent chat and the MCP server.
|
||||
- **[Interface, web app & plugins](https://github.com/tangency-labs/solidcraft/milestone/4)**: in progress, 31 of 96 pieces done. Ribbon redesign, navigation, themes, the web app and plugins.
|
||||
- **[SMLib kernel migration](https://github.com/tangency-labs/solidcraft/milestone/1)**: in progress, 1 tracking issue open. The kernel is moving from truck to NVIDIA's SMLib (usd-brep, Apache-2.0) on the `kernel-smlib` branch; it is not in releases yet. Tracked in [#4012](https://github.com/tangency-labs/solidcraft/issues/4012).
|
||||
## Milestones
|
||||
|
||||
## Next
|
||||
| # | Milestone | Status | Hours left |
|
||||
|---|---|---|---|
|
||||
| M0 | Vertical slice | done | — |
|
||||
| M1 | Sketch depth: command coverage implemented — projection/intersect/include as linked geometry (auto-project on faces, lost-reference warnings, break link), full SKETCH create/modify set (slots, polygons, tangent arcs/circles, ellipse, fit/control splines, conic, text, trim/extend/break, fillet/chamfer, offset, mirror, patterns, move/scale, blend curve, centerline), constraints incl. curvature and polygon with over-constraint refusal, Shift-picked line midpoints in Horizontal/Vertical using persisted points and ordinary constraints (#2087), driven/arc-length/linear-diameter dimensions, AutoConstrain, DXF/SVG insert, 3D sketch curves (Project To Surface, Intersection Curve, Isoparametric Curve, Spun Profile), exact free-form profiles (B-spline/NURBS edges, any spline degree, curves cut by crossings), sweeps and pipes along 3D sketch curves, canvases and decals, curvature combs. Exact free-form Extend and certified associative offsets now preserve geometry and IDs through edits, deletion and reopen. Occurrence-frame 3D wires now store explicit coordinate spaces and retain points and analytic fits through moves, refresh, undo and reopen. Constructive sketch solving (Owen 1991, Fudos & Hoffmann 1997) ahead of the numerical solver: points and rigid clusters are placed on the branch the sketch had, so dimension edits and drags keep their solution; edits that would turn a sketch over are refused; drags re-solve only the sketch per frame. Remaining workflow repairs: mixed offset junctions, legacy natural-spline seams, and damaged legacy wire links lacking frame provenance | in progress | 4 |
|
||||
| M2 | Robust modelling: coplanar booleans in the kernel, holes (placed on faces, at distances from reference edges, and on sketch points), threads (cosmetic, then modelled), patterns, mirror, shell, draft, split, construction geometry | in progress | 16 |
|
||||
| M3 | General fillets and chamfers (curved edges, chains, vertex blends); saddle rolling-ball surfaces now use continuous seam sections and arc-length fitting with shared contact boundaries and bounded accuracy checks; tiny rim edges remain selected instead of nearby wall seams when filleting (#2498) | in progress | 30 |
|
||||
| M4 | Files: STEP import (done — [docs/step-import.md](docs/step-import.md)), FreeCAD/Rhino solids through cadmpeg (done, desktop, browser and CLI), 3MF export and 3MF/STL import as mesh bodies (done), 3MF with B-rep face tags ([docs/formats/3mf-face-tags.md](docs/formats/3mf-face-tags.md), done), constant-section parametric mesh reconstruction, DXF sketches, OBJ import | in progress | 6 |
|
||||
| M5 | Persistent naming of faces and edges (docs/book/src/dev/naming.md): faces named from their exact kernel-reported history (sketch curve sides, caps, revolve sections, primitive sides, blends, shell inner walls, pattern/mirror copies; split pieces told apart by the faces around them, lumps of a cut body named after their faces), edges by their faces; fillets/chamfers, sketches on faces, shell/draft/offset/replace faces, hole placements, joint origins and face appearances resolve by name first and warn when the entity split or vanished: done. sheet metal edges by base sketch line and flange side, shell inner faces, sweep and loft roles: done. kernel-reported face history: done (merged faces answer to every name; operations without history yet fall back to geometric names, flagged unverified; format 14 keeps older designs' geometric names). Left: history through sheet metal, plastic, surface and emboss features; FaceSource certificates through history | in progress | 20 |
|
||||
| M6 | UI depth: selection feedback and origin (done), timeline editing (done: hover and selection highlight modifier faces, including fillets and chamfers; edit feature, history marker drag, dependency-checked single and multi-item reorder, suppress, rename, delete with dependents, re-resolved references; redefining selections refreshes persistent edge/face references while retaining feature identity and dimension names; responsive feature history with width-driven icons/names/title, snapping width resize, bottom/left/browser-stack docking, shared-column collapse, rollback bar, multi-selection and persistent folders), right-docked dialogs (done), live previews for every feature dialog (done), keyboard-first commands with on-canvas value boxes and sketch inline dimensions (done), drag manipulators with automatic join/cut (done: extrude incl. two sides, fillet, shell, hole depth, move, pattern spacing, revolve rotator, extrude taper), dark theme (done), right-click menu with Repeat (done), measure tool (done), section analysis (done), selection filter (done), drag-solve of sketch points (done); appearance controls now preserve body/face/component targeting, named transparent looks, reuse/search and override removal; preferences | in progress | 6 |
|
||||
| M7 | Thin extrude (walls along open or closed curves: centre, inside, outside or two sides; mitred corners, crossing walls joined; #721), sweep, loft (incl. to a point), rib and web (to next / depth), emboss (planar faces and exact radial emboss/deboss of bounded orthogonal profiles on complete exterior cylinders/sleeves), thread (cosmetic, and modelled M3–M20 outside and inside: the ISO profile screwed in as a faceted cut), coil (smooth B-spline helical walls), pipe, pattern on path, patterns of bodies and components, replace face (parallel), align, remove, feature copy/paste: done. Unique eligible cylindrical targets are inferred for new Emboss features. Left: general curved-face emboss and exact (unfaceted) modelled threads | in progress | 12 |
|
||||
| M8 | Surface workspace (#772). Done: Patch (sketch regions; edge loops filled G0, plus `crown`), Stitch, Thicken (planes and cylinders), Extend (straight edges of planar surfaces), Trim (by a plane, a face or sketch curves swept onto the surface, keeping a side or the picked regions; mutual trim of two surfaces, extended to meet and stitched, #767), Delete Face (healing), Boundary Fill, zebra/draft/curvature-map analysis, Surface Extrude/Revolve/Sweep/Loft from open and closed curves, Offset, Unstitch and Reverse Normal; the continuity-driven core (`kernel/src/bridge.rs`, G0–G3 Hermite rows) with **Blend** (`surface.blend`, #764: edge chains along one or several faces, closed rims paired seam to seam or from picked seams, a dialog), **Match** (`surface.match`), **Boundary** (`surface.boundary`, #763 — two sketch curves exact with G0–G3, three or four as the flat patch of their loop, and curve networks: Coons and Gordon surfaces through sketch, 3D-sketch or edge curves, a point for a three-sided side, G1/G2 against the faces along one direction's edges, joined into their body on request; G1/G2 on two sides that meet at a corner waits for the Gregory-type corner), **Fair** (`surface.fair`, CV fairing), **continuity analysis** (`inspect.continuity`, #770; a toolbar tool with edge markers and a legend, and curvature combs along face isocurves) and a multi-section **loft** with per-end continuity and crown, and guide rails on solid and surface lofts (one or two, scaling or stretching the sections between; #201). Also in the Class-A pass: G1…G4 blend curves in the sketch, chord and variable-radius fillets on curved edges (with honest refusals where the fit cannot hold), and a G2 corner patch where filleted edges meet (kernel; the command refuses until the patch's sections can be sewn to the blends' contacts). Left: bias/form-factor fillet sections (design recorded), a Gregory-type corner, surface Fillet between separate surface bodies (#769), Ruled (#762), Untrim (#761), curved-face Extend (#760), and every remaining #757–#772 item | deferred | — |
|
||||
| M9 | Components and assemblies: New Component / Create Components from Bodies, active component, per-component origin/bodies/sketches/timeline entries, occurrences with transforms, Copy/Paste vs Paste New, nested Browser tree with visibility and isolate; Ground, Move/Copy of occurrences, joints (rigid, revolute, slider, cylindrical, pin-slot, planar, ball) by joint origins with limits and Drive Joints, closed kinematic loops (four-bar, slider-crank: the loop's other joints follow the driven one on its branch, and driving stops where a loop can no longer close), cams (`joint.cam`: a revolute cam drives a slider or revolute follower along a closed sketch profile or the cam's edges, knife, roller or flat followers, worked out again when the profile changes; drives, studies, closed loops, contact sets), As-built Joint, Rigid Group, Contact Sets (driving and motion studies stop where bodies in a set would pass through each other), Motion Study (joint values keyed along a timeline, playback, export of occurrence positions as JSON/CSV), motion analysis (joint values and a point's path charted over a study as value, velocity and acceleration; the parts that collide along a study, with the steps where they do), Exploded views (automatic or by hand, shown without changing placements); interference, per-component physical properties; STEP with assembly structure; URDF export for robot simulators (links with meshes, mass and inertia; joint chains whose values match Drive Joints; docs/urdf.md). Physical properties now measure material-aware component/occurrence placements, descendants and repeated instances, including pending moves and body reassignment offsets; contact geometry and automatic exploded centres respect those offsets. Groundwork done: component tree, occurrences with transforms (instances, Paste New), world placement, active component as authoring frame, ground, Capture/Revert Position, moving bodies and sketches. UI: ASSEMBLE panel dialogs for New Component, Joint (snaps on faces, edges, circles and vertices; animated motion preview; offsets, flip, limits), As-built Joint, Joint Origin, Rigid Group, Drive Joints (live slider), Motion Link, Cam and Interference (overlaps shown red); joints in the timeline and the Browser | in progress | 32 |
|
||||
| M10 | Sheet metal: flange (base, edge, contour), hem, unfold/refold, convert, flat pattern + DXF, rules: done. PLASTIC enclosure subset: Boss, Rib, Web, Lip/Groove, Snap Fit, Rest, Plastic Rules (library, edit, assign; rule draft and clearance as feature defaults): done, tested against hand-computed geometry. UI: SHEET METAL and PLASTIC tabs with dialogs (live preview, Edit Feature, pre-selection) for Flange, Hem, Unfold/Refold, Convert, Sheet Metal Rules, Create Flat Pattern (flat view with bend lines, bend table CSV/JSON and DXF export with exact circle/arc/line cutouts), Boss, Lip/Groove, Snap Fit, Rest, Manage and Assign Plastic Rules; Fold (bend along a sketch line); flanges meeting at a corner closed at any angle with the rule's gap; edge flanges with their own angle and height per edge in one feature; corner reliefs (notch, round, rectangular, tear) chosen by the rule; Convert to Sheet Metal from a solid or shelled body by its bend edges (flat faces, sharp bends) | in progress | 36 |
|
||||
| M11 | 2D drawings | deferred | — |
|
||||
| M12 | Release: cross-platform packages, verification and deliberate publication implemented; original current-UI demo refreshed. Trusted distribution signing and broader performance work remain | in progress | 16 |
|
||||
| M14 | Configurations: a table of rows (parameter expressions and feature suppressions per row) switched with config.activate (undoable, all or nothing), kept through parameter renames and feature deletes (config.configure, config.table, config.column/row/activate). Per-row body materials and appearances (library looks, custom colour/opacity and clearing overrides), undoable activation and save/reopen: done. Ordered typed configuration rules, atomic validation and structured rule editing: done. Component appearance and certified semantic face style columns, atomic preview/apply and independent-copy remapping: done. Exact constructed sketch-side and immutable imported-face lineage now preserve legacy names and reject retargeting. Left: ambiguous shifted/two-sided caps | in progress | 4 |
|
||||
| M13 | Never lose work: atomic saves (temporary file, flush, rename; a crash at any moment leaves the old file or the new one) with the replaced version kept as `<file>.bak`; autosave of unsaved changes to the recovery folder every 5 minutes (preference) and after big operations, only when something changed; "Recover unsaved design?" on the next launch (`doc.recovery_list`, `doc.recover`, `doc.recovery_discard`); a kill-mid-save test (process killed while saving a 0.5 MB design 25 times). File format: `"format": "solidcraft/12"`, older formats upgraded step by step (`doc::format`), newer ones refused with a message, out-of-range numbers and broken placements refused; a corpus of designs saved by M0, components-era, joints-era and current builds (apps/solidcraft-cli/tests/corpus) that must open and evaluate as saved; damaged and hostile variants (truncation, bit flips, hostile values, removed keys, repeated features, deep nesting) never panic | done | 6 |
|
||||
| M15 | Frames and weldments (#4003): structural members along 2D and 3D sketch lines, butt and mitre corners, trim to faces and bodies, end caps, gussets, cosmetic welds and a cut list. Done: the profile library (`frame.profile.list`, `frame.profile.properties`, `frame.profile.insert`): bar, round bar, rectangular and round tube, angle, channel and I-beam, each SolidCraft's own parametric definition with exact fillet arcs and exact section properties (area, centroid, second moments), tested against hand-computed values; members (`frame.member`): one body per straight line of a 2D or 3D frame sketch, following parameter edits, with a built-in profile (its dimensions parameter expressions) or a profile sketch (its drawn material by alternating nested loops), anchor, angle and mirror; each member's name kept with its line. Left: corners, trims, end caps, gussets, welds, cut list, FRAME tab and dialogs | in progress | 60 |
|
||||
|
||||
- **[Surfacing](https://github.com/tangency-labs/solidcraft/milestone/16)**: next, 6 of 14 pieces done. Patch, trim, blend and match exist; the rest of the surface toolset is planned.
|
||||
- **[Manufacturing / CAM](https://github.com/tangency-labs/solidcraft/milestone/9)**: next, 1 pieces tracked, none done. There is no CAM in SolidCraft yet. Send to Slicer and a first toolpath slice are in review; proposals are [#1153](https://github.com/tangency-labs/solidcraft/issues/1153) and [#1621](https://github.com/tangency-labs/solidcraft/issues/1621).
|
||||
M6 navigation preferences include **Touchpad**: Shift + pointer movement pans, Alt/Option +
|
||||
pointer movement orbits without a mouse button, and vertical scrolling zooms. Other presets
|
||||
retain their saved two-finger scroll preference; camera motion is available through
|
||||
`view.navigate` and modifier-aware `ui.move` automation.
|
||||
With the **Fusion** preset and two-finger pan enabled, two fingers pan and Shift + two fingers
|
||||
orbit freely, with pinch zoom and mouse bindings unchanged. The shared navigation handler
|
||||
applies the same gestures to model and Render views and blocks orbit on flat pages.
|
||||
|
||||
Features are not tied to a release or a date. Work lands in the daily releases as it is finished.
|
||||
Anything without a milestone, such as bug fixes, is not a feature and is not listed here.
|
||||
UI redesign (#2016): the File/start view (#2027) uses shared light/dark navigation,
|
||||
action tiles, recent rows and sample cards, with recovery review and Save As/Export
|
||||
entry points. Document loading, sample caches and recovery safety retain their
|
||||
existing command paths.
|
||||
|
||||
## How releases work
|
||||
M11 precursor: File → Export Current View as DXF exports shown placed bodies and sketches from the current
|
||||
camera as a 2D millimetre drawing, with hidden edges omitted or dashed. Vector occlusion splits
|
||||
partly hidden edges and includes curved silhouettes. Repeated part meshes and triangle
|
||||
hierarchies are shared; bounded desktop preparation and occlusion use up to eight workers.
|
||||
Curves are approximated; drawing sheets,
|
||||
annotations and exact projected curves remain deferred. See [view DXF](docs/view-dxf.md).
|
||||
|
||||
- Releases ship daily, as numbered versions (v0.22.0, v0.22.1) with nightly pre-releases in
|
||||
between. They are delivered through the in-app updater.
|
||||
- SolidCraft tells you when a newer stable release exists and never updates without asking.
|
||||
Where it can replace itself safely (AppImage, Windows installer and portable builds) there is
|
||||
an **Update now** button; downloads are checked against the release's SHA-256 list. See
|
||||
[updates and privacy](docs/book/src/guide/updates.md).
|
||||
- Design files carry a format number. A new version opens designs from older versions, but an
|
||||
older version refuses a design saved by a newer one (0.22.0 writes format 35, and 0.21.0 and
|
||||
earlier will not open it). Each release's notes say when the format changed. Keep a copy of
|
||||
an important design before you move to a new version.
|
||||
Drawing foundations: [RFC #860](https://github.com/tangency-labs/solidcraft/issues/860)
|
||||
and the [foundation ledger](docs/drawing-foundations.md) track the bounded implementation
|
||||
packages. The first geometry increment defines validated section records in a plane-local
|
||||
frame; the ledger separates that contract from section producers and the Drawing workspace.
|
||||
|
||||
## Licence
|
||||
Desktop view DXF export runs in the file-operation worker with five-stage progress and
|
||||
cooperative cancellation. Curve tolerance (0.0001–1 mm, default 0.01 mm) is remembered alongside
|
||||
the hidden-edge setting. Cancelled exports preserve the model and the previous destination;
|
||||
completed drawings publish atomically. The browser still exports synchronously.
|
||||
|
||||
SolidCraft is licensed MIT OR Apache-2.0, at your option; see [LICENSE-MIT](LICENSE-MIT) and
|
||||
[LICENSE-APACHE](LICENSE-APACHE). Versions already released stay available under those licences
|
||||
permanently.
|
||||
## Interactive modelling
|
||||
|
||||
## How to follow along
|
||||
- Modelled threads preview their cut before OK using the background worker on desktop.
|
||||
Browser users apply the cut with OK to avoid blocking on every edit.
|
||||
|
||||
- Feature milestones: <https://github.com/tangency-labs/solidcraft/milestones>
|
||||
- Issues: <https://github.com/tangency-labs/solidcraft/issues> (area labels such as
|
||||
`area:kernel`, `area:sketch` and `area:ui` group them)
|
||||
- Discord: <https://discord.gg/FtU4NQNVDH>
|
||||
- Releases: <https://github.com/tangency-labs/solidcraft/releases>
|
||||
- Contributing: [CONTRIBUTING.md](CONTRIBUTING.md)
|
||||
- Thread resolves a suggested metric size when a cylindrical face is picked. Diameter and
|
||||
pitch selectors expose the existing metric designation parameter; custom designations stay
|
||||
editable, and Automatic size returns to the face's suggestion. Cosmetic/modelled and
|
||||
full/partial-length options remain available. This does not add tolerance classes or
|
||||
left-hand threads.
|
||||
- New Body extrusion and face-offset previews show completed intermediate geometry while a
|
||||
drag continues, then catch up to the exact final value. Changes to the selected geometry,
|
||||
operation or feature still discard obsolete results. Distance arrows and their value boxes
|
||||
follow the current frame's input.
|
||||
- Large fine modelled threads use pitch-based helix resolution and tighter faceted
|
||||
reconstruction fallbacks, preserving thin crests and closed shells on large shafts.
|
||||
|
||||
## Physical material data
|
||||
|
||||
- Physical material card validation (#923/#922) provides typed SI mechanical and
|
||||
thermal data, per-property sources/conditions and isotropic/orthotropic stability
|
||||
checks through read-only `material.properties.validate`. It does not assign or
|
||||
persist properties, provide a solver or enable rendering; see
|
||||
[the contract and staged follow-ups](docs/physical-material-cards.md).
|
||||
|
||||
- Directional elasticity (#994) queries the validated card in material-local axes
|
||||
through read-only `material.properties.directional`, with source/condition review.
|
||||
It does not assign body frames, predict structural behavior or enable a solver.
|
||||
|
||||
## Library foundations (#785, #786, #787)
|
||||
|
||||
- Local scalar libraries (#846) inspect/export bounded portable catalogs and copy exact
|
||||
selected appearance/material revisions into existing design definitions in one undo step.
|
||||
Appearance offers local loading/search/export; physical materials use existing commands.
|
||||
This slice is independent of the render stack and does not change the document format.
|
||||
See [the contract and limits](docs/book/src/dev/local-libraries.md).
|
||||
|
||||
- Proposed phases cover offline bundled/project/user packs, physical materials and
|
||||
appearances, editable wood mapping, and parameterized component/assembly families.
|
||||
Stable identity/revision, copied design definitions, provenance and resource limits
|
||||
apply from the first implementation. Existing material, appearance and standard-part
|
||||
commands remain the application paths. See [the proposal](docs/book/src/dev/libraries.md)
|
||||
for source/license review and acceptance gates. This is planning only: pack loading,
|
||||
texture rendering and expanded component families are not implemented by this entry.
|
||||
|
||||
## Render foundation
|
||||
|
||||
- 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).
|
||||
|
||||
- Definition-local visual mapping (#1214) supplies a bounded numerical frame, physical repeat coordinates and a deterministic one-time grain-axis suggestion. It adds no saved record or texture consumer; see [the mapping contract](docs/render-visual-mapping.md).
|
||||
|
||||
- Explicit texture normalization (#1218) produces completed float RGBA with
|
||||
role-aware linear color/data semantics, bounded matrix/TRC ICC conversion and
|
||||
opt-in EXIF orientation. It retains high-precision raw samples and records lossy
|
||||
gamut clipping only when explicitly requested. It is not a renderer/resource
|
||||
consumer; see [the supported policy and limits](docs/render-texture-normalization.md).
|
||||
|
||||
- Opt-in opaque PBR (#1046) connects persisted surface parameters to the integrated
|
||||
product viewport and CPU PNG, using a fixed direct-light studio. Geometry and mass
|
||||
stay unchanged. HDRI, transmission and textures remain separate gates.
|
||||
See [the consumer and limitations](docs/render-opaque-pbr.md).
|
||||
|
||||
- Combined integration is tracked in #842, with independent scalar libraries in
|
||||
#863 and richer PBR/material-card transport in #1086. See [phase boundaries and validation gates](docs/render-integration.md).
|
||||
Render/library consumers use the merged asynchronous picker (#1127). Material-card
|
||||
UI (#1094), texture mapping and HDRI lighting remain explicit follow-ups.
|
||||
|
||||
- R4a adds bounded CPU product-image jobs, cancellation and atomic PNG export through
|
||||
engine commands, the Render panel, CLI and persistent MCP. Immutable captures and
|
||||
results stay outside persistence/undo; native workers and cooperative browser tasks
|
||||
share the same work protocol. See [jobs, limits and export](docs/render-jobs.md).
|
||||
|
||||
- R3 adds an overlay-free product scene and the integrated Render workspace, with
|
||||
runtime GPU host capabilities, per-document drafts and named camera/background
|
||||
controls. Design keeps its camera and drawing policy. Raster transparency remains
|
||||
approximate; PBR and environments are later slices. See
|
||||
[workspace and scene contracts](docs/render-workspace.md).
|
||||
|
||||
- R2 adds bounded named render setups in document format 21 and the read/save
|
||||
`render.settings.get/set` commands. Proven render-only changes preserve geometry
|
||||
and prepared world data through undo/redo and checkpoint restore. This slice does
|
||||
not add a Render workspace, PBR, environments or image jobs; see
|
||||
[settings and refresh policy](docs/render-settings.md).
|
||||
- Bounded PNG/JPEG texture admission (#900) validates in-memory sources and returns
|
||||
completed RGBA8 pixels with explicit color/data roles. It is an input utility only:
|
||||
ICC/16-bit normalization, local wood mapping, resource persistence and the renderer
|
||||
consumer remain separate work. See [limits and licensed-file probes](docs/render-texture-input.md).
|
||||
|
||||
- The initial opaque surface numerical contract for #1004 provides bounded linear
|
||||
color/metallic/roughness/IOR admission and an original tested direct-light BRDF.
|
||||
The numerical module alone advertises no capabilities; the installed consumer above
|
||||
provides rendering. See
|
||||
[the model, evidence and remaining consumer gates](docs/render-surface.md).
|
||||
|
||||
- A bounded cooperative Radiance RGBE input utility is available in L1 render;
|
||||
it does not yet enable HDRI/PBR rendering or a document import command. See
|
||||
[the input contract](docs/render-hdr-input.md) and #813 for integration work.
|
||||
- R1 adds renderer-independent backend/profile metadata, strict request resolution and
|
||||
bounded image/scratch admission checks, exposed by the read-only `render.backends`
|
||||
command. It describes the current CPU raster snapshot path without evaluating the model.
|
||||
The legacy Design snapshot route remains unchanged. Later slices above provide the
|
||||
workspace/jobs and opaque PBR; HDRI and optional engines remain future work. See [the contract](docs/render-backends.md)
|
||||
and [the phased proposal (#620)](https://github.com/bherbruck/solidcraft/issues/620).
|
||||
|
||||
## Dimensioning and error reporting
|
||||
|
||||
- MCP `execute` accepts `verbose: false`, sharing the compact reply helper used by
|
||||
`batch`: body changes and created sketch IDs stay, while repeated lists are omitted.
|
||||
Both tools retain full replies by default.
|
||||
|
||||
- Sketch Create groups drawing variants under Line, Rectangle, Circle, Arc, Slot,
|
||||
Polygon and Spline. Each open part remembers its last interactive choice per family
|
||||
across sketch sessions and document-tab switches; closing the part resets these UI choices.
|
||||
- Inscribed, circumscribed and edge polygon tools preview their actual outline before
|
||||
placement, including orientation and the selected side count.
|
||||
- Construction (X) preserves the active sketch drawing tool, picked anchors and inline
|
||||
dimension values, so line type can change while a shape is in progress.
|
||||
- Planar sketch construction previews and hover feedback keep the same dashed pattern as
|
||||
committed geometry; centerlines retain their dash-dot pattern.
|
||||
|
||||
- Point has a default Sketch CREATE toolbar button alongside the existing drawing tools.
|
||||
Saved custom layouts are preserved; Reset Toolbar restores the default Point button.
|
||||
- Construction planes cannot take an origin-plane name (XY, XZ, YZ, Top, Front, Right). Plane
|
||||
arguments reject such a name when an older design's construction plane already has it. Use
|
||||
the other alias, the construction feature's numeric ID or an explicit plane; existing
|
||||
stored references keep their saved meaning. Section Analysis uses the same check.
|
||||
- Sketch text can be edited from a letter interior or outline's context menu or the Edit Text command.
|
||||
Interior selection requires every profile boundary to belong to the same text source;
|
||||
the editor highlights the whole source without replacing profile selections.
|
||||
Content, baseline X/Y, height and rotation update the original text source in one undo step;
|
||||
cancelling or rejecting an invalid value preserves the sketch. Position and height fields
|
||||
use the document units captured when editing starts; generated outlines remain associated
|
||||
with their text source, and untouched inputs preserve their saved internal values.
|
||||
Dragging a text outline moves the entire source in one undo step; Escape restores the
|
||||
gesture. Move/Copy on a selected text outline provides unit-aware X/Y distances and rotation
|
||||
about its baseline; copies keep an independent editable text source.
|
||||
- Repeating the initial point of a planar multi-point sketch drawing tool is neutral: the tool
|
||||
waits for a distinct point without a degenerate-shape error or history entry. Typed input
|
||||
and programmatic commands retain their validation.
|
||||
- Circular-pattern and angled-plane axes reject explicitly supplied malformed or expression
|
||||
origins instead of silently rotating about zero. Numeric origins remain in millimetres;
|
||||
associative axis expressions and construction-axis references remain unsupported.
|
||||
- Single-line and rectangle-side dimensioning supports selecting a line or its two endpoints.
|
||||
Type a value and press Enter to commit one undo step; rejected values remain editable and
|
||||
cancellation leaves the sketch unchanged. Fit keeps geometry clear of the Sketch Palette
|
||||
and navigation controls, including enlarged displays and nested pending placements.
|
||||
- Sketch Dimension takes a line and a circle or arc (#1803): it measures from the line to the
|
||||
circle's edge (the gap, 0 when tangent, or how far the edge reaches past a line that cuts
|
||||
it), drives it like any other dimension and keeps the circle on its side of the line.
|
||||
- Sketch annotations, live drawing lengths and Measure use document units. Inches show
|
||||
three decimals and retain a meaningful fourth digit. Bare length dimension entry captures
|
||||
the current design unit; changing units leaves existing geometry and expressions intact.
|
||||
Component/canvas distance controls, sketch cursor coordinates, flat-pattern dimensions
|
||||
and pocket depths, interference volumes and parts-library lengths also use design units.
|
||||
- Curvature and Polygon constraints accept complete preselected operands. Fix/Unfix
|
||||
consumes complete point/curve selections consistently; mixed or ambiguous selections
|
||||
retain the picking workflow. Rejected constraints preserve selection and undo history.
|
||||
- Contextual error reporting: a **Create Issue** action beside feature errors opens a
|
||||
reviewable GitHub issue draft with the error, operation settings, build diagnostics and
|
||||
useful reproduction context. The draft is editable, with optional capture of the actual
|
||||
viewport and copy/save controls. Opening GitHub does not submit the issue. Help also exposes
|
||||
reporting for unexpected results that did not produce an error.
|
||||
- The header exposes **Report a bug**, including an accessible compact control on
|
||||
narrow windows. It opens the same editable report and screenshot review without
|
||||
changing the active model or a pending dimension.
|
||||
- Crash reports: every panic (in a command, in the kernel, anywhere else) and every unclean
|
||||
exit (abort, stack overflow, kill: found at the next start through a locked per-run command
|
||||
journal) leaves a local report with the backtrace, version and commit, the command and the
|
||||
design that reproduce it (`solidcraft-cli crashes replay <id>`). Repeats are counted, not
|
||||
rewritten; at most 20 reports, 50 MB and 90 days. The app shows them after a crash (Help >
|
||||
Crash Reports…) with a reviewed issue draft; agents read them through
|
||||
`diagnostics.crash_reports`, the MCP `crash_reports` tool or the CLI. Nothing is sent
|
||||
anywhere. See [crash reports](docs/crash-reports.md).
|
||||
- Agent chat (off by default, desktop): a side panel that drives a coding agent the user
|
||||
already has (Claude Code through its ACP adapter, Codex, Gemini CLI, OpenCode, Grok Build, …) over the
|
||||
Agent Client Protocol, under the user's own login; SolidCraft stores no keys. The agent works
|
||||
through SolidCraft's MCP tools on a gated endpoint of its own: file actions wait for Allow or
|
||||
Deny, crash reports are off limits, 24 changes per message, one undo step per message, and
|
||||
the agent's whole process group ends with the panel or the app. Docks left or right,
|
||||
minimises to a strip. Grok Build discovery includes `~/.grok/bin` and reuses the user's
|
||||
Grok login. See [agent chat](docs/agent-chat.md).
|
||||
- Command scripts from designs (#886): `timeline.export_script` (CLI `solidcraft-cli script`)
|
||||
writes a design's history as a script that `eval` and `run` replay into the same design —
|
||||
user parameters, units, sheet metal and plastic rules and materials as their own commands,
|
||||
then each feature through `timeline.add` with its ids, names and dimension parameters kept,
|
||||
so every feature kind replays exactly and a failing feature fails the same way. Trimming the
|
||||
script after a step gives the design at that step. Appearances, folders, views and
|
||||
configurations are listed as left out; components and joints are not supported yet.
|
||||
|
||||
## 0.2.0 release batch
|
||||
|
||||
- Includes the new axis navigation, adaptive sketch drawing/picking, origin-corner repairs,
|
||||
durable supported profile/face references, endpoint dimensioning and contextual reporting.
|
||||
- Smooth ruled-wall display joins are hidden only within the supported geometric certificate;
|
||||
authored splits and actual creases remain protected. Wireframe retains physical edges.
|
||||
- Paste New uses a bounded current-geometry comparison for supported convex line/circle
|
||||
prisms with separated circular holes; unsupported shapes retain the existing strict check.
|
||||
- Common-bore Union retries check additional boundary curves before recutting the bore,
|
||||
preventing the reproduced thin-web loss. Unknown additional trims refuse; inherited
|
||||
common-cylinder trims retain their existing policy, so this is not a general Boolean proof.
|
||||
- Exact full-path assembly joint editing remains separate work and is not part of this batch.
|
||||
- Stable 0.2.0 is published: all release payload checksums, native cold reopening, deployed
|
||||
web app and both Flatpak architectures were verified. The replacement README demo uses
|
||||
the original electronics enclosure story and animated pointer in the actual English
|
||||
0.2.0 UI, at 4× playback. Publication follows [the release workflow](docs/releasing.md).
|
||||
|
||||
## 0.2.1 critical fixes
|
||||
|
||||
- Separated multi-profile Join/Cut extrusions into one solid target batch their tools using
|
||||
conservative construction bounds, avoiding a Boolean rebuild for every profile. Live
|
||||
extrusion previews build only their tools; confirmation validates the final Boolean.
|
||||
- Coplanar selected body faces share one linked sketch and one extrusion. Automatic Join/Cut
|
||||
keeps the evaluator's participant scope, including parts of an empty imported component. Desktop OK/Enter
|
||||
computes on a scratch session in the background, with atomic acceptance and cancellation.
|
||||
Planar section stitching projects lines/arcs analytically and removes duplicate endpoints.
|
||||
- Imported rotated face groups can check all full start-face seams in one guarded
|
||||
construction. Separated tools may batch a Join when each meets the same single body;
|
||||
nearby uninvolved bodies retain the original contact decisions. Regression comparisons
|
||||
cover all four extrusion operations.
|
||||
- Inward multi-face cuts retain every eligible target and subtract separated tools together.
|
||||
Safe interior classification handles roundoff at shared imported line/arc endpoints.
|
||||
Straight extrusion feedback caches existing face/profile triangles and updates the display
|
||||
directly, including a newly opened STEP's single identity component placement; other
|
||||
extents, moved or repeated components and curved sketch profiles keep the Engine preview.
|
||||
- The extrusion fixes retain tracked Boolean history and the current `solidcraft` packages.
|
||||
Confirmation and tool previews use main's shared background calculation. Main's collapsed
|
||||
blend-wall refusal and its tests are retained, including smaller valid radii and mirrored parts.
|
||||
- Axis view and navigation controls stay anchored to the viewport when feature dialogs or
|
||||
the command palette open, at normal and enlarged display scales. Fit still accounts for
|
||||
dialog coverage; covered controls do not receive clicks through dialogs.
|
||||
- Eligible planar faces survive fillet, chamfer and shell operations with exact source
|
||||
certificates. Sketches placed directly on the enclosure wall and inner floor follow
|
||||
width and wall changes, including after the USB cut and cold reopening.
|
||||
- Remove artificial straight-wall and rounded-corner subdivisions left by shell and
|
||||
extrusion cuts. Identical generated wall segments span adjacent slabs as one physical
|
||||
face, with exact full-height supports and boundary stitching. Preserve authored face
|
||||
splits and face appearances. Permanent regressions cover material, STEP, reopening and
|
||||
unaffected bodies across the extrusion end plane.
|
||||
- Feature dialogs use the available screen height for their fields, with OK and Cancel
|
||||
outside the scrolling region, including enlarged display scales.
|
||||
- Design format 13 records shell-face attachments. Older designs remain readable;
|
||||
newly saved designs require 0.2.1 or later. Ambiguous or unsupported faces still refuse
|
||||
attachment rather than guessing a plane.
|
||||
- Saved projections from a removed strip retain their geometry and report the lost
|
||||
reference. Explicitly reselect the wall to repair that projection.
|
||||
|
||||
## Workspace layouts (#2029)
|
||||
|
||||
- General preferences save Floating or Compact using the universal UI containers.
|
||||
Compact joins the workspace selector and ribbon, docks the content-hugging Browser
|
||||
flush beneath them with a square join, and opens tool sheets below the compass.
|
||||
Sheets remain draggable, rounding all corners away from their right dock; footer
|
||||
backgrounds follow the same corners. A separate preference hides ribbon icon labels
|
||||
for a shorter ribbon in either layout.
|
||||
Assistant keeps the outermost edge and History keeps its chosen dock. Switching
|
||||
retains the active tool, camera, selection and design; system windows and menus
|
||||
keep their existing behavior.
|
||||
|
||||
## View navigation
|
||||
|
||||
- Navigation has an opt-in Free orbit preference for mouse and 3D-mouse rotation through
|
||||
both poles with roll. It defaults off, preserving existing preset snapping and pole limits.
|
||||
The SolidWorks face menu offers Normal to Selection (`view.normal_to`), shared with Look At
|
||||
and Ctrl+8, including repeat-to-flip, preserving scale and projection.
|
||||
|
||||
- Desktop Ctrl/Cmd +/- and 0 change or reset the saved UI scale. The scale slider keeps
|
||||
its layout stable during a drag and applies the selected scale on release.
|
||||
|
||||
- OpenSCAD navigation preset: left drag orbits, right/middle drag pans, Shift+right/middle
|
||||
drag zooms, and the wheel zooms. Available in the shared desktop/web Navigation preferences.
|
||||
- Compact clickable signed X/Y/Z axis tips, smooth drag-to-orbit, and Shift-orbit snapping
|
||||
to 26 standard and diagonal views. Pointer motion accumulates independently of a lightly
|
||||
damped spring, with a boundary margin to prevent flicker. Z-up/Y-up and the nearest square
|
||||
top/bottom heading are preserved. Diagonals include 45-degree edge and isometric corner views.
|
||||
|
||||
## Known limitations
|
||||
|
||||
- Scheduled and manual CI scan locked dependency advisories, bans and sources with
|
||||
cargo-deny. Vulnerabilities fail the scan; unmaintained crates warn. Local vendored
|
||||
patches require separate upstream review because advisory databases cannot version-match
|
||||
their path dependencies. Pull requests retain the existing licence checks.
|
||||
|
||||
- Associative shell-face sketches require a unique certified planar source. Nested shell
|
||||
attachments and ambiguous coincident sources remain unsupported.
|
||||
- G2 fillets: exact quintic blends of straight edges between planar faces, with one planar
|
||||
end face at each end, including sloped ends (#2708). The UI and `solid.fillet` expose
|
||||
`curvature` (alias `g2`); the size is an equivalent circular setback. Curved faces/edges,
|
||||
multi-face ends and corner junctions remain unsupported.
|
||||
- Fillets and chamfers: constant, variable-radius and chord-length fillets; a radius per edge,
|
||||
rounds next to earlier rounds of another radius, and smooth corner patches where three rounds
|
||||
meet (a sphere for equal radii); perpendicular corner fillets after a closed ring of earlier
|
||||
rounds preserve the ring's radii and the enclosed planar face (#1557);
|
||||
mitres between separate rounds of the same radius keep their endpoint tangent, so a later
|
||||
fillet can select an upright and its adjoining bottom mitre as one chain (#2422);
|
||||
straight edges (convex or concave) between planar faces with perpendicular
|
||||
planar end faces (equal-distance chamfers also meet sloped planar end faces, including inner
|
||||
basin rims within the rim width); convex fillets also meet sloped planar ends and reflex
|
||||
corners around pocket rims, with unequal wall slopes and extensions confined to the face;
|
||||
connected equal-distance chamfers also trim each other across unequal sloped rim walls;
|
||||
curved chamfer chains with straight planar ends can terminate across two existing planar
|
||||
bevel faces, splitting their shared seam; picks on the same tangent chain are applied once (#1303);
|
||||
whole smooth loops of a planar face, picked whole or by one edge (tangent
|
||||
chain), including circles and walls at any angle (boss bases and tops, bores, pocket floors,
|
||||
plate outlines), loops with sharp corners between straight or mixed arc/line edges (straight
|
||||
blends meet in mitres; perpendicular curved walls use offset intersections, with D-rim removal
|
||||
checked within 0.1% of analytic offset slices), runs of
|
||||
neighbouring edges along a planar face's loop (two top edges of a box: mitred, ending square;
|
||||
with the edge between them too, the corner becomes a sphere octant),
|
||||
rounded corners rounded again (sphere octants, tori at concave corners); every edge of a
|
||||
convex planar body or of an extruded part; fillets inside patterns; closed chains of curved
|
||||
edges between any two smooth sides (a branch pipe on a main pipe), by a rolling ball (the
|
||||
blend a rational B-spline through exact arcs), closed or ending at planar faces square to the
|
||||
edges, where straight or curved cap edges are trimmed to the contacts (the straight edge
|
||||
between two overlapping extruded arcs); knife edges, where the two faces have opposite
|
||||
normals (a wall that runs out tangent into a face: the ball that fits there); an edge that
|
||||
runs out along an end face tangent to one side (the arc top edge of that wall), cut where the
|
||||
blend meets it; rounds that end in an earlier round on curved faces, in any order (the rounds
|
||||
of two edges at a vertex meet in a crease, as on a box), picked together or one after the
|
||||
other, except a round after a round of the edge between the wall and a flat face that it
|
||||
would run over; the three edges at a vertex of any smooth faces, of one radius, end in a
|
||||
piece of sphere. A limiting blend that collapses a side wall into coincident boundary
|
||||
curves is refused as too large rather than returning an unmeshable face (#1113).
|
||||
- Booleans: prisms along one direction (plates with holes, slots, patterns of them, equal
|
||||
sections stacked end to end) are combined exactly in 2D. Qualified axial cuts preserve
|
||||
annular holes with overrun cutters, including inward Press Pull after a joined extrusion;
|
||||
curved wall qualification rejects false transverse axes instead of filling holes.
|
||||
Bodies touching along one whole face
|
||||
(a part and its mirror image) are stitched; coincident planar faces are pushed apart when
|
||||
their neighbours are planes or cylinders. Other coincident curved configurations can still
|
||||
fail. Bounded containment classification preserves fully internal cavities and nested
|
||||
material islands independently of shell order, with STEP round-trip and material-membership
|
||||
regressions. Near-contact curved shells use the general boolean path rather than a
|
||||
containment shortcut. Spheres are built from six pole-free patches.
|
||||
- Mass properties come from fine tessellation (curved faces within ~1e-4 relative); triangles
|
||||
that cut through curved faces (trimmed faces after booleans) are split onto the surface first.
|
||||
- STEP import (our own reader, crates/kernel/src/step_in) reads solids (B-rep with analytic and B-spline geometry), units,
|
||||
product names, colours and assemblies; analytic offsets on planes, cylinders, cones, spheres
|
||||
and regular tori are read exactly (signed/nested distances, units and face sense). Freeform
|
||||
offset surfaces and pcurve-only edges are not read yet,
|
||||
and assembly components are flattened into bodies. Closed periodic faces (torus, B-spline
|
||||
bands) are split in two for meshing; measure() uses a finer tolerance for small radii.
|
||||
- Persistent naming follows the kernel's exact face history (docs/book/src/dev/naming.md):
|
||||
references resolve by name first, follow splits and merges, and warn when the entity split,
|
||||
merged or vanished, or was found by geometry only (operations that report no history yet).
|
||||
New planar sketch attachments use exact box, extrusion cap/side and immutable imported-face
|
||||
source certificates with checked target and author occurrence paths. A missing or changed
|
||||
source fails explicitly; Redefine Sketch Plane previews reattachment or a fixed plane while
|
||||
retaining current geometry and dimension identities. Unsupported modified faces need an
|
||||
explicit fixed plane. Legacy attachments retain their original resolver behavior.
|
||||
- Modelled threads are cut as a faceted body (curved faces within 1/200 of the pitch); Extrude
|
||||
and Revolve To Object on a curved face cut the tool back to the target's surface.
|
||||
- Explicit sketch breaks preserve exact ellipses, fit/control splines (including high degrees)
|
||||
and conics; analytic trim/break also sees free-form cutters. Free-form curves cut at a tangent
|
||||
touch (no clean crossing) enter that profile as polylines.
|
||||
Sweeps along 3D sketch curves follow their polyline (smooth walls when the path is smooth).
|
||||
A sweep with a twist, an end scale or a parallel or direction orientation (#771) follows its
|
||||
path that way too, even along sketch lines and arcs: B-spline walls through stations, ruled
|
||||
walls through denser stations where the path has corners or the profile has holes.
|
||||
Guide rails (one or two, #771) do the same: each station meets every rail once, and the rail
|
||||
turns the profile and scales it (or stretches it across to the rail) against the start; two
|
||||
rails carry it between them. Rails along a path with sharp corners are refused for now.
|
||||
- Extend intersects exact ellipses and rational/high-degree splines without moving the line or
|
||||
circular support. Associative free-form offsets have a whole-span 0.002 mm normal-distance
|
||||
certificate and refine within bounded node limits while retaining existing IDs.
|
||||
At the node limit, geometric certificate subdivisions accommodate unequal spline parameter
|
||||
speeds without changing stored nodes or relaxing the distance tolerance (#971).
|
||||
Associated offset fit nodes stay hidden from display, picking and snapping; dragging the
|
||||
editable source updates the derived boundary live. Break Association restores point editing.
|
||||
Singular, self-intersecting and ambiguous curves refuse atomically; mixed junctions and nonsmooth
|
||||
closed natural-spline seams remain limitations. New explicitly closed fit splines and closed
|
||||
offset outputs use periodic C2 interpolation, supporting repeated certified offsets; older
|
||||
natural splines are preserved. Clicking the starting fit point deliberately closes a new loop.
|
||||
Newly captured adaptive profile references with complete source coverage retain their outer
|
||||
and hole identities through span-count changes, including parameter edits after extrusion.
|
||||
Partial, intersected or ambiguous boundaries do not qualify for these whole-source references.
|
||||
Legacy curve-span multisets remain exact and unchanged; already-lost selections may need
|
||||
reselection. Format 12 stores the new bounded whole-boundary references.
|
||||
Projected analytic boundaries follow nested loop identities through resizing, including
|
||||
stationary holes and rounded corners. Supported convex, tangent line-and-arc boundaries
|
||||
can lose or regain corner arcs during an inset while preserving reserved curve identities
|
||||
and linked profile selections. Format 12 saves those associations; compatible older closed
|
||||
offsets upgrade structurally without changing their curve IDs. Breaking an association
|
||||
freezes its current boundary and materializes downstream profile references. Child offsets
|
||||
of a boundary with retired arcs still require breaking its association first; mixed or
|
||||
nonconvex junctions and retained references to disappearing individual arcs remain limited.
|
||||
- Extrude and Revolve editors retain original direction, extent expressions, explicit targets,
|
||||
axes, From/To inputs and All-profile intent. No-op edits preserve downstream dimension names
|
||||
and selected occurrence frames; cancel and successful edits preserve one-step undo history.
|
||||
Placed feature edits preview through the edited timeline entry, and cylindrical Emboss guides
|
||||
use the sketch's actual target, author or uniquely placed third-component frame.
|
||||
Revolve rotators follow the actual signed axis and support symmetric and two-sided extents;
|
||||
dragging a To Object extent converts its derived value to an explicit angle. Legacy reversed-axis
|
||||
symmetric geometry is preserved through format 11's explicit legacy convention. New revolves
|
||||
use the sketch-normal direction for each selected profile, including To Object and curved
|
||||
target trimming, independently of the selected axis sign.
|
||||
- Cylindrical emboss/deboss wraps bounded orthogonal line profiles, including holes, concave
|
||||
outlines and nested islands, onto complete exterior cylinders or coaxial sleeves with exact
|
||||
radial walls. New features infer a unique eligible cylinder when the face field is empty;
|
||||
ambiguous targets require an explicit face. Saved target
|
||||
and author occurrence paths retain the chosen placements; missing/split faces refuse rather
|
||||
than retarget. General free-form profiles, seams, stepped cylinders and other curved surfaces
|
||||
remain unsupported. STEP round-trip and independent radial/volume checks cover this subset.
|
||||
- Shell and offset faces work on bodies of planes, cylinders, cones, spheres and tori (convex or not:
|
||||
filleted boxes, bosses, domes, countersinks; corners where four or more planes meet, such as a
|
||||
floor falling to a drain, as long as the corner's edges are all convex or all concave); draft on convex planar bodies and on walls (planes, cylinders → cones) between caps; lofts through three or more sections are smooth (B-spline sides through every section), two
|
||||
sections ruled. Circular-meridian barrels retain curved inner walls through either axial
|
||||
opening (#2421). Solid lofts preserve holes through sections with matching hole counts
|
||||
(#2435), including inner walls and caps with openings; a hole cannot appear or disappear
|
||||
between sections, and sections with holes cannot collapse to a point.
|
||||
|
||||
- Sketch corner tools: fillet and all three chamfer modes pick a connected corner even when
|
||||
its endpoint overlaps the fixed origin or a loose point. Ordinary drawing still snaps to
|
||||
the origin; clicked UI regressions verify geometry, closed profiles and undo.
|
||||
- Sketch display: drawing, picking, highlighting, tool previews and profile shading share
|
||||
adaptive projected curve boundaries. Fit, zoom, display scale and placed sketch frames
|
||||
invalidate the display cache without changing CAD geometry or timeline history. Work
|
||||
budgets and clipped paths prevent unbounded sampling or invented edges across the eye plane.
|
||||
The Sketch Palette scrolls its options on short windows and enlarged displays while
|
||||
keeping constraint status and Finish Sketch visible.
|
||||
- Face projection: closed boundaries have a deterministic outer-first order. Refresh matches
|
||||
existing geometry before assigning positions, retaining saved curve IDs and constraints
|
||||
across enumeration changes and resize, including concentric circles. Ambiguous identities
|
||||
retain the last geometry and report a lost reference; permutation matching is bounded.
|
||||
- Project accepts complete supported preselection in one undo step. Visible external sketch
|
||||
curves and points retain their source sketch identity; model geometry, axes and planes use
|
||||
the existing linked projection commands. Rejection preserves the document and selection;
|
||||
mixed selections retain ordinary tool-first picking.
|
||||
- Inline sketch dimensions: Tab and Shift+Tab cycle within the active shape's value boxes,
|
||||
selecting visited values for replacement. Expression completion retains forward-Tab
|
||||
priority; real keyboard regressions cover rectangles, lines and single-value circles.
|
||||
Typed lengths, radii, line angles, signed arc sweeps and slot lengths persist as constraints
|
||||
and expressions through save/reopen. Shape creation and its dimensions form one undo step;
|
||||
rejected values keep the editable field focused.
|
||||
- Repeated components: Browser selection, visibility, properties, copy and Move/Copy use
|
||||
checked occurrence paths. Active nested occurrences author planar sketches and solids in
|
||||
their selected frame. Pending moves compose through parents; dialog cancellation restores
|
||||
its selected move. Source materials and body/face appearances follow transformed instances.
|
||||
Shared nested definitions remain shared. Deep Paste New copies reachable definition graphs,
|
||||
independent feature/input histories, styles and internal motion records, with bounded geometry
|
||||
checks and explicit external-link notices. Paste New selects the copy while preserving the active
|
||||
authoring context. Imported periodic STEP seams survive placement and reexport with shared
|
||||
topology and appearances intact. Recorded body transfers preserve historical Join/Cut membership,
|
||||
material, placements and machining data through edits, copying, undo and reopen. New 3D wires
|
||||
record their coordinate frame; legacy wires migrate from known provenance without changing
|
||||
coordinates. Broken legacy links with missing provenance can require explicit refresh or relink.
|
||||
See [occurrence workflows](docs/occurrence-workflows.md).
|
||||
- Toolbar drops on panel labels, gaps and margins append commands with insertion feedback;
|
||||
button drops keep insert-before ordering.
|
||||
- Parameters and Configurations have distinct actions in the SOLID toolbar's CONFIGURE panel.
|
||||
Parameters opens the user/model variable editor; Configurations opens the configuration table.
|
||||
Configure remains available in the panel dropdown.
|
||||
- Parameter creation supports Enter with validation and continued keyboard entry. Shared
|
||||
expression autocomplete supports Up/Down selection, Enter/Tab acceptance and Escape dismissal;
|
||||
accepting a suggestion does not submit a parameter or trigger viewport shortcuts.
|
||||
- Movable window title bars follow the app's dark/light header palette. Radial right-click
|
||||
menus are opt-in in Preferences > Navigation; ordinary context menus remain the default.
|
||||
- Desktop distribution: running Windows windows and executable resources use the app icon;
|
||||
packaging validates icon resources. Nix installs launchers, MIME data and generated icons,
|
||||
with explicit launch apps and Vulkan-loader/driver discovery. Flatpak feed instructions
|
||||
register the required runtime source; release checks exercise desktop modelling on X11/Wayland.
|
||||
- OpenGL rendering clamps effective anti-aliasing to 1x across UI pipelines, textures and CAD
|
||||
callbacks to prevent black frames. Other backends retain the requested sample count, and
|
||||
stored preferences remain unchanged.
|
||||
- Configuration rules: ordered typed conditions can drive parameters and suppressions or
|
||||
restrict values. Preview explains effective values without modifying the document; applying
|
||||
rules validates rows atomically. Length expressions retain their authoring units across
|
||||
document-unit changes. The UI supports structured All/Any/Not conditions and rule ordering.
|
||||
Component styles and certified semantic face styles are available; unsupported lineage refuses
|
||||
atomically, and stale certified face paint cannot jump to another surface. See [configurations](docs/configurations.md).
|
||||
- Sheet-metal material: inherited holes, crossing edge notches, overlapping cuts and retained
|
||||
islands share the same boundaries in flat preview, DXF and unfolded solids. Detached material
|
||||
pieces survive unfold/refold as one logical sheet group, including split flanged sheets.
|
||||
Cut-outs made in the flat pattern cross bends at any angle or curved: their edges fold to
|
||||
helices and curves on the bend, their walls square to the sheet. Free-form bases use bounded
|
||||
sampling.
|
||||
Parallel prismatic partial-depth cuts retain pocket side and depth through transfer and
|
||||
unfold/refold, with separate pocket contours in preview and DXF. Planar Intersect preserves
|
||||
exact holes and retained islands, including the union of multiple selected profiles, and
|
||||
records complementary depth layers as pockets while retaining the stock gauge. Unsupported
|
||||
tapered or bend-crossing machining refuses atomically instead of restoring material later.
|
||||
Sketches attach to a sheet's flat faces (base and flanges, either side) by panel, in the
|
||||
flat pattern's coordinates, and follow it through rebuilds and unfold/refold.
|
||||
A flange on another flange's far edge (a return) follows that flange's closed end and keeps
|
||||
clear of the flange beside it, and the returns of one feature are mitred toward each other
|
||||
with the rule's gap where their flanges meet at a corner; returns of separate features, and
|
||||
flanges on returns, are not closed toward each other.
|
||||
- Rolling-ball fillet accuracy: independent offset-cylinder tee references check surface
|
||||
distance, actual trimmed tessellation, supporting-cylinder contacts and tangency at two
|
||||
scales. The measured trimmed-part error is below 0.0001 mm; this is an analytic geometry
|
||||
check. Under-resolved fits return an error.
|
||||
- Contact driving checks the trajectory between clear endpoints, including linked joints,
|
||||
playback and sparse motion exports. Checks use bounded tessellation samples, not exact
|
||||
continuous collision detection; local thin walls and small details remain limitations.
|
||||
See [contact-set limits and recovery](docs/contact-sets.md).
|
||||
|
||||
## Assembly dragging
|
||||
|
||||
The design's first part is grounded when it is made, and a left-button drag repositions a free part (`occurrence.drag`: one undo step, exact occurrence paths including repeated jointed definitions, any floating-chain member carries the chain, grounded parts and joints hold where present, Escape cancels).
|
||||
|
||||
## 3D sketch bridges
|
||||
|
||||
3D sketch drawing snaps to points on other visible sketch planes without flattening them onto
|
||||
the active plane. Bridge endpoints capture their source positions; associative endpoint links,
|
||||
construction-plane selection of 3D wires and loft guide rails remain separate work.
|
||||
Feature dialog header chevrons collapse and restore the form without discarding selections
|
||||
or edited values, including the Appearance panel.
|
||||
## Construction on 3D paths
|
||||
|
||||
Plane Along Path has a viewport path selector, fractional position, live preview and timeline
|
||||
editing. Construction planes and points resolve drawn 3D wires without applying their sketch
|
||||
plane a second time. Curved paths use sampled segments; multi-curve chains, absolute distance
|
||||
and reference-object placement remain separate work, as do loft rails and centerlines.
|
||||
|
||||
## Design properties (#1015)
|
||||
|
||||
- Designs carry project, part number, revision, author, organization, description and licence
|
||||
in an optional `metadata` object (File → Design Properties…, `document.metadata.*`,
|
||||
`inspect_design`). STEP and IGES headers and 3MF model metadata take the fields their format
|
||||
has a place for. Other tools keep their own data under a reverse-DNS key, kept as it is
|
||||
through open, edit and save and never exported. See [design properties](docs/book/src/guide/files.md#design-properties).
|
||||
Per-component properties (a part number per component) can follow with the same type.
|
||||
|
||||
## Document-opening performance
|
||||
|
||||
- Desktop STEP import prepares consecutive independent solids in bounded batches with up
|
||||
to eight workers, retaining serial traversal, result order and persistent source identities.
|
||||
The web path remains serial. Original multi-solid fixtures show 64–81% lower complete
|
||||
import times; single-solid assembly parts are not scheduled across nodes. See
|
||||
[measurements and limits](docs/perf.md#parallel-step-body-preparation-2026-10-12).
|
||||
- Design containers (#303, phase one) separate and deduplicate STEP, raw images and
|
||||
lossless binary mesh arrays while keeping plain JSON as a supported Save As/export
|
||||
format. Generated-mesh storage measurements reduce serialization and decoding work;
|
||||
a seekable reader defers original blobs until after provisional display.
|
||||
See [container format](docs/design-container.md) and [performance measurements](docs/perf.md).
|
||||
- Warm saves with explicit `cache: true` can include a versioned native B-rep cache through SolidCraft-owned topology
|
||||
and geometry records, with a separate truck adapter and bounded binary reader. Exact
|
||||
face-source certificates and operation history survive codec round trips. A provisional
|
||||
picture can be generated before original blobs are read; editing and measurement wait
|
||||
for authoritative reconstruction. Cached metadata binds preview and reconstruction to
|
||||
the same document despite concurrent file writes. Default saves omit this cache and never force cold
|
||||
evaluation or meshing; explicit `preview: true` remains a separate warm-mesh option.
|
||||
Keys, resource budgets and document/revision guards reject incompatible, hostile or
|
||||
stale results. A second modelling kernel still requires its own tested adapter.
|
||||
- Warm saves with explicit `cache: true` can also carry a bounded PNG thumbnail from already cached meshes.
|
||||
Home reads it without rebuilding the design when its local picture is missing;
|
||||
mismatched or malformed thumbnails fall back to normal generation. The image is
|
||||
kernel-independent and saving never forces cold meshing to produce it.
|
||||
|
||||
- Cold rebuild profiling separates parsing, geometry, placement, display meshing and
|
||||
thumbnails. Straight-line closest-point projection and exact curved-surface evaluation
|
||||
reuse reduce repeated work without relaxing geometry checks. Three alternating optimized
|
||||
benchmark runs reduce median rebuild time by 26% on the public sensor enclosure and 22%
|
||||
on a local case assembly; shape measurements and topology match the original build.
|
||||
See [performance measurements](docs/perf.md). Further cold Boolean and import costs remain.
|
||||
- Prepared assembly geometry survives selection and view changes while real geometry and
|
||||
placement changes invalidate it. A Pi assembly edge-selection probe drops from 9.24 s to
|
||||
0.57 s; initial opening is unchanged. Actual Windows desktop profiling measures 15.87 s
|
||||
median opening, with 82% spent rebuilding geometry; halving full opening remains open.
|
||||
|
||||
## Kernel
|
||||
|
||||
- Successful Boolean retries report their strategy and numerical adjustments as
|
||||
feature warnings and structured inspection data. Diagnostics survive cached
|
||||
evaluation, undo/redo and cold reopening; the robustness runner counts retry
|
||||
events by strategy and reports any bounded-log truncation. Existing geometry
|
||||
acceptance checks remain in force.
|
||||
|
||||
- Cylinders of one radius whose axes cross (a second bore across a bore of the same size, a pipe
|
||||
tee of equal pipes) touch at two points, where the mesh intersection cannot trace the curves
|
||||
and a boolean cannot tell the four arms of the crossing apart. When the ordinary boolean fails
|
||||
on such a pair, both faces are split along the line of their cylinder through the touching
|
||||
points (the solids are unchanged), the boolean is taken of those, and its history is composed
|
||||
back over the operands as given (#788). The result keeps the split lines as edges of the bore
|
||||
walls. A crossing the ordinary route fails on and the split faces do not help (a radius of 17.5
|
||||
in a 45 mm block with a tool of 80 mm) is still refused.
|
||||
- Boolean intersection-curve construction refuses point-only, collapsed and non-finite
|
||||
polylines without panicking. Direct constructor regressions retain valid open endpoints
|
||||
and closed spherical intersections; the original interactive trigger remains unidentified.
|
||||
- Shell of a stepped, joined planar body no longer creates horizontal subdivisions across
|
||||
continuous walls. Fresh axis-aligned Cut walls are stitched using their exact construction
|
||||
support and shared topology, preserving real steps and intentional boundaries. The local
|
||||
reported case returns from 86 to 49 faces with unchanged material and bounds; regressions
|
||||
cover stepped stock, STEP round trips and saved projections from removed pieces.
|
||||
|
||||
truck (Apache-2.0) behind our own `kernel` crate, with our own blend operation and boolean
|
||||
retry/validation; see `plan/adr/0001-geometry-kernel.md` (local) — summary in README.
|
||||
|
||||
+34
-48
@@ -1,6 +1,6 @@
|
||||
Third-party software in SolidCraft
|
||||
|
||||
SolidCraft (the desktop app, solidcraft-cli and the web app) is built from the 560 crates below.
|
||||
SolidCraft (the desktop app, solidcraft-cli and the web app) is built from the 559 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.
|
||||
|
||||
@@ -439,7 +439,6 @@ typed-path 0.12.3 (MIT OR Apache-2.0)
|
||||
typeid 1.0.3 (MIT OR Apache-2.0)
|
||||
typenum 1.20.1 (MIT OR Apache-2.0)
|
||||
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-segmentation 1.13.3 (MIT OR Apache-2.0)
|
||||
@@ -1124,9 +1123,9 @@ parking_lot_core 0.9.12, percent-encoding 2.3.2, piper 0.2.5, plain 0.2.3, png 0
|
||||
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, syn 1.0.109, 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,
|
||||
1.2.1, syn 1.0.109, tempfile 3.27.0, ttf-parser 0.25.1, 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
|
||||
================================================================================
|
||||
@@ -14525,49 +14524,6 @@ MIT License
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE
|
||||
|
||||
================================================================================
|
||||
unicode-bidi 0.3.18
|
||||
================================================================================
|
||||
|
||||
This project is copyright 2015, The Servo Project Developers (given in the
|
||||
file AUTHORS).
|
||||
|
||||
Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
||||
http://www.apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||
http://opensource.org/licenses/MIT>, at your option. All files in the project
|
||||
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
|
||||
================================================================================
|
||||
|
||||
Copyright (c) 2015 The Rust Project Developers
|
||||
|
||||
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.
|
||||
|
||||
================================================================================
|
||||
unicode-ident 1.0.26
|
||||
================================================================================
|
||||
@@ -14616,6 +14572,36 @@ authorization of the copyright holder.
|
||||
unicode-segmentation 1.13.3, unicode-width 0.2.2
|
||||
================================================================================
|
||||
|
||||
Copyright (c) 2015 The Rust Project Developers
|
||||
|
||||
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.
|
||||
|
||||
================================================================================
|
||||
unicode-segmentation 1.13.3, unicode-width 0.2.2
|
||||
================================================================================
|
||||
|
||||
Licensed under the Apache License, Version 2.0
|
||||
<LICENSE-APACHE or
|
||||
http://www.apache.org/licenses/LICENSE-2.0> or the MIT
|
||||
|
||||
@@ -19,17 +19,6 @@ path = "src/main.rs"
|
||||
name = "solvecraft-cli"
|
||||
path = "src/alias.rs"
|
||||
|
||||
[features]
|
||||
default = ["raytrace-cpu", "smlib", "profile"]
|
||||
bundled-render-library = ["solidcraft-engine/bundled-render-library"]
|
||||
raytrace-cpu = ["solidcraft-engine/raytrace-cpu"]
|
||||
# Link SMLib into the kernel (solidcraft-engine/smlib; needs scripts/fetch-smlib.sh). SMLib is the
|
||||
# only kernel, so without it the command line models nothing.
|
||||
smlib = ["solidcraft-engine/smlib"]
|
||||
# The profiler, compiled in so any build can say where its time goes (`--profile`,
|
||||
# SOLIDCRAFT_PROFILE=1); it records nothing until asked.
|
||||
profile = ["solidcraft-engine/profile"]
|
||||
|
||||
[dependencies]
|
||||
solidcraft-engine = { workspace = true }
|
||||
solidcraft-gpu = { workspace = true }
|
||||
@@ -40,5 +29,3 @@ serde_json = { workspace = true }
|
||||
[dev-dependencies]
|
||||
# Snapshot tests decode the pictures to compare them with a tolerance.
|
||||
image = { workspace = true }
|
||||
# Tests measure meshes at their own tolerances.
|
||||
solidcraft-kernel = { workspace = true }
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
//! Reproducible evidence through the production command/job interfaces.
|
||||
//! Usage: studio_evidence RECIPE.json RENDER-PARAMS.json OUTPUT.png
|
||||
//! Writes the editable design, command timings and actual adapter/job metadata beside PNG.
|
||||
//! Accepts runtime requests as well as inline or named saved render jobs.
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_engine::{
|
||||
Session,
|
||||
doc::render_settings::{RenderJob, RenderSetup},
|
||||
render::job::{CpuHost, ErrorCode, JobId, Phase},
|
||||
};
|
||||
use std::{
|
||||
@@ -23,35 +21,6 @@ fn read_json(path: &str) -> Result<Value, String> {
|
||||
serde_json::from_slice(&bytes).map_err(|e| format!("{path}: {e}"))
|
||||
}
|
||||
|
||||
// Only decide which optional host must be installed. The production command
|
||||
// still validates and resolves the actual request without a second wire format.
|
||||
fn needs_gpu(session: &Session, params: &Value) -> Result<bool, String> {
|
||||
let inline = params.get("settings").filter(|value| !value.is_null());
|
||||
let named = params.get("setup").filter(|value| !value.is_null());
|
||||
if inline.is_some() && named.is_some() {
|
||||
return Err("choose either setup or settings".into());
|
||||
}
|
||||
let inline_setup = inline
|
||||
.filter(|value| value.get("job").is_some_and(|job| !job.is_null()))
|
||||
.map(|value| serde_json::from_value::<RenderSetup>(value.clone()).map_err(|e| e.to_string()))
|
||||
.transpose()?;
|
||||
let setup = if let Some(name) = named.and_then(Value::as_str) {
|
||||
Some(session.doc.render.setups.get(name).ok_or("no saved render setup with that name")?)
|
||||
} else {
|
||||
inline_setup.as_ref()
|
||||
};
|
||||
if let Some(setup) = setup
|
||||
&& let Some(job) = &setup.job
|
||||
{
|
||||
if params.as_object().is_some_and(|object| object.keys().any(|key| !matches!(key.as_str(), "setup" | "settings" | "replace"))) {
|
||||
return Err("saved render jobs cannot be mixed with runtime controls".into());
|
||||
}
|
||||
setup.validate().map_err(|e| e.to_string())?;
|
||||
return Ok(matches!(job, RenderJob::GpuPath { .. }));
|
||||
}
|
||||
Ok(params.get("progressive").and_then(Value::as_bool) == Some(true))
|
||||
}
|
||||
|
||||
fn run() -> Result<(), String> {
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
let [recipe_path, params_path, output_path] = args.as_slice() else {
|
||||
@@ -78,36 +47,18 @@ fn run() -> Result<(), String> {
|
||||
.collect();
|
||||
drop(scene);
|
||||
|
||||
let cpu_host_start = Instant::now();
|
||||
let policy = solidcraft_gpu::select::Policy::from_env().map_err(|e| e.to_string())?;
|
||||
let (adapter, adapter_report) = solidcraft_gpu::select::pick(policy).ok_or("no adapter satisfies the requested GPU policy")?;
|
||||
let host_start = Instant::now();
|
||||
session.set_render_job_host(Some(CpuHost::for_platform().map_err(|e| e.to_string())?)).map_err(|e| e.to_string())?;
|
||||
let cpu_host_seconds = cpu_host_start.elapsed().as_secs_f64();
|
||||
// Match runtime and saved-job dispatch. CPU evidence must not require a
|
||||
// Vulkan device, parse GPU policy or initialize a GPU host.
|
||||
let (adapter_report, gpu_host_seconds) = if needs_gpu(&session, ¶ms)? {
|
||||
let gpu_host_start = Instant::now();
|
||||
let policy = solidcraft_gpu::select::Policy::from_env().map_err(|e| e.to_string())?;
|
||||
let (adapter, report) =
|
||||
solidcraft_gpu::select::pick_vulkan(policy, |_| true).ok_or("no Vulkan adapter satisfies the requested GPU policy")?;
|
||||
session.register_render_job_host(solidcraft_gpu::path::host_on_adapter(&adapter).map_err(|e| e.to_string())?).map_err(|e| e.to_string())?;
|
||||
(Some(report), Some(gpu_host_start.elapsed().as_secs_f64()))
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
session.register_render_job_host(solidcraft_gpu::path::host_on_adapter(&adapter).map_err(|e| e.to_string())?).map_err(|e| e.to_string())?;
|
||||
let host_seconds = host_start.elapsed().as_secs_f64();
|
||||
let preflight = session.execute("render.preflight", ¶ms).map_err(|e| e.to_string())?;
|
||||
let start = Instant::now();
|
||||
let started = session.execute("render.start", ¶ms).map_err(|e| e.to_string())?;
|
||||
let executor = started
|
||||
.get("resolved")
|
||||
.and_then(|resolved| resolved.get("executor"))
|
||||
.and_then(Value::as_str)
|
||||
.ok_or("started job did not report the resolved image executor")?
|
||||
.to_owned();
|
||||
let id: JobId = serde_json::from_value(started.get("job").cloned().ok_or("missing job id")?).map_err(|e| e.to_string())?;
|
||||
let handle = session.render_job_handle(id).map_err(|e| e.to_string())?;
|
||||
eprintln!("Rendering through production {executor} executor");
|
||||
if let Some(adapter) = &adapter_report {
|
||||
eprintln!("GPU adapter: {adapter:?}");
|
||||
}
|
||||
eprintln!("Rendering through production gpu-path host: {adapter_report:?}");
|
||||
let status = loop {
|
||||
match handle.wait_timeout(Duration::from_secs(10)) {
|
||||
Ok(status) => break status,
|
||||
@@ -133,11 +84,11 @@ fn run() -> Result<(), String> {
|
||||
}
|
||||
let exported = session.execute("render.export", &json!({"job":id,"path":output_path,"overwrite":true})).map_err(|e| e.to_string())?;
|
||||
let report = json!({
|
||||
"recipe":recipe_path,"render_params":params,"executor":executor,"adapter":adapter_report,
|
||||
"recipe":recipe_path,"render_params":params,"adapter":adapter_report,
|
||||
"environment":{"SOLIDCRAFT_GPU":std::env::var("SOLIDCRAFT_GPU").ok(),"WGPU_BACKEND":std::env::var("WGPU_BACKEND").ok()},
|
||||
"cad_generation_seconds":generation_seconds,"cpu_host_setup_seconds":cpu_host_seconds,"gpu_host_setup_seconds":gpu_host_seconds,
|
||||
"cad_generation_seconds":generation_seconds,"gpu_host_setup_seconds":host_seconds,
|
||||
"render_and_encode_seconds":render_and_encode_seconds,
|
||||
"timing_scope":"wall time from render.start through completed PNG encoding; includes job preparation and readback, excludes CAD generation, preflight and initial CPU/GPU host setup; GPU setup timing includes adapter selection and pipelines when requested",
|
||||
"timing_scope":"wall time from render.start through completed PNG encoding; includes job preparation and readback, excludes CAD generation and initial GPU pipeline setup",
|
||||
"bodies":bodies,"commands":commands,"command_timings":command_timings,
|
||||
"preflight":preflight,"export":exported,"editable_design":design_path
|
||||
});
|
||||
|
||||
@@ -184,7 +184,7 @@ pub fn run(args: &[String]) -> Result<(), String> {
|
||||
s.execute(id, &c["params"]).map_err(|e| format!("command {i} ({id}): {e}"))?;
|
||||
run.push(t.elapsed().as_secs_f64() * 1000.0);
|
||||
let t = std::time::Instant::now();
|
||||
s.world_state().map_err(|e| e.to_string())?.warm_meshes(|| true);
|
||||
s.world_state().warm_meshes(|| true);
|
||||
shown.push(t.elapsed().as_secs_f64() * 1000.0);
|
||||
if run.len() == 100 || i + 1 == commands.len() {
|
||||
let mean = |v: &[f64]| v.iter().sum::<f64>() / v.len().max(1) as f64;
|
||||
@@ -198,7 +198,7 @@ pub fn run(args: &[String]) -> Result<(), String> {
|
||||
shown.clear();
|
||||
}
|
||||
}
|
||||
let world = s.world_state().map_err(|e| e.to_string())?;
|
||||
let world = s.world_state();
|
||||
println!(
|
||||
"{}",
|
||||
json!({"step": "built", "ms": began.elapsed().as_secs_f64() * 1000.0, "commands": commands.len(), "features": s.doc.features.len(), "bodies": world.bodies.len()})
|
||||
@@ -233,7 +233,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
assert!(s.model.results.iter().all(|r| r.error.is_none()));
|
||||
let world = s.world_state().unwrap();
|
||||
let world = s.world_state();
|
||||
assert!(world.bodies.len() > 40, "{} bodies", world.bodies.len());
|
||||
assert!(world.bodies.iter().any(|b| b.name.contains("spoke")));
|
||||
}
|
||||
|
||||
@@ -69,7 +69,6 @@ impl Control {
|
||||
return Err("--connect requires a loopback address and a nonzero port (the app listens on 127.0.0.1)".into());
|
||||
}
|
||||
let token = solidcraft_engine::control_auth::client_token(addr.port(), token)
|
||||
.map(|t| t.token)
|
||||
.ok_or("no control token: pass --token, set SOLIDCRAFT_CONTROL_TOKEN, or run on the app's computer (its token file)")?;
|
||||
if token.len() > 4096 {
|
||||
return Err("the control token is longer than 4096 bytes".into());
|
||||
|
||||
@@ -40,6 +40,8 @@ pub fn one(args: &[String]) -> Result<(), String> {
|
||||
"steps": run.steps.iter().map(|s| s.kind).collect::<Vec<_>>(),
|
||||
"unsupported": run.unsupported,
|
||||
"rejected": run.rejected,
|
||||
"boolean_fallbacks": run.boolean_fallbacks,
|
||||
"boolean_reports_truncated": run.boolean_reports_truncated,
|
||||
"panics": PANICS.swap(0, std::sync::atomic::Ordering::SeqCst),
|
||||
});
|
||||
if run.outcome.is_bug() {
|
||||
@@ -189,9 +191,23 @@ fn summarise(results: &[Value], took: Duration) -> String {
|
||||
let mut boolean: BTreeMap<String, usize> = BTreeMap::new();
|
||||
let mut at_step: BTreeMap<String, usize> = BTreeMap::new();
|
||||
let mut unsupported: BTreeMap<String, usize> = BTreeMap::new();
|
||||
let mut fallbacks: BTreeMap<String, [u64; 2]> = BTreeMap::new();
|
||||
let mut truncated = [0usize; 2];
|
||||
let mut steps = 0;
|
||||
let (mut dp, mut bp) = (0, 0);
|
||||
for v in results {
|
||||
for (i, group) in ["design", "boolean"].into_iter().enumerate() {
|
||||
if v[group]["boolean_reports_truncated"].as_bool().unwrap_or(false) {
|
||||
truncated[i] += 1;
|
||||
}
|
||||
if let Some(counts) = v[group]["boolean_fallbacks"].as_object() {
|
||||
for (strategy, count) in counts {
|
||||
if let Some(total) = fallbacks.entry(strategy.clone()).or_default().get_mut(i) {
|
||||
*total = total.saturating_add(count.as_u64().unwrap_or(0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
dp += usize::from(v["design"]["panics"].as_u64().unwrap_or(0) > 0);
|
||||
bp += usize::from(v["boolean_panics"].as_u64().unwrap_or(0) > 0);
|
||||
let k = if v.get("timeout").is_some() {
|
||||
@@ -230,6 +246,11 @@ fn summarise(results: &[Value], took: Duration) -> String {
|
||||
for (k, c) in &boolean {
|
||||
s += &format!("- {k}: {}\n", pct(*c));
|
||||
}
|
||||
s += "\nSuccessful Boolean retries (events, including nested operations):\n";
|
||||
for (strategy, [designs, checks]) in &fallbacks {
|
||||
s += &format!("- {strategy}: designs {designs}, boolean checks {checks}\n");
|
||||
}
|
||||
s += &format!("- Capped reports: designs {}, boolean checks {}\n", truncated[0], truncated[1]);
|
||||
s
|
||||
}
|
||||
|
||||
@@ -238,13 +259,15 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn report_counts_outcomes_and_timeouts() {
|
||||
fn report_counts_retry_events_separately_and_marks_incomplete_logs() {
|
||||
let results = [
|
||||
json!({"design":{"outcome":{"kind":"ok"}},"boolean":{"outcome":"ok"}}),
|
||||
json!({"design":{"outcome":{"kind":"ok"}},"boolean":{"outcome":"ok"}}),
|
||||
json!({"design":{"outcome":{"kind":"ok"},"boolean_fallbacks":{"shifted_operands":2}},"boolean":{"outcome":"ok","boolean_fallbacks":{"shifted_operands":3},"boolean_reports_truncated":true}}),
|
||||
json!({"design":{"outcome":{"kind":"ok"},"boolean_fallbacks":{"shifted_operands":1}},"boolean":{"outcome":"ok"}}),
|
||||
json!({"timeout":90}),
|
||||
];
|
||||
let text = summarise(&results, Duration::ZERO);
|
||||
assert!(text.contains("shifted_operands: designs 3, boolean checks 3"), "{text}");
|
||||
assert!(text.contains("Capped reports: designs 0, boolean checks 1"), "{text}");
|
||||
assert!(text.contains("timeout: 1 (33.3 %)"), "{text}");
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+32
-144
@@ -1,25 +1,23 @@
|
||||
//! SolidCraft headless command line.
|
||||
//!
|
||||
//! ```text
|
||||
//! solidcraft-cli run <script.json|design.solidcraft|part.step|mesh.3mf> [--in design|part.step|mesh.3mf] [--out FILE]... [--save FILE] [--quiet] [--results] [--timings] [--profile] [--json-out FILE] [--allow-errors] [--validation MODE]
|
||||
//! solidcraft-cli eval <script.json|design.solidcraft|part.step> [--in design.solidcraft|part.step] [--json-out FILE] (alias: inspect) measurements as JSON
|
||||
//! solidcraft-cli run <script.json|design.solidcraft|part.step|mesh.3mf> [--in design|part.step|mesh.3mf] [--out FILE]... [--save FILE] [--quiet] [--results] [--timings] [--profile] [--json-out FILE] [--allow-errors]
|
||||
//! solidcraft-cli eval <script.json|design.solidcraft|part.step> [--json-out FILE] (alias: inspect) measurements as JSON
|
||||
//! solidcraft-cli script <design.solidcraft> [--out script.json] the design's history as a command script
|
||||
//! solidcraft-cli bench-open <design.solidcraft | part.step> [--parse-only] [--save OUT.solidcraft] [--display LEVEL [--edit NAME EXPR]] [--ops script.json] loading timings as JSON lines
|
||||
//! solidcraft-cli bench-agent [--scale F] [--save F] [--script-out F] a ~1,300-body design built command by command, timed
|
||||
//! solidcraft-cli bench-assistant --connect 127.0.0.1:PORT [--token T] --base-url URL --model NAME [--tasks 1,2,5] [--repeat N]
|
||||
//! [--timeout-s 300] [--json out.json] [--markdown out.md]
|
||||
//! the standard Assistant model test (#4851) on a running app
|
||||
//! solidcraft-cli snapshot <script|design> [--in design|part.step] --out shot.png [--width W] [--height H] [--view iso|front|top|…|NAME | --camera JSON] [--no-grid] [--no-axes] [--quality LEVEL]
|
||||
//! solidcraft-cli snapshot <script|design> [--in design|part.step] --out shot.png [--width W] [--height H] [--view iso|front|top|…|NAME | --camera JSON] [--no-grid] [--no-axes]
|
||||
//! [--body NAME:opacity=0.3,color=#rrggbb]... [--tool NAME]... [--section z=27]
|
||||
//! solidcraft-cli exec <command> [json-params] run one command on an empty design
|
||||
//! solidcraft-cli commands the command registry as JSON
|
||||
//! solidcraft-cli recipe <recipe.json> translate an oracle recipe to a script
|
||||
//! solidcraft-cli oracle <case-dir>... replay recipes and compare with expected.json
|
||||
//! solidcraft-cli kernel-cases [--json] [--dumps=DIR] [--validation=MODE] <case-dir>... the kernel regression rig (tests/kernel-cases)
|
||||
//! solidcraft-cli mcp [--headless] [--in design|script] [--connect HOST:PORT [--token T]] [--allow-path DIR]... [--allow-any-path]
|
||||
//! [--param-check off|warn|strict]
|
||||
//! MCP server on stdio (docs/mcp.md)
|
||||
//! 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
|
||||
//! solidcraft-cli icon --out F.png|F.ico [--size N] the app icon (packaging; drawn, not a file)
|
||||
//!
|
||||
@@ -32,11 +30,9 @@ mod bench_agent;
|
||||
mod bench_assistant;
|
||||
mod crashes;
|
||||
mod fuzz;
|
||||
mod kernel_cases;
|
||||
mod logger;
|
||||
mod oracle;
|
||||
mod preview;
|
||||
mod profiling;
|
||||
mod recipe;
|
||||
mod render;
|
||||
mod spatial;
|
||||
@@ -59,19 +55,19 @@ struct Command {
|
||||
}
|
||||
|
||||
/// `run`'s lines in the usage.
|
||||
const USAGE_RUN: &str = " solidcraft-cli run <script.json|design.solidcraft|part.step> [--in design.solidcraft|part.step] [--out FILE]... [--save FILE] [--quiet] [--results] [--timings] [--profile] [--profile-out trace.json] [--json-out FILE] [--allow-errors] [--validation each_step|each_feature|off]\n solidcraft-cli run --in <design.solidcraft|part.step> [--out FILE]...\n (a timeline feature that fails fails the run and nothing is written; --allow-errors writes anyway)\n (--validation: when SMLib's full check runs on the features computed (diagnostics.validation), default each_feature)\n (--timings prints {timings: [{index, command, ms}], total_ms} for a script, with --results its results too;\n --profile adds each command's profile: {boolean_ms, tessellate_ms, other_ms}, the kernel's share)\n (--profile also records where the time goes, SMLib's calls, kernel operations, features, commands and import stages,\n and prints the table to stderr: count, total, self time and longest per span; --profile-out FILE writes the spans\n as a Chrome trace for chrome://tracing or ui.perfetto.dev; SOLIDCRAFT_PROFILE=1 does the same for any command)\n";
|
||||
const USAGE_RUN: &str = " solidcraft-cli run <script.json|design.solidcraft|part.step> [--in design.solidcraft|part.step] [--out FILE]... [--save FILE] [--quiet] [--results] [--timings] [--profile] [--json-out FILE] [--allow-errors]\n solidcraft-cli run --in <design.solidcraft|part.step> [--out FILE]...\n (a timeline feature that fails fails the run and nothing is written; --allow-errors writes anyway)\n (--timings prints {timings: [{index, command, ms}], total_ms} for a script, with --results its results too;\n --profile adds each command's profile: {boolean_ms, tessellate_ms, other_ms}, the kernel's share)\n";
|
||||
/// `eval`'s lines in the usage.
|
||||
const USAGE_EVAL: &str = " solidcraft-cli eval <script.json|design.solidcraft|part.step> [--in design.solidcraft|part.step] [--json-out FILE] [--profile] [--profile-out trace.json] (alias: inspect)\n solidcraft-cli eval --in <script.json|design.solidcraft|part.step> [--json-out FILE]\n (with both inputs, runs the positional script on --in before measuring; does not automatically save the modified design)\n (--json-out FILE writes the JSON that run (with --results, else the measurement) and eval print to FILE instead of stdout,\n as plain UTF-8: a shell's > can re-encode it, e.g. Windows PowerShell adds a byte-order mark)\n (--profile and --profile-out as for run)\n";
|
||||
const USAGE_EVAL: &str = " solidcraft-cli eval <script.json|design.solidcraft|part.step> [--json-out FILE] (alias: inspect; or --in FILE)\n (--json-out FILE writes the JSON that run (with --results, else the measurement) and eval print to FILE instead of stdout,\n as plain UTF-8: a shell's > can re-encode it, e.g. Windows PowerShell adds a byte-order mark)\n";
|
||||
/// `script`'s lines in the usage.
|
||||
const USAGE_SCRIPT: &str = " solidcraft-cli script <design.solidcraft|script.json> [--out script.json]\n (the design's history as a command script that eval and run replay)\n";
|
||||
/// `bench-open`'s lines in the usage.
|
||||
const USAGE_BENCH_OPEN: &str = " solidcraft-cli bench-open <design.solidcraft | part.step> [--parse-only] [--save OUT.solidcraft] [--display LEVEL [--edit NAME EXPR]] [--ops script.json] [--profile] [--profile-out trace.json]\n (--profile and --profile-out as for run)\n (--ops: after opening, run each command of the script and undo it, timing the command, the\n evaluations it caused and the view it leaves: placing and meshing what changed)\n";
|
||||
const USAGE_BENCH_OPEN: &str = " solidcraft-cli bench-open <design.solidcraft | part.step> [--parse-only] [--save OUT.solidcraft] [--display LEVEL [--edit NAME EXPR]] [--ops script.json]\n (--ops: after opening, run each command of the script and undo it, timing the command, the\n evaluations it caused and the view it leaves: placing and meshing what changed)\n";
|
||||
/// `bench-agent`'s lines in the usage.
|
||||
const USAGE_BENCH_AGENT: &str = " solidcraft-cli bench-agent [--scale F] [--save design.solidcraft] [--script-out script.json]\n";
|
||||
/// `bench-assistant`'s lines in the usage.
|
||||
const USAGE_BENCH_ASSISTANT: &str = " solidcraft-cli bench-assistant --connect 127.0.0.1:PORT [--token T] --base-url URL --model NAME [--tasks 1,2,5] [--repeat N] [--timeout-s 300] [--json out.json] [--markdown out.md]\n";
|
||||
/// `snapshot`'s lines in the usage.
|
||||
const USAGE_SNAPSHOT: &str = " solidcraft-cli snapshot <script|design> [--in design.solidcraft|part.step] --out shot.png [--width W] [--height H] [--view NAME | --camera JSON] [--no-grid] [--no-axes]\n [--body NAME:opacity=0.3,color=#rrggbb]... [--tool NAME]... [--section z=27] [--quality coarse|normal|fine|ultra]\n solidcraft-cli snapshot --in <design.solidcraft|part.step> --out shot.png ...\n (as with run, --in starts from a design and the script runs on top of it; an option snapshot doesn't take is an error;\n --view: iso, front, back, top, bottom, left, right, or the name of a view saved in the design with view.save;\n --camera: {target: [x,y,z], yaw, pitch, distance, fov?} in mm and radians, as the MCP screenshot takes it (docs/mcp.md);\n --no-grid and --no-axes leave the ground grid (on Z 0) and the origin's axes out of the picture;\n --body: how one body is drawn, as appearance.assign takes it: opacity 0-1 (below 1 is see-through, so what is inside\n it shows) and color #rrggbb, either or both, once per body;\n --tool: draw this body solid in the tool colour and every other body see-through, so a cut's or a join's tool body\n is visible inside the material it meets; a later --body overrides it;\n --section: cut the model as inspect.section does, so the inside shows: z=27 (the plane through that world\n coordinate, keeping the smaller side), add ,flip to keep the other side, or XY, XZ, YZ, or its own JSON;\n --quality: the display quality the bodies are meshed at, as the app's setting (default normal))\n";
|
||||
const USAGE_SNAPSHOT: &str = " solidcraft-cli snapshot <script|design> [--in design.solidcraft|part.step] --out shot.png [--width W] [--height H] [--view NAME | --camera JSON] [--no-grid] [--no-axes]\n [--body NAME:opacity=0.3,color=#rrggbb]... [--tool NAME]... [--section z=27]\n solidcraft-cli snapshot --in <design.solidcraft|part.step> --out shot.png ...\n (as with run, --in starts from a design and the script runs on top of it; an option snapshot doesn't take is an error;\n --view: iso, front, back, top, bottom, left, right, or the name of a view saved in the design with view.save;\n --camera: {target: [x,y,z], yaw, pitch, distance, fov?} in mm and radians, as the MCP screenshot takes it (docs/mcp.md);\n --no-grid and --no-axes leave the ground grid (on Z 0) and the origin's axes out of the picture;\n --body: how one body is drawn, as appearance.assign takes it: opacity 0-1 (below 1 is see-through, so what is inside\n it shows) and color #rrggbb, either or both, once per body;\n --tool: draw this body solid in the tool colour and every other body see-through, so a cut's or a join's tool body\n is visible inside the material it meets; a later --body overrides it;\n --section: cut the model as inspect.section does, so the inside shows: z=27 (the plane through that world\n coordinate, keeping the smaller side), add ,flip to keep the other side, or XY, XZ, YZ, or its own JSON)\n";
|
||||
/// `render`'s lines in the usage.
|
||||
const USAGE_RENDER: &str = " solidcraft-cli render <script|design> --out image.png [--setup NAME] [--width W] [--height H] [--transparent] [--overwrite] [--timeout SECONDS]\n [--progressive] [--samples N] [--max-depth N] [--seed N] [--exposure EV] [--lighting FILE.json] [--environment FILE.json] [--tone-map reinhard|aces_fitted]\n";
|
||||
/// `exec`'s lines in the usage.
|
||||
@@ -82,13 +78,11 @@ const USAGE_COMMANDS: &str = " solidcraft-cli commands\n";
|
||||
const USAGE_RECIPE: &str = " solidcraft-cli recipe <recipe.json>\n";
|
||||
/// `oracle`'s lines in the usage.
|
||||
const USAGE_ORACLE: &str = " solidcraft-cli oracle <case-dir>... [--json]\n";
|
||||
/// `kernel-cases`'s lines in the usage.
|
||||
const USAGE_KERNEL_CASES: &str = " solidcraft-cli kernel-cases <case-dir>... [--json] [--dumps=DIR] [--validation=each_step|each_feature|off]\n (the kernel regression rig, tests/kernel-cases, on SMLib; --dumps: each case's\n failing SMLib calls leave their inputs for replay in DIR/<area>-<case>; --validation: when SMLib's\n full check runs (diagnostics.validation), default each_feature)\n";
|
||||
/// `fuzz`'s lines in the usage.
|
||||
const USAGE_FUZZ: &str =
|
||||
" solidcraft-cli fuzz [--from N] [--count N] [--jobs J] [--timeout S] [--steps N] [--minimise] [--out DIR] [--report FILE]\n";
|
||||
/// `mcp`'s lines in the usage.
|
||||
const USAGE_MCP: &str = " solidcraft-cli mcp [--headless] [--in design.solidcraft|script.json] [--connect 127.0.0.1:PORT [--token T]] [--allow-path DIR]... [--allow-any-path]\n [--param-check off|warn|strict] [--render-gpu]\n solidcraft-cli mcp setup [--dry-run] [--client opencode|claude-desktop|claude-code|cursor] [--live]\n (adds a `solidcraft` server, with this program's path, to each installed AI client; --live adds `solidcraft-live` for the running app; alias: mcp install)\n";
|
||||
const USAGE_MCP: &str = " solidcraft-cli mcp [--headless] [--in design.solidcraft|script.json] [--connect 127.0.0.1:PORT [--token T]] [--allow-path DIR]... [--allow-any-path]\n [--param-check off|warn|strict]\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.
|
||||
@@ -121,7 +115,6 @@ const COMMANDS: &[Command] = &[
|
||||
Command { name: "commands", usage: USAGE_COMMANDS, listed: true },
|
||||
Command { name: "recipe", usage: USAGE_RECIPE, listed: true },
|
||||
Command { name: "oracle", usage: USAGE_ORACLE, listed: true },
|
||||
Command { name: "kernel-cases", usage: USAGE_KERNEL_CASES, listed: true },
|
||||
Command { name: "fuzz", usage: USAGE_FUZZ, listed: true },
|
||||
Command { name: "fuzz-one", usage: USAGE_FUZZ_ONE, listed: false },
|
||||
Command { name: "mcp", usage: USAGE_MCP, listed: true },
|
||||
@@ -137,19 +130,6 @@ const COMMANDS: &[Command] = &[
|
||||
fn main() -> ExitCode {
|
||||
let mut args: Vec<String> = std::env::args().skip(1).collect();
|
||||
logger::init(&mut args);
|
||||
// `--profile` / `--profile-out` (run, eval, bench-open) or SOLIDCRAFT_PROFILE (any command):
|
||||
// record from here, and print the table when the command ends.
|
||||
let profiling = match args.first().map(String::as_str) {
|
||||
Some("run" | "eval" | "inspect" | "bench-open") => profiling::start(&args[1..]),
|
||||
_ => profiling::start(&[]),
|
||||
};
|
||||
let profiling = match profiling {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("error: {e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
let r = match args.first().map(String::as_str) {
|
||||
// Asked-for help is not an error: the command's usage on stdout, exit 0. It is read
|
||||
// before the command runs, so `--help` never starts a run (a fuzz run, a server).
|
||||
@@ -183,7 +163,6 @@ fn main() -> ExitCode {
|
||||
recipe::to_script(&v).map(|s| println!("{}", pretty(&s)))
|
||||
}),
|
||||
Some("oracle") => oracle::run(&args[1..]),
|
||||
Some("kernel-cases") => kernel_cases::run(&args[1..]),
|
||||
Some("fuzz") => fuzz::run(&args[1..]),
|
||||
Some("fuzz-one") => fuzz::one(&args[1..]),
|
||||
Some("mcp") => cmd_mcp(&args[1..]),
|
||||
@@ -216,11 +195,6 @@ fn main() -> ExitCode {
|
||||
}
|
||||
_ => Err(usage()),
|
||||
};
|
||||
// The table also when the command failed: where it spent its time before failing.
|
||||
let r = match (r, profiling.map(profiling::Profiling::finish)) {
|
||||
(Ok(()), Some(Err(e))) => Err(e),
|
||||
(r, _) => r,
|
||||
};
|
||||
match r {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
Err(e) => {
|
||||
@@ -278,15 +252,8 @@ pub fn load(path: &str) -> Result<Session, String> {
|
||||
/// `load`, with scripts and recipes run at the parameter-check level `check`, which the
|
||||
/// session keeps.
|
||||
fn load_checked(path: &str, check: solidcraft_engine::schema::ParamCheck) -> Result<Session, String> {
|
||||
load_with(path, check, solidcraft_engine::kernel::Validation::default())
|
||||
}
|
||||
|
||||
/// `load_checked`, the features computed under `validation` (`diagnostics.validation`), which
|
||||
/// the session keeps.
|
||||
fn load_with(path: &str, check: solidcraft_engine::schema::ParamCheck, validation: solidcraft_engine::kernel::Validation) -> Result<Session, String> {
|
||||
let mut s = Session::default();
|
||||
s.param_check = check;
|
||||
s.set_validation(validation);
|
||||
if solidcraft_engine::io::is_step_path(path) || solidcraft_engine::io::is_mesh_path(path) {
|
||||
s.execute("doc.open", &json!({"path": path})).map_err(|e| e.to_string())?;
|
||||
return Ok(s);
|
||||
@@ -294,7 +261,8 @@ fn load_with(path: &str, check: solidcraft_engine::schema::ParamCheck, validatio
|
||||
let bytes = solidcraft_engine::io::vfs::read(path, solidcraft_engine::io::MAX_DESIGN_BYTES).map_err(|e| format!("{path}: {e}"))?;
|
||||
if solidcraft_engine::io::is_design_container(&bytes) {
|
||||
let doc = solidcraft_engine::io::read_design(&bytes).map_err(|e| format!("{path}: {e}"))?;
|
||||
let mut session = Session::opened_at_validating(doc, None, Some(path), validation);
|
||||
let mut session = Session::new(doc);
|
||||
session.path = Some(path.to_owned());
|
||||
session.param_check = check;
|
||||
return Ok(session);
|
||||
}
|
||||
@@ -340,16 +308,6 @@ fn positional(args: &[String]) -> Option<&String> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Is `path` a command script (or a recipe), not a design: JSON that is no saved design.
|
||||
fn is_script(path: &str) -> bool {
|
||||
let Ok(bytes) = solidcraft_engine::io::vfs::read(path, solidcraft_engine::io::MAX_DESIGN_BYTES) else { return false };
|
||||
if solidcraft_engine::io::is_design_container(&bytes) {
|
||||
return false;
|
||||
}
|
||||
serde_json::from_slice::<Value>(&bytes)
|
||||
.is_ok_and(|v| !v.get("format").and_then(Value::as_str).is_some_and(|f| solidcraft_engine::doc::format::version_of(f).is_some()))
|
||||
}
|
||||
|
||||
/// `--json-out FILE`: where the JSON a command prints goes instead of stdout.
|
||||
fn json_out(args: &[String]) -> Result<Option<&str>, String> {
|
||||
if !args.iter().any(|a| a == "--json-out") {
|
||||
@@ -381,34 +339,20 @@ fn print_json(v: &Value, file: Option<&str>) -> Result<(), String> {
|
||||
fn cmd_run(args: &[String]) -> Result<(), String> {
|
||||
install_renderer();
|
||||
let json_out = json_out(args)?;
|
||||
known_args(
|
||||
"run",
|
||||
args,
|
||||
&["--in", "--out", "--save", "--json-out", "--profile-out", "--validation"],
|
||||
&["--quiet", "--results", "--timings", "--profile", "--allow-errors"],
|
||||
true,
|
||||
)?;
|
||||
let validation = validation_flag("run", args)?;
|
||||
let base = flag(args, "--in");
|
||||
let path = positional(args);
|
||||
if path.is_none() && base.is_none() {
|
||||
return Err(usage());
|
||||
}
|
||||
let want_results = args.iter().any(|a| a == "--results");
|
||||
// `--profile` records the profiler's spans (the table goes to stderr, `profiling.rs`); for a
|
||||
// script it also times each command and splits out its kernel time, implying `--timings`.
|
||||
let script = path.is_some_and(|p| is_script(p));
|
||||
let profile = script && args.iter().any(|a| a == "--profile" || a == "--profile-out");
|
||||
// `--profile` times each command and splits out its kernel time; it implies `--timings`.
|
||||
let profile = args.iter().any(|a| a == "--profile");
|
||||
let timings = profile || args.iter().any(|a| a == "--timings");
|
||||
let (mut s, results) = if want_results || timings {
|
||||
// Collect every command's result or time (scripts only), then process requested files too.
|
||||
let mut s = match base {
|
||||
Some(b) => load_with(b, solidcraft_engine::schema::ParamCheck::Off, validation)?,
|
||||
None => {
|
||||
let mut s = Session::default();
|
||||
s.set_validation(validation);
|
||||
s
|
||||
}
|
||||
Some(b) => load(b)?,
|
||||
None => Session::default(),
|
||||
};
|
||||
let needs = if want_results { "--results needs a script" } else { "--timings needs a script" };
|
||||
let v = read_json(path.ok_or(needs)?)?;
|
||||
@@ -423,7 +367,7 @@ fn cmd_run(args: &[String]) -> Result<(), String> {
|
||||
};
|
||||
(s, Some(out))
|
||||
} else {
|
||||
(open_validating(base, path, validation)?, None)
|
||||
(open(base, path)?, None)
|
||||
};
|
||||
// A feature that fails leaves the bodies as they were before it (a parameter change can
|
||||
// break a feature without failing the command that made it): writing them would hand out
|
||||
@@ -447,15 +391,9 @@ fn cmd_run(args: &[String]) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The session `run`, `eval` and `snapshot` start from: `--in` (a design or STEP file) with the
|
||||
/// The session `run` and `snapshot` start from: `--in` (a design or STEP file) with the
|
||||
/// positional script run on top of it, or either one alone.
|
||||
fn open(base: Option<&str>, path: Option<&String>) -> Result<Session, String> {
|
||||
open_validating(base, path, solidcraft_engine::kernel::Validation::default())
|
||||
}
|
||||
|
||||
/// [`open`], the features computed under `validation`.
|
||||
fn open_validating(base: Option<&str>, path: Option<&String>, validation: solidcraft_engine::kernel::Validation) -> Result<Session, String> {
|
||||
let load = |p: &str| load_with(p, solidcraft_engine::schema::ParamCheck::Off, validation);
|
||||
match (base, path) {
|
||||
(Some(b), Some(p)) => {
|
||||
let mut s = load(b)?;
|
||||
@@ -468,15 +406,6 @@ fn open_validating(base: Option<&str>, path: Option<&String>, validation: solidc
|
||||
}
|
||||
}
|
||||
|
||||
/// `--validation MODE` (`diagnostics.validation`): the default, each_feature, when not given.
|
||||
fn validation_flag(cmd: &str, args: &[String]) -> Result<solidcraft_engine::kernel::Validation, String> {
|
||||
match flag(args, "--validation") {
|
||||
Some(m) => solidcraft_engine::kernel::Validation::parse(m)
|
||||
.ok_or_else(|| format!("{cmd}: --validation must be each_step, each_feature or off, not `{m}`")),
|
||||
None => Ok(solidcraft_engine::kernel::Validation::default()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Refuses an option `cmd` doesn't take, and one missing its value: a typo, or another
|
||||
/// command's option, would otherwise be ignored without a word. `positionals` says whether the
|
||||
/// command takes a word of its own (a file, a seed): `fuzz` takes none, and would start a run.
|
||||
@@ -514,8 +443,8 @@ fn timeline_ok(s: &Session) -> Result<(), String> {
|
||||
|
||||
fn cmd_eval(args: &[String]) -> Result<(), String> {
|
||||
let json_out = json_out(args)?;
|
||||
known_args("eval", args, &["--in", "--json-out", "--profile-out"], &["--profile"], true)?;
|
||||
let mut s = open(flag(args, "--in"), positional(args))?;
|
||||
let path = flag(args, "--in").or_else(|| positional(args).map(String::as_str)).ok_or_else(usage)?;
|
||||
let mut s = load(path)?;
|
||||
let m = s.execute("inspect.measure", &json!({})).map_err(|e| e.to_string())?;
|
||||
let d = s.execute("document.inspect", &json!({})).map_err(|e| e.to_string())?;
|
||||
let out = json!({
|
||||
@@ -596,7 +525,7 @@ fn cmd_snapshot(args: &[String]) -> Result<(), String> {
|
||||
known_args(
|
||||
"snapshot",
|
||||
args,
|
||||
&["--in", "--out", "--width", "--height", "--view", "--camera", "--body", "--tool", "--section", "--quality"],
|
||||
&["--in", "--out", "--width", "--height", "--view", "--camera", "--body", "--tool", "--section"],
|
||||
&["--no-grid", "--no-axes"],
|
||||
true,
|
||||
)?;
|
||||
@@ -609,16 +538,6 @@ fn cmd_snapshot(args: &[String]) -> Result<(), String> {
|
||||
Some(x) => x.parse().ok().filter(|n| (16..=8192).contains(n)).ok_or_else(|| format!("{name} must be 16 to 8192 pixels, not `{x}`")),
|
||||
};
|
||||
let (w, h) = (size("--width", 1280)?, size("--height", 800)?);
|
||||
// The display quality the bodies are meshed at, as the app's Display Quality setting: set
|
||||
// before the design opens, so every display plan is made at it.
|
||||
if let Some(q) = flag(args, "--quality") {
|
||||
use solidcraft_engine::doc::display_quality::{DisplayQuality, set_current};
|
||||
let level = DisplayQuality::parse(q).ok_or_else(|| {
|
||||
let ids: Vec<&str> = DisplayQuality::ALL.iter().map(|q| q.id()).collect();
|
||||
format!("--quality must be one of {}, not `{q}`", ids.join(", "))
|
||||
})?;
|
||||
set_current(level);
|
||||
}
|
||||
let mut s = open(flag(args, "--in"), positional(args))?;
|
||||
// How the bodies are drawn and where the model is cut, through the commands the app's
|
||||
// Appearance dialog and Section Analysis run (`preview`).
|
||||
@@ -638,12 +557,9 @@ fn cmd_snapshot(args: &[String]) -> Result<(), String> {
|
||||
}
|
||||
(None, None) => {}
|
||||
}
|
||||
let cam = match s.camera {
|
||||
Some(cam) => cam,
|
||||
None => solidcraft_engine::view::home_camera(&s).map_err(|e| e.to_string())?,
|
||||
};
|
||||
let cam = s.camera.unwrap_or_else(|| solidcraft_engine::view::home_camera(&s));
|
||||
let guides = Guides { grid: !args.iter().any(|a| a == "--no-grid"), axes: !args.iter().any(|a| a == "--no-axes") };
|
||||
let scene = solidcraft_engine::view::scene_with_guides(&s, &cam, s.section, guides).map_err(|e| e.to_string())?;
|
||||
let scene = solidcraft_engine::view::scene_with_guides(&s, &cam, s.section, guides);
|
||||
let png = solidcraft_engine::capture::png(&scene, &cam, w, h)?;
|
||||
std::fs::write(out, &png.bytes).map_err(|e| format!("{out}: {e}"))?;
|
||||
let mut reply = json!({"path": out, "width": w, "height": h, "bytes": png.bytes.len(), "renderer": png.renderer});
|
||||
@@ -709,27 +625,10 @@ fn cmd_exec(args: &[String]) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `mcp setup`: add the server to each installed AI client, with this program's own path.
|
||||
fn cmd_mcp_setup(args: &[String]) -> Result<(), String> {
|
||||
use solidcraft_engine::agent_setup as setup;
|
||||
let (lines, ok) = setup::setup(args, &setup::Env::from_process(), &setup::current_exe()?)?;
|
||||
for l in lines {
|
||||
println!("{l}");
|
||||
}
|
||||
if ok { Ok(()) } else { Err("mcp setup: some clients could not be configured".into()) }
|
||||
}
|
||||
|
||||
/// MCP server on stdio: headless (optionally starting from a design or script) or bridged to
|
||||
/// a running app's control port. stdout carries only protocol messages. Files the agent's tool
|
||||
/// calls reach are confined to `solidcraft_mcp::agent_scope` unless `--allow-any-path` is given.
|
||||
fn cmd_mcp(args: &[String]) -> Result<(), String> {
|
||||
if matches!(args.first().map(String::as_str), Some("setup" | "install")) {
|
||||
return cmd_mcp_setup(&args[1..]);
|
||||
}
|
||||
let render_gpu = args.iter().any(|arg| arg == "--render-gpu");
|
||||
if render_gpu && flag(args, "--connect").is_some() {
|
||||
return Err("mcp: --render-gpu installs a headless image host; a connected application owns its own device".into());
|
||||
}
|
||||
install_renderer();
|
||||
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)));
|
||||
@@ -773,30 +672,19 @@ fn cmd_mcp(args: &[String]) -> Result<(), String> {
|
||||
}
|
||||
Box::new(remote)
|
||||
}
|
||||
(None, Some(path)) => {
|
||||
let mut headless = solidcraft_mcp::Headless::new(load_checked(path, param_check)?);
|
||||
if render_gpu {
|
||||
headless.session.register_render_job_host(solidcraft_gpu::path::host().map_err(|e| e.to_string())?).map_err(|e| e.to_string())?;
|
||||
}
|
||||
Box::new(headless)
|
||||
}
|
||||
(None, Some(path)) => Box::new(solidcraft_mcp::Headless::new(load_checked(path, param_check)?)),
|
||||
// No arguments: the running desktop app, found through the discovery file it wrote
|
||||
// (docs/control-protocol.md); none running (or `--headless`): a session of its own.
|
||||
(None, None) => {
|
||||
let auto = !args.iter().any(|a| a == "--headless");
|
||||
if auto && let Some(port) = discover_app() {
|
||||
if !args.iter().any(|a| a == "--headless")
|
||||
&& 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)");
|
||||
return serve_mcp(Box::new(solidcraft_mcp::Remote::new(&format!("127.0.0.1:{port}"), flag(args, "--token"))), scope);
|
||||
}
|
||||
let mut headless = solidcraft_mcp::Headless::default();
|
||||
headless.session.param_check = param_check;
|
||||
if render_gpu {
|
||||
headless.session.register_render_job_host(solidcraft_gpu::path::host().map_err(|e| e.to_string())?).map_err(|e| e.to_string())?;
|
||||
}
|
||||
if auto {
|
||||
// The app may start after the agent: look for it again until there is work here.
|
||||
Box::new(solidcraft_mcp::Auto::new(headless, Box::new(discover_app), flag(args, "--token")))
|
||||
} else {
|
||||
Box::new(headless)
|
||||
}
|
||||
Box::new(headless)
|
||||
}
|
||||
};
|
||||
serve_mcp(backend, scope)
|
||||
@@ -880,7 +768,7 @@ fn bench_ops(mut s: Session, script: &str) -> Result<Session, String> {
|
||||
let r = s.execute(id, p).map_err(|e| format!("{id}: {e}"))?;
|
||||
let ms = t.elapsed().as_secs_f64() * 1000.0;
|
||||
let t = std::time::Instant::now();
|
||||
s.world_state().map_err(|e| e.to_string())?.warm_meshes(|| true);
|
||||
s.world_state().warm_meshes(|| true);
|
||||
let view_ms = t.elapsed().as_secs_f64() * 1000.0;
|
||||
println!("{}", json!({"step": "op", "command": id, "ms": ms, "view_ms": view_ms}));
|
||||
Ok(r)
|
||||
@@ -926,7 +814,7 @@ pub(crate) fn cmd_bench_open(args: &[String]) -> Result<(), String> {
|
||||
return Ok(());
|
||||
}
|
||||
let t = std::time::Instant::now();
|
||||
let s = Session::opened_at(doc, saved.as_ref(), (!step).then_some(path.as_str()));
|
||||
let s = Session::opened(doc, saved.as_ref());
|
||||
drop(read);
|
||||
// Taken from the file: nothing evaluated (SOLIDCRAFT_NO_MODEL_CACHE evaluates anyway), or
|
||||
// only the features whose state a thinned file left out.
|
||||
@@ -936,7 +824,7 @@ pub(crate) fn cmd_bench_open(args: &[String]) -> Result<(), String> {
|
||||
println!("{}", json!({"step": "feature", "id": r.id, "name": r.name, "ms": r.ms, "error": r.error}));
|
||||
}
|
||||
let t = std::time::Instant::now();
|
||||
let st = s.world_state().map_err(|e| e.to_string())?;
|
||||
let st = s.world_state();
|
||||
let place_ms = t.elapsed().as_secs_f64() * 1000.0;
|
||||
// As the desktop app prepares a design it opened.
|
||||
st.warm_meshes(|| true);
|
||||
@@ -1006,7 +894,7 @@ pub(crate) fn cmd_bench_open(args: &[String]) -> Result<(), String> {
|
||||
let t = std::time::Instant::now();
|
||||
s.execute("parameters.change", &json!({"name": name, "expression": expr})).map_err(|e| e.to_string())?;
|
||||
let eval_ms = t.elapsed().as_secs_f64() * 1000.0;
|
||||
let st = s.world_state().map_err(|e| e.to_string())?;
|
||||
let st = s.world_state();
|
||||
let t = std::time::Instant::now();
|
||||
st.warm_meshes(|| true);
|
||||
let mesh_ms = t.elapsed().as_secs_f64() * 1000.0;
|
||||
|
||||
@@ -13,28 +13,28 @@ use crate::{read_json, recipe};
|
||||
/// Default relative tolerance for volume and area (our curved-face tessellation is within ~2e-4).
|
||||
pub const REL_TOL: f64 = 1e-3;
|
||||
/// Default tolerance for centre of mass and bounding box, as a fraction of the bbox diagonal.
|
||||
pub(crate) const ABS_FRAC: f64 = 1e-3;
|
||||
const ABS_FRAC: f64 = 1e-3;
|
||||
/// Chord tolerance used to triangulate our part for the surface-distance check (mm).
|
||||
const MESH_TOL: f64 = 0.01;
|
||||
/// Relative tolerance on the principal second moments of the two triangulations.
|
||||
const INERTIA_REL: f64 = 2e-3;
|
||||
|
||||
/// What SolidCraft built: totals over every body.
|
||||
pub(crate) struct Got {
|
||||
pub(crate) body_count: usize,
|
||||
pub(crate) volume: f64,
|
||||
pub(crate) area: f64,
|
||||
pub(crate) com: [f64; 3],
|
||||
pub(crate) bbox: ([f64; 3], [f64; 3]),
|
||||
pub(crate) faces: u64,
|
||||
struct Got {
|
||||
body_count: usize,
|
||||
volume: f64,
|
||||
area: f64,
|
||||
com: [f64; 3],
|
||||
bbox: ([f64; 3], [f64; 3]),
|
||||
faces: u64,
|
||||
}
|
||||
|
||||
pub(crate) fn v3(v: &Value) -> Option<[f64; 3]> {
|
||||
fn v3(v: &Value) -> Option<[f64; 3]> {
|
||||
let a = v.as_array()?;
|
||||
Some([a.first()?.as_f64()?, a.get(1)?.as_f64()?, a.get(2)?.as_f64()?])
|
||||
}
|
||||
|
||||
pub(crate) fn totals(m: &Value) -> Got {
|
||||
fn totals(m: &Value) -> Got {
|
||||
let bodies = m["bodies"].as_array().cloned().unwrap_or_default();
|
||||
let (mut volume, mut area, mut faces) = (0.0, 0.0, 0);
|
||||
let mut moment = [0.0; 3];
|
||||
@@ -57,21 +57,21 @@ pub(crate) fn totals(m: &Value) -> Got {
|
||||
Got { body_count: bodies.len(), volume, area, com, bbox: (lo, hi), faces }
|
||||
}
|
||||
|
||||
pub(crate) struct Checks {
|
||||
pub(crate) list: Vec<Value>,
|
||||
pub(crate) pass: bool,
|
||||
struct Checks {
|
||||
list: Vec<Value>,
|
||||
pass: bool,
|
||||
}
|
||||
|
||||
impl Checks {
|
||||
pub(crate) fn push(&mut self, what: &str, got: Value, want: Value, ok: bool) {
|
||||
fn push(&mut self, what: &str, got: Value, want: Value, ok: bool) {
|
||||
self.pass &= ok;
|
||||
self.list.push(json!({"check": what, "got": got, "want": want, "ok": ok}));
|
||||
}
|
||||
pub(crate) fn rel(&mut self, what: &str, g: f64, w: Option<f64>, rel: f64) {
|
||||
fn rel(&mut self, what: &str, g: f64, w: Option<f64>, rel: f64) {
|
||||
let ok = w.is_some_and(|w| g.is_finite() && (g - w).abs() <= rel * w.abs().max(1.0));
|
||||
self.push(what, json!(g), json!(w), ok);
|
||||
}
|
||||
pub(crate) fn vec(&mut self, what: &str, g: [f64; 3], w: Option<[f64; 3]>, tol: f64) {
|
||||
fn vec(&mut self, what: &str, g: [f64; 3], w: Option<[f64; 3]>, tol: f64) {
|
||||
let ok = w.is_some_and(|w| g.iter().zip(w).all(|(g, w)| g.is_finite() && (g - w).abs() <= tol));
|
||||
self.push(what, json!(g), json!(w), ok);
|
||||
}
|
||||
@@ -186,8 +186,8 @@ fn step_export_check(s: &solidcraft_engine::Session, volume: f64) -> (bool, Stri
|
||||
/// Our part as a triangle soup (curved faces refined onto the surface).
|
||||
fn our_mesh(s: &solidcraft_engine::Session) -> Result<Vec<Tri>, String> {
|
||||
let mut ours: Vec<Tri> = Vec::new();
|
||||
for b in &s.world_state().map_err(|e| e.to_string())?.bodies {
|
||||
let m = b.body.mesh(&solidcraft_engine::kernel::MeshTolerance::chord(MESH_TOL)).map_err(|e| e.to_string())?;
|
||||
for b in &s.world_state().bodies {
|
||||
let m = b.body.tessellate_with(MESH_TOL, true).map_err(|e| e.to_string())?;
|
||||
let p = |i: u32| m.positions.get(i as usize).map(|v: &Vec3| [v.x, v.y, v.z]);
|
||||
for t in &m.triangles {
|
||||
if let (Some(a), Some(b), Some(c)) = (p(t[0]), p(t[1]), p(t[2])) {
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
//! `--profile` and `--profile-out FILE` for `run`, `eval` and `bench-open` (and
|
||||
//! `SOLIDCRAFT_PROFILE=1` for any command): record where the time goes, SMLib's calls to the
|
||||
//! commands, print the table to stderr when the command ends (stdout stays the command's JSON)
|
||||
//! and write the spans as a Chrome trace for chrome://tracing or Perfetto.
|
||||
|
||||
use solidcraft_engine::profile;
|
||||
|
||||
/// Rows the table prints.
|
||||
const TOP: usize = 30;
|
||||
|
||||
/// A recording that prints its table when finished.
|
||||
pub struct Profiling {
|
||||
trace: Option<String>,
|
||||
}
|
||||
|
||||
/// Start recording when `args` (after the command) ask for it, or `SOLIDCRAFT_PROFILE` does.
|
||||
pub fn start(args: &[String]) -> Result<Option<Profiling>, String> {
|
||||
let trace = match args.iter().position(|a| a == "--profile-out") {
|
||||
Some(i) => match args.get(i + 1) {
|
||||
Some(f) if !f.starts_with("--") => Some(f.clone()),
|
||||
_ => return Err("--profile-out needs a FILE".into()),
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
let asked = trace.is_some() || args.iter().any(|a| a == "--profile");
|
||||
if !asked && !solidcraft_engine::timing::profile_from_env() {
|
||||
return Ok(None);
|
||||
}
|
||||
if !profile::COMPILED {
|
||||
return Err("this solidcraft-cli was built without the profiler (its `profile` feature)".into());
|
||||
}
|
||||
profile::reset();
|
||||
profile::set_enabled(true);
|
||||
Ok(Some(Profiling { trace }))
|
||||
}
|
||||
|
||||
impl Profiling {
|
||||
/// Stop recording, print the table on stderr and write the trace.
|
||||
pub fn finish(self) -> Result<(), String> {
|
||||
profile::set_enabled(false);
|
||||
eprint!("{}", profile::report().table(TOP));
|
||||
if let Some(path) = self.trace {
|
||||
let mut bytes = Vec::new();
|
||||
profile::write_chrome_trace(&mut bytes).map_err(|e| format!("{path}: {e}"))?;
|
||||
std::fs::write(&path, bytes).map_err(|e| format!("{path}: {e}"))?;
|
||||
eprintln!("profile: trace written to {path} (open it in chrome://tracing or ui.perfetto.dev)");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,86 +1,32 @@
|
||||
//! Complete headless image-job workflow through the same engine commands as the UI/MCP.
|
||||
use serde_json::{Value, json};
|
||||
use serde_json::json;
|
||||
use solidcraft_engine::{
|
||||
Session,
|
||||
doc::render_settings::{RenderJob, RenderSetup},
|
||||
render::job::{CpuHost, JobId, Phase, ToneMap},
|
||||
doc::render_settings::RenderSetup,
|
||||
render::job::{CpuHost, JobId, Phase},
|
||||
};
|
||||
|
||||
/// Only explicit runtime CLI flags are stored here. A typed setup owns these
|
||||
/// values even when a supplied flag happens to match the saved value.
|
||||
#[derive(Default)]
|
||||
struct RuntimeOptions {
|
||||
progressive: bool,
|
||||
transparent: bool,
|
||||
samples: Option<u32>,
|
||||
max_depth: Option<u32>,
|
||||
seed: Option<u32>,
|
||||
lighting: Option<String>,
|
||||
tone_map: Option<ToneMap>,
|
||||
environment: Option<String>,
|
||||
}
|
||||
|
||||
impl RuntimeOptions {
|
||||
fn explicit_flag(&self) -> Option<&'static str> {
|
||||
[
|
||||
(self.progressive, "--progressive"),
|
||||
(self.transparent, "--transparent"),
|
||||
(self.samples.is_some(), "--samples"),
|
||||
(self.max_depth.is_some(), "--max-depth"),
|
||||
(self.seed.is_some(), "--seed"),
|
||||
(self.lighting.is_some(), "--lighting"),
|
||||
(self.tone_map.is_some(), "--tone-map"),
|
||||
(self.environment.is_some(), "--environment"),
|
||||
]
|
||||
.into_iter()
|
||||
.find_map(|(present, flag)| present.then_some(flag))
|
||||
}
|
||||
|
||||
fn request(&self, session: &Session, settings: RenderSetup) -> Result<(Value, bool), String> {
|
||||
if let Some(job) = &settings.job {
|
||||
if let Some(flag) = self.explicit_flag() {
|
||||
return Err(format!("{flag} cannot accompany a typed saved render setup; change the saved job options instead"));
|
||||
}
|
||||
settings.validate().map_err(|e| e.to_string())?;
|
||||
session.doc.render.validate_resources(&settings).map_err(|e| e.to_string())?;
|
||||
let needs_gpu = matches!(job, RenderJob::GpuPath { .. });
|
||||
return Ok((json!({"settings":settings}), needs_gpu));
|
||||
}
|
||||
// Legacy setups and unnamed requests retain their runtime wire contract.
|
||||
settings.validate().map_err(|e| e.to_string())?;
|
||||
let mut params = json!({"settings":settings,"transparent_background":self.transparent,
|
||||
"progressive":self.progressive,"samples":self.samples,"max_depth":self.max_depth,"seed":self.seed});
|
||||
if let Some(path) = &self.lighting {
|
||||
let text = solidcraft_engine::io::vfs::read_to_string(path, 65536).map_err(|e| format!("could not read lighting: {e}"))?;
|
||||
let lighting: solidcraft_engine::render::job::StudioLighting = serde_json::from_str(&text).map_err(|e| e.to_string())?;
|
||||
params["lighting"] = json!(lighting);
|
||||
}
|
||||
if let Some(tone_map) = self.tone_map {
|
||||
params["tone_map"] = json!(tone_map);
|
||||
}
|
||||
if let Some(path) = &self.environment {
|
||||
let text = solidcraft_engine::io::vfs::read_to_string(path, 65536).map_err(|e| format!("could not read environment: {e}"))?;
|
||||
params["environment"] = serde_json::from_str(&text).map_err(|e| e.to_string())?;
|
||||
}
|
||||
Ok((params, self.progressive))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(args: &[String]) -> Result<(), String> {
|
||||
let input = args.first().filter(|p| !p.starts_with('-')).ok_or("render requires an input design or script")?;
|
||||
let mut out = None;
|
||||
let mut setup = None;
|
||||
let mut width = None;
|
||||
let mut height = None;
|
||||
let mut progressive = false;
|
||||
let mut samples = None;
|
||||
let mut max_depth = None;
|
||||
let mut seed = None;
|
||||
let mut exposure = None;
|
||||
let mut runtime = RuntimeOptions::default();
|
||||
let mut lighting = None;
|
||||
let mut tone_map = None;
|
||||
let mut environment = None;
|
||||
let mut transparent = false;
|
||||
let mut overwrite = false;
|
||||
let mut timeout = 120u64;
|
||||
let mut i = 1;
|
||||
while let Some(arg) = args.get(i) {
|
||||
match arg.as_str() {
|
||||
"--progressive" => runtime.progressive = true,
|
||||
"--transparent" => runtime.transparent = true,
|
||||
"--progressive" => progressive = true,
|
||||
"--transparent" => transparent = true,
|
||||
"--overwrite" => overwrite = true,
|
||||
"--out" | "--setup" | "--width" | "--height" | "--timeout" | "--samples" | "--max-depth" | "--seed" | "--exposure" | "--lighting"
|
||||
| "--tone-map" | "--environment" => {
|
||||
@@ -91,17 +37,23 @@ pub fn run(args: &[String]) -> Result<(), String> {
|
||||
"--setup" => setup = Some(value.clone()),
|
||||
"--width" => width = Some(value.parse::<u32>().map_err(|_| "invalid width")?),
|
||||
"--height" => height = Some(value.parse::<u32>().map_err(|_| "invalid height")?),
|
||||
"--samples" => runtime.samples = Some(value.parse::<u32>().map_err(|_| "invalid samples")?),
|
||||
"--max-depth" => runtime.max_depth = Some(value.parse::<u32>().map_err(|_| "invalid depth")?),
|
||||
"--seed" => runtime.seed = Some(value.parse::<u32>().map_err(|_| "invalid seed")?),
|
||||
"--samples" => samples = Some(value.parse::<u32>().map_err(|_| "invalid samples")?),
|
||||
"--max-depth" => max_depth = Some(value.parse::<u32>().map_err(|_| "invalid depth")?),
|
||||
"--seed" => seed = Some(value.parse::<u32>().map_err(|_| "invalid seed")?),
|
||||
"--exposure" => exposure = Some(value.parse::<f64>().map_err(|_| "invalid exposure")?),
|
||||
// Defer file reads until a saved job has rejected conflicting controls.
|
||||
"--environment" => runtime.environment = Some(value.clone()),
|
||||
"--lighting" => runtime.lighting = Some(value.clone()),
|
||||
"--environment" => {
|
||||
let text =
|
||||
solidcraft_engine::io::vfs::read_to_string(value, 65536).map_err(|e| format!("could not read environment: {e}"))?;
|
||||
environment = Some(serde_json::from_str::<serde_json::Value>(&text).map_err(|e| e.to_string())?);
|
||||
}
|
||||
"--lighting" => {
|
||||
let text = solidcraft_engine::io::vfs::read_to_string(value, 65536).map_err(|e| format!("could not read lighting: {e}"))?;
|
||||
lighting = Some(serde_json::from_str::<solidcraft_engine::render::job::StudioLighting>(&text).map_err(|e| e.to_string())?);
|
||||
}
|
||||
"--tone-map" => {
|
||||
runtime.tone_map = Some(match value.as_str() {
|
||||
"reinhard" => ToneMap::Reinhard,
|
||||
"aces_fitted" => ToneMap::AcesFitted,
|
||||
tone_map = Some(match value.as_str() {
|
||||
"reinhard" => solidcraft_engine::render::job::ToneMap::Reinhard,
|
||||
"aces_fitted" => solidcraft_engine::render::job::ToneMap::AcesFitted,
|
||||
_ => return Err("tone-map must be reinhard or aces_fitted".into()),
|
||||
})
|
||||
}
|
||||
@@ -117,10 +69,21 @@ pub fn run(args: &[String]) -> Result<(), String> {
|
||||
}
|
||||
let out = out.ok_or("render requires --out FILE.png")?;
|
||||
let mut session = crate::load(input)?;
|
||||
let mut settings = if let Some(name) = setup.as_deref() {
|
||||
session.doc.render.setups.get(name).cloned().ok_or("no saved render setup with that name")?
|
||||
session.set_render_job_host(Some(CpuHost::for_platform().map_err(|e| e.to_string())?)).map_err(|e| e.to_string())?;
|
||||
if progressive {
|
||||
session.register_render_job_host(solidcraft_gpu::path::host().map_err(|e| e.to_string())?).map_err(|e| e.to_string())?;
|
||||
}
|
||||
session.execute("render.scene", &json!({})).map_err(|e| e.to_string())?;
|
||||
let mut settings = if let Some(name) = setup {
|
||||
session.doc.render.setups.get(&name).cloned().ok_or("no saved render setup with that name")?
|
||||
} else {
|
||||
RenderSetup::default()
|
||||
let mut settings = RenderSetup::default();
|
||||
if let Some(camera) = session.camera {
|
||||
settings.camera = camera;
|
||||
} else {
|
||||
settings.camera.fit(&solidcraft_engine::product_scene::extract(&session).map_err(|e| e.to_string())?.bounds);
|
||||
}
|
||||
settings
|
||||
};
|
||||
if let Some(width) = width {
|
||||
settings.width = width;
|
||||
@@ -131,24 +94,7 @@ pub fn run(args: &[String]) -> Result<(), String> {
|
||||
if let Some(exposure) = exposure {
|
||||
settings.exposure_ev = exposure;
|
||||
}
|
||||
// Resolve saved intent before opening a GPU or explicitly settling geometry.
|
||||
// Width/height/exposure only change this validated inline settings clone.
|
||||
let (mut params, needs_gpu) = runtime.request(&session, settings)?;
|
||||
session.set_render_job_host(Some(CpuHost::for_platform().map_err(|e| e.to_string())?)).map_err(|e| e.to_string())?;
|
||||
if needs_gpu {
|
||||
session.register_render_job_host(solidcraft_gpu::path::host().map_err(|e| e.to_string())?).map_err(|e| e.to_string())?;
|
||||
}
|
||||
session.execute("render.scene", &json!({})).map_err(|e| e.to_string())?;
|
||||
if setup.is_none() {
|
||||
let camera = if let Some(camera) = session.camera {
|
||||
camera
|
||||
} else {
|
||||
let mut camera = RenderSetup::default().camera;
|
||||
camera.fit(&solidcraft_engine::product_scene::extract(&session).map_err(|e| e.to_string())?.bounds);
|
||||
camera
|
||||
};
|
||||
params["settings"]["camera"] = json!(camera);
|
||||
}
|
||||
let params = json!({"settings":settings,"transparent_background":transparent,"progressive":progressive,"samples":samples,"max_depth":max_depth,"seed":seed,"lighting":lighting,"tone_map":tone_map,"environment":environment});
|
||||
let preflight = session.execute("render.preflight", ¶ms).map_err(|e| e.to_string())?;
|
||||
let started = session.execute("render.start", ¶ms).map_err(|e| e.to_string())?;
|
||||
let id: JobId = serde_json::from_value(started["job"].clone()).map_err(|e| e.to_string())?;
|
||||
|
||||
@@ -196,7 +196,7 @@ fn round3(x: f64) -> f64 {
|
||||
|
||||
/// Every action on every entity of the design's final bodies, in a fixed order.
|
||||
fn actions(s: &Session) -> Vec<Action> {
|
||||
let st = s.world_state().unwrap();
|
||||
let st = s.world_state();
|
||||
let placed = s.placed_document();
|
||||
let paths: BTreeMap<String, Vec<u64>> =
|
||||
placed.body_frames_with_source(&s.model.state()).into_iter().map(|(world, _, _, path, _)| (world, path)).collect();
|
||||
@@ -395,10 +395,8 @@ fn actions(s: &Session) -> Vec<Action> {
|
||||
let r = round3((shortest * 0.1).max(0.01));
|
||||
let mut add = |action, act| out.push(Action { entity: entity.clone(), action, act });
|
||||
if !smooth(e, mid) {
|
||||
// The midpoint is interior to this edge, but other bodies (including component
|
||||
// copies in their local frames) can have coincident edges. Retain the picked body.
|
||||
add("edge.fillet", Act::Run("solid.fillet", json!({"body": world, "edges": [pt(mid)], "radius": r})));
|
||||
add("edge.chamfer", Act::Run("solid.chamfer", json!({"body": world, "edges": [pt(mid)], "distance": r})));
|
||||
add("edge.fillet", Act::Run("solid.fillet", json!({"edges": [pt(mid)], "radius": r})));
|
||||
add("edge.chamfer", Act::Run("solid.chamfer", json!({"edges": [pt(mid)], "distance": r})));
|
||||
}
|
||||
let dir = straight(pts);
|
||||
if dir.is_some() {
|
||||
@@ -1280,41 +1278,6 @@ fn rectangle(plane: Value, p0: [f64; 2], p1: [f64; 2]) -> [Value; 3] {
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edge_round_actions_keep_the_picked_body() {
|
||||
for (corner, length) in [([10, 0, 20], 4), ([0, 0, 0], 20)] {
|
||||
let mut s = Session::default();
|
||||
s.execute("solid.box", &json!({"length": 20, "width": 20, "height": 20, "body_name": "Plate"})).unwrap();
|
||||
s.execute(
|
||||
"solid.box",
|
||||
&json!({"corner": corner, "length": length, "width": length, "height": length, "body_name": "Block", "operation": "new"}),
|
||||
)
|
||||
.unwrap();
|
||||
let mut tried = 0;
|
||||
for a in actions(&s) {
|
||||
if !a.entity.starts_with("Plate/") || !matches!(a.action, "edge.chamfer" | "edge.fillet") {
|
||||
continue;
|
||||
}
|
||||
let Act::Run(id, ref p) = a.act else { continue };
|
||||
let q = &p["edges"][0];
|
||||
let x = q[0].as_f64().unwrap();
|
||||
if q[1] != 0.0 || q[2] != 20.0 || !(0.0..20.0).contains(&x) {
|
||||
continue;
|
||||
}
|
||||
// Block either meets this midpoint or coincides with the whole edge.
|
||||
assert_eq!(p["body"], "Plate", "{}: {p}", a.action);
|
||||
assert_eq!(q, &json!([10.0, 0.0, 20.0]));
|
||||
let mut unscoped = p.clone();
|
||||
unscoped.as_object_mut().unwrap().remove("body");
|
||||
let err = s.scratch().execute(id, &unscoped).unwrap_err().to_string();
|
||||
assert!(err.contains("edges meet"), "{err}");
|
||||
perform(&mut s.scratch(), &a.act, &errors(&s)).unwrap();
|
||||
tried += 1;
|
||||
}
|
||||
assert_eq!(tried, 2);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_fillet_or_chamfer_on_one_edge_ending_at_a_cylinder() {
|
||||
for (cmd, size) in [("solid.fillet", "radius"), ("solid.chamfer", "distance")] {
|
||||
@@ -1338,9 +1301,7 @@ fn a_fillet_or_chamfer_on_one_edge_ending_at_a_cylinder() {
|
||||
fn a_round_on_a_knuckle_cap_circle() {
|
||||
for (cmd, size, removed) in [("solid.fillet", "radius", (2.4, 2.8)), ("solid.chamfer", "distance", (5.6, 6.3))] {
|
||||
let mut s = load(&root().join("tests/oracle/63-hinge-knuckle/recipe.json")).unwrap();
|
||||
let volume = |s: &Session| {
|
||||
s.model.state().bodies.iter().map(|b| b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.001)).unwrap().measure().volume).sum::<f64>()
|
||||
};
|
||||
let volume = |s: &Session| s.model.state().bodies.iter().map(|b| b.body.tessellate(0.001).unwrap().measure().volume).sum::<f64>();
|
||||
let before = volume(&s);
|
||||
s.execute(cmd, &json!({"edges": [[40.0, 29.837_722, 6.449_490]], size: 0.728})).unwrap_or_else(|e| panic!("{cmd}: {e}"));
|
||||
check(&s, 0, &BTreeMap::new()).unwrap_or_else(|(kind, e)| panic!("{cmd}: {kind}: {e}"));
|
||||
@@ -1360,9 +1321,7 @@ fn a_round_on_a_knuckle_cap_circle() {
|
||||
#[test]
|
||||
fn a_hair_thin_hole_on_a_ledge_beside_a_loft_cut() {
|
||||
let mut s = load(&root().join("examples/regressions/loft_cut_beside_round_hole.json")).unwrap();
|
||||
let volume = |s: &Session| {
|
||||
s.model.state().bodies.iter().map(|b| b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.005)).unwrap().measure().volume).sum::<f64>()
|
||||
};
|
||||
let volume = |s: &Session| s.model.state().bodies.iter().map(|b| b.body.tessellate_with(0.005, true).unwrap().measure().volume).sum::<f64>();
|
||||
let before = volume(&s);
|
||||
s.execute("solid.hole", &json!({"diameter": 0.133, "position": [357.22, -150.993_333, 730.0]})).unwrap();
|
||||
check(&s, 0, &BTreeMap::new()).unwrap_or_else(|(kind, e)| panic!("{kind}: {e}"));
|
||||
@@ -1379,9 +1338,7 @@ fn a_hair_thin_hole_on_a_ledge_beside_a_loft_cut() {
|
||||
#[test]
|
||||
fn a_small_hole_through_the_wall_of_a_plastic_rest() {
|
||||
let mut s = load(&root().join("apps/solidcraft-cli/tests/corpus/modern-plastic.solidcraft")).unwrap();
|
||||
let volume = |s: &Session| {
|
||||
s.model.state().bodies.iter().map(|b| b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.002)).unwrap().measure().volume).sum::<f64>()
|
||||
};
|
||||
let volume = |s: &Session| s.model.state().bodies.iter().map(|b| b.body.tessellate_with(0.002, true).unwrap().measure().volume).sum::<f64>();
|
||||
let before = volume(&s);
|
||||
s.execute("solid.hole", &json!({"diameter": 0.8, "position": [63.069_841_5, 28.333_333, 4.0]})).unwrap();
|
||||
check(&s, 0, &BTreeMap::new()).unwrap_or_else(|(kind, e)| panic!("{kind}: {e}"));
|
||||
@@ -1396,9 +1353,7 @@ fn a_small_hole_through_the_wall_of_a_plastic_rest() {
|
||||
#[test]
|
||||
fn a_hole_square_to_a_45_degree_flange() {
|
||||
let mut s = load(&root().join("tests/oracle/70-sm-flange-45/recipe.json")).unwrap();
|
||||
let volume = |s: &Session| {
|
||||
s.model.state().bodies.iter().map(|b| b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.002)).unwrap().measure().volume).sum::<f64>()
|
||||
};
|
||||
let volume = |s: &Session| s.model.state().bodies.iter().map(|b| b.body.tessellate_with(0.002, true).unwrap().measure().volume).sum::<f64>();
|
||||
let before = volume(&s);
|
||||
s.execute("solid.hole", &json!({"diameter": 3.448, "position": [66.666_667, -4.511_845, 4.511_845]})).unwrap();
|
||||
check(&s, 0, &BTreeMap::new()).unwrap_or_else(|(kind, e)| panic!("{kind}: {e}"));
|
||||
@@ -1736,7 +1691,7 @@ fn the_ribbed_lid_builds_the_same_body_every_time() {
|
||||
st.bodies
|
||||
.iter()
|
||||
.map(|b| {
|
||||
let mesh = b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.1)).unwrap();
|
||||
let mesh = b.body.tessellate(0.1).unwrap();
|
||||
let bits = mesh.positions.iter().fold(0u64, |acc, p| {
|
||||
acc.rotate_left(7).wrapping_add(p.x.to_bits() ^ p.y.to_bits().rotate_left(21) ^ p.z.to_bits().rotate_left(42))
|
||||
});
|
||||
|
||||
@@ -26,8 +26,8 @@ fn assert_two_through_holes(session: &Session, stage: &str) {
|
||||
let expected = 2.0 * PI * 4.0_f64.powi(2) * 6.0;
|
||||
assert!((removed - expected).abs() < expected * 1e-3, "{stage}: the two holes remove {removed} mm³, expected {expected} mm³");
|
||||
|
||||
let before_mesh = uncut.mesh(&solidcraft_kernel::MeshTolerance::chord(0.01)).unwrap();
|
||||
let after_mesh = bracket.mesh(&solidcraft_kernel::MeshTolerance::chord(0.01)).unwrap();
|
||||
let before_mesh = uncut.tessellate(0.01).unwrap();
|
||||
let after_mesh = bracket.tessellate(0.01).unwrap();
|
||||
let down = Vec3::new(0.0, 0.0, -1.0);
|
||||
for y in [-15.0, -45.0] {
|
||||
let center = Vec3::new(25.0, y, 1.0);
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
# Design file corpus
|
||||
|
||||
Designs saved by older SolidCraft builds. `tests/design_corpus.rs` opens each one with the
|
||||
current build and checks that it is upgraded to the current file format, that it evaluates
|
||||
(a feature may fail only by asking for a reference it lost to be picked again; what it rebuilds
|
||||
to is today's), and that damaged and hostile variants of it never panic. The `.expected.json`
|
||||
next to each is `solidcraft-cli eval` of that file by the build that saved it (bodies, volumes,
|
||||
areas, world bounding boxes), kept as a record of what that build made; no test compares with
|
||||
it.
|
||||
current build and checks that it evaluates as it did when saved (the `.expected.json` next to
|
||||
it is `solidcraft-cli eval` of that file by the build that saved it: bodies, volumes, areas,
|
||||
world bounding boxes), that it is upgraded to the current file format, and that damaged and
|
||||
hostile variants of it never panic.
|
||||
|
||||
| File | Saved by | Era |
|
||||
|---|---|---|
|
||||
|
||||
@@ -90,6 +90,10 @@
|
||||
],
|
||||
"timeline": [
|
||||
{
|
||||
"boolean_diagnostics": {
|
||||
"events": [],
|
||||
"truncated": false
|
||||
},
|
||||
"error": null,
|
||||
"id": 1,
|
||||
"ms": 0.131943,
|
||||
@@ -100,6 +104,10 @@
|
||||
"warning": null
|
||||
},
|
||||
{
|
||||
"boolean_diagnostics": {
|
||||
"events": [],
|
||||
"truncated": false
|
||||
},
|
||||
"error": null,
|
||||
"id": 2,
|
||||
"ms": 1.0812709999999999,
|
||||
@@ -110,6 +118,10 @@
|
||||
"warning": null
|
||||
},
|
||||
{
|
||||
"boolean_diagnostics": {
|
||||
"events": [],
|
||||
"truncated": false
|
||||
},
|
||||
"error": null,
|
||||
"id": 3,
|
||||
"ms": 0.450002,
|
||||
@@ -120,6 +132,10 @@
|
||||
"warning": null
|
||||
},
|
||||
{
|
||||
"boolean_diagnostics": {
|
||||
"events": [],
|
||||
"truncated": false
|
||||
},
|
||||
"error": null,
|
||||
"id": 4,
|
||||
"ms": 5.191514000000001,
|
||||
|
||||
@@ -54,6 +54,10 @@
|
||||
],
|
||||
"timeline": [
|
||||
{
|
||||
"boolean_diagnostics": {
|
||||
"events": [],
|
||||
"truncated": false
|
||||
},
|
||||
"error": null,
|
||||
"id": 1,
|
||||
"ms": 1.178957,
|
||||
@@ -64,6 +68,10 @@
|
||||
"warning": null
|
||||
},
|
||||
{
|
||||
"boolean_diagnostics": {
|
||||
"events": [],
|
||||
"truncated": false
|
||||
},
|
||||
"error": null,
|
||||
"id": 2,
|
||||
"ms": 83.24342399999999,
|
||||
@@ -74,6 +82,10 @@
|
||||
"warning": null
|
||||
},
|
||||
{
|
||||
"boolean_diagnostics": {
|
||||
"events": [],
|
||||
"truncated": false
|
||||
},
|
||||
"error": null,
|
||||
"id": 3,
|
||||
"ms": 0.150889,
|
||||
@@ -84,6 +96,10 @@
|
||||
"warning": null
|
||||
},
|
||||
{
|
||||
"boolean_diagnostics": {
|
||||
"events": [],
|
||||
"truncated": false
|
||||
},
|
||||
"error": null,
|
||||
"id": 4,
|
||||
"ms": 94.465481,
|
||||
@@ -94,6 +110,10 @@
|
||||
"warning": null
|
||||
},
|
||||
{
|
||||
"boolean_diagnostics": {
|
||||
"events": [],
|
||||
"truncated": false
|
||||
},
|
||||
"error": null,
|
||||
"id": 5,
|
||||
"ms": 50.426736,
|
||||
@@ -104,6 +124,10 @@
|
||||
"warning": null
|
||||
},
|
||||
{
|
||||
"boolean_diagnostics": {
|
||||
"events": [],
|
||||
"truncated": false
|
||||
},
|
||||
"error": null,
|
||||
"id": 6,
|
||||
"ms": 40.383005,
|
||||
@@ -114,6 +138,10 @@
|
||||
"warning": null
|
||||
},
|
||||
{
|
||||
"boolean_diagnostics": {
|
||||
"events": [],
|
||||
"truncated": false
|
||||
},
|
||||
"error": null,
|
||||
"id": 7,
|
||||
"ms": 59.041461000000005,
|
||||
@@ -124,6 +152,18 @@
|
||||
"warning": null
|
||||
},
|
||||
{
|
||||
"boolean_diagnostics": {
|
||||
"events": [
|
||||
{
|
||||
"face_shift_mm": 0.0,
|
||||
"rotation_rad": 0.0,
|
||||
"strategy": "planar_fallback",
|
||||
"tolerance_mm": 0.0,
|
||||
"translation_mm": 0.0
|
||||
}
|
||||
],
|
||||
"truncated": false
|
||||
},
|
||||
"error": null,
|
||||
"id": 8,
|
||||
"ms": 562.5477970000001,
|
||||
@@ -131,9 +171,13 @@
|
||||
"rolled_back": false,
|
||||
"suppressed": false,
|
||||
"type": "BoxFeature",
|
||||
"warning": null
|
||||
"warning": "Boolean retry: exact planar fallback (no displacement)"
|
||||
},
|
||||
{
|
||||
"boolean_diagnostics": {
|
||||
"events": [],
|
||||
"truncated": false
|
||||
},
|
||||
"error": null,
|
||||
"id": 9,
|
||||
"ms": 89.56076,
|
||||
@@ -144,6 +188,10 @@
|
||||
"warning": null
|
||||
},
|
||||
{
|
||||
"boolean_diagnostics": {
|
||||
"events": [],
|
||||
"truncated": false
|
||||
},
|
||||
"error": null,
|
||||
"id": 10,
|
||||
"ms": 65.330634,
|
||||
@@ -154,6 +202,10 @@
|
||||
"warning": null
|
||||
},
|
||||
{
|
||||
"boolean_diagnostics": {
|
||||
"events": [],
|
||||
"truncated": false
|
||||
},
|
||||
"error": null,
|
||||
"id": 11,
|
||||
"ms": 97.229403,
|
||||
|
||||
@@ -99,6 +99,10 @@
|
||||
],
|
||||
"timeline": [
|
||||
{
|
||||
"boolean_diagnostics": {
|
||||
"events": [],
|
||||
"truncated": false
|
||||
},
|
||||
"error": null,
|
||||
"id": 1,
|
||||
"ms": 0.204585,
|
||||
@@ -109,6 +113,10 @@
|
||||
"warning": null
|
||||
},
|
||||
{
|
||||
"boolean_diagnostics": {
|
||||
"events": [],
|
||||
"truncated": false
|
||||
},
|
||||
"error": null,
|
||||
"id": 2,
|
||||
"ms": 2.318662,
|
||||
@@ -119,6 +127,10 @@
|
||||
"warning": null
|
||||
},
|
||||
{
|
||||
"boolean_diagnostics": {
|
||||
"events": [],
|
||||
"truncated": false
|
||||
},
|
||||
"error": null,
|
||||
"id": 3,
|
||||
"ms": 0.109196,
|
||||
@@ -129,6 +141,10 @@
|
||||
"warning": null
|
||||
},
|
||||
{
|
||||
"boolean_diagnostics": {
|
||||
"events": [],
|
||||
"truncated": false
|
||||
},
|
||||
"error": null,
|
||||
"id": 4,
|
||||
"ms": 0.338859,
|
||||
@@ -139,6 +155,10 @@
|
||||
"warning": null
|
||||
},
|
||||
{
|
||||
"boolean_diagnostics": {
|
||||
"events": [],
|
||||
"truncated": false
|
||||
},
|
||||
"error": null,
|
||||
"id": 5,
|
||||
"ms": 0.312259,
|
||||
@@ -149,6 +169,10 @@
|
||||
"warning": null
|
||||
},
|
||||
{
|
||||
"boolean_diagnostics": {
|
||||
"events": [],
|
||||
"truncated": false
|
||||
},
|
||||
"error": null,
|
||||
"id": 6,
|
||||
"ms": 0.23144699999999999,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//! Designs saved by older SolidCraft builds (tests/corpus, see its README) open, are upgraded to
|
||||
//! the current format and evaluate (what they rebuild to today is today's, not what the build
|
||||
//! that saved them made); damaged and hostile design files never panic.
|
||||
//! Designs saved by older SolidCraft builds (tests/corpus, see its README) open and evaluate as
|
||||
//! they did when they were saved; damaged and hostile design files never panic.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
@@ -22,24 +21,67 @@ fn open(path: &Path) -> Session {
|
||||
s
|
||||
}
|
||||
|
||||
/// Every corpus design opens, is upgraded to the current format, evaluates, and saves and reads
|
||||
/// back the same. A feature may fail only by asking for a reference it lost to be picked again
|
||||
/// (a name saved before format 14 finds nothing today).
|
||||
/// Differences that are deliberate behaviour changes made before the file format had versions
|
||||
/// (no migration can tell those files apart): (file, check, why).
|
||||
const KNOWN: [(&str, &str, &str); 2] = [
|
||||
(
|
||||
"m0-revolve-primitives",
|
||||
"Body1 bbox",
|
||||
"partial revolves turn toward the sketch normal since ac43365 (as most parametric CAD tools do); M0 turned them the other way",
|
||||
),
|
||||
("components-arm", "body_count", "Measure counted only the root component's bodies then; it measures every placed body now"),
|
||||
];
|
||||
|
||||
fn known(file: &str, check: &str) -> bool {
|
||||
KNOWN.iter().any(|(f, c, _)| *f == file && *c == check)
|
||||
}
|
||||
|
||||
/// Volumes and areas then came from coarser tessellation (and curved booleans and fillets have
|
||||
/// become exact since): the same part within this relative tolerance (the oracle's).
|
||||
const REL: f64 = 1e-3;
|
||||
|
||||
fn close(a: f64, b: f64, rel: f64) -> bool {
|
||||
(a - b).abs() <= rel * b.abs().max(1.0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn old_designs_open_upgrade_and_evaluate() {
|
||||
fn old_designs_open_and_evaluate_as_saved() {
|
||||
let mut problems = Vec::new();
|
||||
for p in corpus() {
|
||||
let name = p.file_stem().unwrap_or_default().to_string_lossy().to_string();
|
||||
let want: Value = serde_json::from_slice(&std::fs::read(p.with_extension("expected.json")).unwrap()).unwrap();
|
||||
let mut s = open(&p);
|
||||
for r in &s.model.results {
|
||||
if let Some(e) = r.error.as_deref().filter(|e| solidcraft_engine::doc::reselect_parts(e).is_none()) {
|
||||
if let Some(e) = &r.error {
|
||||
problems.push(format!("{name}: feature {}: {e}", r.id));
|
||||
}
|
||||
}
|
||||
assert_eq!(s.doc.format, solidcraft_engine::doc::format::format_string(solidcraft_engine::doc::format::FORMAT), "{name}: upgraded");
|
||||
let got = s.execute("inspect.measure", &json!({})).unwrap();
|
||||
if got["body_count"].as_u64().unwrap_or(0) == 0 {
|
||||
problems.push(format!("{name}: no bodies"));
|
||||
if got["body_count"] != want["body_count"] && !known(&name, "body_count") {
|
||||
problems.push(format!("{name}: {} bodies, saved with {}", got["body_count"], want["body_count"]));
|
||||
}
|
||||
for w in want["bodies"].as_array().into_iter().flatten() {
|
||||
let bname = w["name"].as_str().unwrap_or_default();
|
||||
let Some(g) = got["bodies"].as_array().into_iter().flatten().find(|g| g["name"] == w["name"]) else {
|
||||
problems.push(format!("{name}: body {bname} missing"));
|
||||
continue;
|
||||
};
|
||||
for k in ["volume_mm3", "area_mm2"] {
|
||||
let (a, b) = (g[k].as_f64().unwrap_or(f64::NAN), w[k].as_f64().unwrap_or(f64::NAN));
|
||||
if !close(a, b, REL) {
|
||||
problems.push(format!("{name}: {bname} {k} {a} (saved {b})"));
|
||||
}
|
||||
}
|
||||
if known(&name, &format!("{bname} bbox")) {
|
||||
continue;
|
||||
}
|
||||
for (end, i) in [("min", 0), ("min", 1), ("min", 2), ("max", 0), ("max", 1), ("max", 2)] {
|
||||
let (a, b) = (g["bbox"][end][i].as_f64().unwrap_or(f64::NAN), w["bbox"][end][i].as_f64().unwrap_or(f64::NAN));
|
||||
if (a - b).abs() > 1e-3 * b.abs().max(1.0) {
|
||||
problems.push(format!("{name}: {bname} bbox {end}[{i}] {a} (saved {b})"));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Saved again, it reads back the same.
|
||||
let again = solidcraft_engine::doc::Document::from_json(&s.doc.to_json()).unwrap();
|
||||
@@ -52,48 +94,40 @@ fn old_designs_open_upgrade_and_evaluate() {
|
||||
}
|
||||
|
||||
/// Names saved before format 14 were worked out from geometry, pieces of a split face numbered by
|
||||
/// where their centres lay (`F2:top#2`). They open marked as such and find no face or edge today:
|
||||
/// each feature holding one fails, asking for its reference to be picked again, and the rest of
|
||||
/// the design builds.
|
||||
/// where their centres lay (`F2:top#2`). They keep naming the faces they named: marked as such,
|
||||
/// they are found as the build that saved them found them, without new notes.
|
||||
#[test]
|
||||
fn names_saved_before_exact_history_ask_for_their_faces_again() {
|
||||
fn names_saved_before_exact_history_find_the_faces_they_named() {
|
||||
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/corpus/naming-split-pieces.solidcraft");
|
||||
let s = open(&path);
|
||||
let feature = |name: &str| s.doc.features.iter().find(|f| f.name == name).unwrap_or_else(|| panic!("{name}"));
|
||||
assert_eq!(feature("Round").edge_names, vec!["?F2:side:l2|F2:top#2".to_string()]);
|
||||
assert_eq!(feature("Bevel").edge_names, vec!["?F2:side:l4|F2:top#1".to_string()]);
|
||||
assert_eq!(feature("Raise").face_names, vec!["?F2:top#1".to_string()]);
|
||||
let asked = |name: &str| {
|
||||
let r = s.model.result(feature(name).id).unwrap_or_else(|| panic!("{name}"));
|
||||
r.error.as_deref().and_then(solidcraft_engine::doc::reselect_parts).map(|(f, reference)| (f.to_string(), reference.to_string()))
|
||||
};
|
||||
assert_eq!(asked("Round"), Some(("Round".into(), "?F2:side:l2|F2:top#2".into())));
|
||||
assert_eq!(asked("Bevel"), Some(("Bevel".into(), "?F2:side:l4|F2:top#1".into())));
|
||||
assert_eq!(asked("Raise"), Some(("Raise".into(), "?F2:top#1".into())));
|
||||
for r in s.model.results.iter().filter(|r| !["Round", "Bevel", "Raise"].contains(&r.name.as_str())) {
|
||||
assert!(r.error.is_none(), "{}: {:?}", r.name, r.error);
|
||||
for r in &s.model.results {
|
||||
assert!(r.error.is_none() && r.warning.is_none(), "{}: {:?} {:?}", r.name, r.error, r.warning);
|
||||
}
|
||||
assert!(!s.model.state().bodies.is_empty());
|
||||
// The raised piece is the left one, as saved (the stored point is on it).
|
||||
let state = s.model.state();
|
||||
let solid = |p: [f64; 3]| state.bodies.iter().any(|b| b.body.contains(solidcraft_engine::geom::Vec3::new(p[0], p[1], p[2])));
|
||||
assert!(solid([7.0, 15.0, 11.0]) && !solid([32.0, 15.0, 11.0]));
|
||||
}
|
||||
|
||||
/// Body names saved before format 14 named the pieces of a split body in the kernel's shell
|
||||
/// order: such a design opens with every feature marked `shell_piece_order` and evaluates, each
|
||||
/// body name it stores naming a piece. (Which piece takes which name is not promised to be as it
|
||||
/// was saved.) Its fillet and chamfer hold edge names saved then, which find nothing today: they
|
||||
/// ask for their edges to be picked again.
|
||||
/// Body names saved before format 14 name the pieces of a split body in the kernel's shell order.
|
||||
/// The features after the split that refer to a piece by its body name still find that piece:
|
||||
/// the far one stays `Body1` (chamfered), the near one `Body1 (1)` (filleted, coloured).
|
||||
#[test]
|
||||
fn body_names_saved_before_exact_history_open_marked_and_evaluate() {
|
||||
fn body_names_saved_before_exact_history_keep_their_pieces() {
|
||||
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/corpus/split-body-pieces.solidcraft");
|
||||
let s = open(&path);
|
||||
assert!(s.doc.features.iter().all(|f| f.shell_piece_order), "every saved feature keeps the shell order");
|
||||
for r in &s.model.results {
|
||||
let asked = r.error.as_deref().and_then(solidcraft_engine::doc::reselect_parts);
|
||||
assert!(r.error.is_none() || asked.is_some_and(|(_, reference)| reference.starts_with('?')), "{}: {:?}", r.name, r.error);
|
||||
assert!(r.error.is_none() && r.warning.is_none(), "{}: {:?} {:?}", r.name, r.error, r.warning);
|
||||
}
|
||||
let state = s.model.state();
|
||||
for name in ["Body1", "Body1 (1)"] {
|
||||
assert!(state.bodies.iter().any(|b| b.name == name), "{name}");
|
||||
}
|
||||
let piece = |name: &str| state.bodies.iter().find(|b| b.name == name).unwrap_or_else(|| panic!("{name}"));
|
||||
let solid = |name: &str, p: [f64; 3]| piece(name).body.contains(solidcraft_engine::geom::Vec3::new(p[0], p[1], p[2]));
|
||||
assert!(solid("Body1", [20.0, 24.0, 5.0]) && solid("Body1 (1)", [20.0, 6.0, 5.0]));
|
||||
assert!(s.doc.appearances.bodies.contains_key("Body1 (1)"));
|
||||
}
|
||||
|
||||
@@ -139,7 +173,6 @@ fn flat_face_attachments() -> Vec<(String, String, usize, Option<(u32, bool)>, O
|
||||
for p in corpus() {
|
||||
let name = p.file_stem().unwrap_or_default().to_string_lossy().to_string();
|
||||
let mut s = open(&p);
|
||||
let failed_on_open: Vec<u64> = s.model.results.iter().filter(|r| r.error.is_some()).map(|r| r.id).collect();
|
||||
let state = s.model.state();
|
||||
let placed = s.placed_document();
|
||||
let mut picks = Vec::new();
|
||||
@@ -147,7 +180,7 @@ fn flat_face_attachments() -> Vec<(String, String, usize, Option<(u32, bool)>, O
|
||||
let Some(body) = state.bodies.iter().find(|b| b.definition_name() == definition && placed.model_body_component(b) == component) else {
|
||||
continue;
|
||||
};
|
||||
let mesh = body.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.01)).unwrap();
|
||||
let mesh = body.body.tessellate(0.01).unwrap();
|
||||
for index in 0..body.body.face_count() {
|
||||
if body.body.planar_face(index).is_none() {
|
||||
continue;
|
||||
@@ -191,8 +224,7 @@ fn flat_face_attachments() -> Vec<(String, String, usize, Option<(u32, bool)>, O
|
||||
let known = known_without_attachment(&name, &body, normal, distance);
|
||||
out.push((name.clone(), body, index, known, problem));
|
||||
}
|
||||
let failed: Vec<u64> = s.model.results.iter().filter(|r| r.error.is_some()).map(|r| r.id).collect();
|
||||
assert_eq!(failed, failed_on_open, "{name}: undo leaves the features that failed on opening, and only those, failed");
|
||||
assert!(s.model.results.iter().all(|r| r.error.is_none()), "{name}: left with errors after undo");
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
@@ -139,13 +139,11 @@ fn eval_json_out_holds_what_stdout_would() {
|
||||
fn json_out_needs_a_file_and_fails_when_it_cannot_write() {
|
||||
let dir = Scratch::new();
|
||||
let script = dir.script();
|
||||
for cmd in ["run", "eval", "inspect"] {
|
||||
for trailing in [&[][..], &["--in"][..]] {
|
||||
let output = cli().arg(cmd).arg(&script).arg("--json-out").args(trailing).output().unwrap();
|
||||
assert_eq!(output.status.code(), Some(1), "{cmd}");
|
||||
assert!(String::from_utf8_lossy(&output.stderr).contains("--json-out needs a FILE"), "{cmd}");
|
||||
assert!(output.stdout.is_empty(), "{cmd}");
|
||||
}
|
||||
for cmd in ["run", "eval"] {
|
||||
let output = cli().arg(cmd).arg(&script).arg("--json-out").output().unwrap();
|
||||
assert_eq!(output.status.code(), Some(1), "{cmd}");
|
||||
assert!(String::from_utf8_lossy(&output.stderr).contains("--json-out needs a FILE"), "{cmd}");
|
||||
assert!(output.stdout.is_empty(), "{cmd}");
|
||||
|
||||
let missing = dir.0.join("missing").join("out.json");
|
||||
let output = cli().arg(cmd).arg(&script).arg("--json-out").arg(&missing).output().unwrap();
|
||||
|
||||
@@ -151,115 +151,3 @@ fn mcp_without_arguments_finds_the_running_app() {
|
||||
assert!(!String::from_utf8_lossy(&out.stderr).contains("bridged to the running app"), "{}", String::from_utf8_lossy(&out.stderr));
|
||||
std::fs::remove_dir_all(&control_dir).ok();
|
||||
}
|
||||
|
||||
/// `mcp setup` against a temporary home: the client folders it finds are the only ones it writes.
|
||||
mod setup {
|
||||
use serde_json::{Value, json};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
fn home(name: &str) -> PathBuf {
|
||||
let h = std::env::temp_dir().join(format!("solidcraft-mcp-setup-{name}-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&h);
|
||||
std::fs::create_dir_all(&h).unwrap();
|
||||
h
|
||||
}
|
||||
|
||||
fn run(home: &Path, args: &[&str]) -> std::process::Output {
|
||||
Command::new(env!("CARGO_BIN_EXE_solidcraft-cli"))
|
||||
.args(["mcp", "setup"])
|
||||
.args(args)
|
||||
.env_clear()
|
||||
.env("HOME", home)
|
||||
.env("USERPROFILE", home)
|
||||
.env("APPDATA", home.join("AppData/Roaming"))
|
||||
.env("LOCALAPPDATA", home.join("AppData/Local"))
|
||||
.env("XDG_CONFIG_HOME", home.join(".config"))
|
||||
.env("PATH", "")
|
||||
.output()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn json_of(p: &Path) -> Value {
|
||||
serde_json::from_str(&std::fs::read_to_string(p).unwrap()).unwrap()
|
||||
}
|
||||
|
||||
fn exe() -> String {
|
||||
let p = Path::new(env!("CARGO_BIN_EXE_solidcraft-cli")).canonicalize().unwrap();
|
||||
p.to_string_lossy().trim_start_matches(r"\\?\").to_owned()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merges_into_opencode_and_cursor_keeping_other_servers() {
|
||||
let h = home("merge");
|
||||
let oc = h.join(".config/opencode");
|
||||
std::fs::create_dir_all(&oc).unwrap();
|
||||
let old = json!({"$schema": "https://opencode.ai/config.json", "theme": "x", "mcp": {"other": {"type": "local", "command": ["o"]}}});
|
||||
std::fs::write(oc.join("opencode.json"), old.to_string()).unwrap();
|
||||
std::fs::create_dir_all(h.join(".cursor")).unwrap();
|
||||
std::fs::write(h.join(".cursor/mcp.json"), r#"{"mcpServers": {"other": {"command": "o"}}}"#).unwrap();
|
||||
|
||||
let out = run(&h, &["--live"]);
|
||||
assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr));
|
||||
let v = json_of(&oc.join("opencode.json"));
|
||||
assert_eq!(v["theme"], "x");
|
||||
assert_eq!(v["mcp"]["other"], old["mcp"]["other"]);
|
||||
assert_eq!(v["mcp"]["solidcraft"], json!({"type": "local", "command": [exe(), "mcp"], "enabled": true}));
|
||||
assert_eq!(v["mcp"]["solidcraft-live"]["command"], json!([exe(), "mcp", "--connect", "127.0.0.1:7878"]));
|
||||
assert_eq!(json_of(&oc.join("opencode.json.bak")), old);
|
||||
let c = json_of(&h.join(".cursor/mcp.json"));
|
||||
assert_eq!(c["mcpServers"]["other"], json!({"command": "o"}));
|
||||
assert_eq!(c["mcpServers"]["solidcraft"], json!({"command": exe(), "args": ["mcp"]}));
|
||||
|
||||
// Codex (TOML) and Claude Code (a big file the CLI owns) are edited in place too.
|
||||
std::fs::create_dir_all(h.join(".codex")).unwrap();
|
||||
std::fs::write(h.join(".codex/config.toml"), "# mine\nmodel = \"x\"\n").unwrap();
|
||||
let cc = "{\n \"numStartups\": 3,\n \"projects\": {}\n}\n";
|
||||
std::fs::write(h.join(".claude.json"), cc).unwrap();
|
||||
assert!(run(&h, &[]).status.success());
|
||||
let toml = std::fs::read_to_string(h.join(".codex/config.toml")).unwrap();
|
||||
assert!(toml.starts_with("# mine\nmodel = \"x\"\n") && toml.contains("[mcp_servers.solidcraft]"), "{toml}");
|
||||
let c = json_of(&h.join(".claude.json"));
|
||||
assert_eq!(c["numStartups"], 3);
|
||||
assert_eq!(c["mcpServers"]["solidcraft"], json!({"type": "stdio", "command": exe(), "args": ["mcp"]}));
|
||||
assert_eq!(std::fs::read_to_string(h.join(".claude.json.bak")).unwrap(), cc);
|
||||
|
||||
// Running again updates the same entries and leaves one of each.
|
||||
assert!(run(&h, &[]).status.success());
|
||||
assert_eq!(json_of(&h.join(".cursor/mcp.json"))["mcpServers"].as_object().unwrap().len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dry_run_writes_nothing_and_missing_clients_are_skipped() {
|
||||
let h = home("dry");
|
||||
std::fs::create_dir_all(h.join(".cursor")).unwrap();
|
||||
let out = run(&h, &["--dry-run"]);
|
||||
assert!(out.status.success());
|
||||
let text = String::from_utf8_lossy(&out.stdout);
|
||||
assert!(text.contains("would write") && text.contains("solidcraft"), "{text}");
|
||||
assert!(!h.join(".cursor/mcp.json").exists());
|
||||
assert!(!h.join(".config/opencode").exists(), "OpenCode was not installed, so nothing is created for it");
|
||||
let out = run(&h, &["--client", "claude-desktop"]);
|
||||
assert!(String::from_utf8_lossy(&out.stdout).contains("not found"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_file_that_is_not_json_is_left_alone() {
|
||||
let h = home("bad");
|
||||
std::fs::create_dir_all(h.join(".cursor")).unwrap();
|
||||
std::fs::write(h.join(".cursor/mcp.json"), "{ // comment").unwrap();
|
||||
let out = run(&h, &[]);
|
||||
assert!(!out.status.success());
|
||||
assert_eq!(std::fs::read_to_string(h.join(".cursor/mcp.json")).unwrap(), "{ // comment");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claude_desktop_msix_folder_is_found() {
|
||||
let h = home("msix");
|
||||
let pkg = h.join("AppData/Local/Packages/Claude_abc/LocalCache/Roaming/Claude");
|
||||
std::fs::create_dir_all(&pkg).unwrap();
|
||||
// The MSIX folder is only read through LOCALAPPDATA, set for every platform here.
|
||||
assert!(run(&h, &["--client", "claude-desktop"]).status.success());
|
||||
assert!(json_of(&pkg.join("claude_desktop_config.json"))["mcpServers"]["solidcraft"].is_object());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
//! Explicit native GPU registration must not change bridged MCP ownership.
|
||||
use std::process::Command;
|
||||
#[test]
|
||||
fn connected_application_rejects_headless_gpu_installation_before_connecting() {
|
||||
let result = Command::new(env!("CARGO_BIN_EXE_solidcraft-cli")).args(["mcp", "--render-gpu", "--connect", "127.0.0.1:1"]).output().unwrap();
|
||||
assert!(!result.status.success());
|
||||
let message = String::from_utf8_lossy(&result.stderr);
|
||||
assert!(message.contains("connected application owns its own device"), "{message}");
|
||||
}
|
||||
@@ -63,7 +63,7 @@ fn model(name: &str, s: &Session) -> Value {
|
||||
.iter()
|
||||
.map(|r| json!({"id": r.id, "name": r.name, "error": r.error, "warning": r.warning, "skipped": r.skipped, "state": state(name, &r.state)}))
|
||||
.collect();
|
||||
json!({"results": results, "world": state(name, &s.world_state().unwrap())})
|
||||
json!({"results": results, "world": state(name, &s.world_state())})
|
||||
}
|
||||
|
||||
/// The same edit on both: a parameter's value when the design has one, else a new body.
|
||||
@@ -288,57 +288,3 @@ fn undoing_an_edit_after_opening_reuses_the_saved_results() {
|
||||
let _ = std::fs::remove_file(&path);
|
||||
let _ = std::fs::remove_file(format!("{}.bak", path.display()));
|
||||
}
|
||||
|
||||
/// A saved model holds the results of the body check mode it was evaluated under: opened under
|
||||
/// the same mode it is taken as it is, under another the design is evaluated again.
|
||||
#[test]
|
||||
fn a_model_saved_under_another_check_mode_is_evaluated_again() {
|
||||
let plate = solidcraft_engine::sample::samples().into_iter().find(|s| s.name == "Sample Plate").unwrap();
|
||||
let mut built = Session::default();
|
||||
for c in (plate.script)()["commands"].as_array().unwrap() {
|
||||
built.execute(c["command"].as_str().unwrap(), &c["params"]).unwrap();
|
||||
}
|
||||
let path = temp("mode");
|
||||
built.execute("file.save_as", &json!({"path": path.to_string_lossy()})).unwrap();
|
||||
let same = open(&path);
|
||||
assert_eq!(same.model.validation(), built.model.validation());
|
||||
assert_eq!(same.model.recomputed, 0, "taken from the file");
|
||||
let mut other = Session::default();
|
||||
other.set_validation(solidcraft_engine::kernel::Validation::EachStep);
|
||||
assert_ne!(other.validation(), built.model.validation());
|
||||
other.execute("doc.open", &json!({"path": path.to_string_lossy()})).unwrap();
|
||||
assert_eq!(other.validation(), solidcraft_engine::kernel::Validation::EachStep);
|
||||
assert!(other.model.recomputed > 0, "evaluated again under its own mode");
|
||||
let _ = std::fs::remove_file(&path);
|
||||
let _ = std::fs::remove_file(format!("{}.bak", path.display()));
|
||||
}
|
||||
|
||||
/// A body made by rounds keeps, in the saved model, the rounds and the body they were made on: a
|
||||
/// later round at the corner where they meet blends them again with it whether the design was
|
||||
/// opened from its model or evaluated anew. Two edges of a 20 mm box rounded r2 (a mitre at the
|
||||
/// corner (20, 0, 20)), saved; opened, the upright edge there rounded r3: the corner all three
|
||||
/// rounded at once make (SMLib's setback corner, kernel `three_rounds_at_a_corner`), 15 faces and
|
||||
/// 7928.142563938 mm³, the same body as the design evaluated anew.
|
||||
#[test]
|
||||
fn a_later_round_after_opening_blends_the_saved_rounds_again() {
|
||||
let mut built = Session::default();
|
||||
built.execute("solid.box", &json!({"length": 20, "width": 20, "height": 20, "body_name": "Box"})).unwrap();
|
||||
built.execute("solid.fillet", &json!({"edges": [[20, 10, 20], [10, 0, 20]], "radius": 2})).unwrap();
|
||||
let path = temp("later-round");
|
||||
built.execute("file.save_as", &json!({"path": path.to_string_lossy()})).unwrap();
|
||||
let bytes = std::fs::read(&path).unwrap();
|
||||
let mut cached = open(&path);
|
||||
assert_eq!(cached.model.last_recomputed, 0, "opened without evaluating");
|
||||
let mut rebuilt = Session::new(solidcraft_engine::io::read_design(&bytes).unwrap());
|
||||
for s in [&mut cached, &mut rebuilt] {
|
||||
s.execute("solid.fillet", &json!({"edges": [[20, 0, 10]], "radius": 3})).unwrap();
|
||||
let st = s.model.state();
|
||||
let b = &st.body("Box").unwrap().body;
|
||||
let v = solidcraft_engine::kernel::measure(b).unwrap().volume;
|
||||
assert_eq!(b.face_count(), 15);
|
||||
assert!((v - 7928.142563938).abs() < 1e-6, "{v}");
|
||||
}
|
||||
assert_eq!(model("later round", &cached), model("later round", &rebuilt));
|
||||
let _ = std::fs::remove_file(&path);
|
||||
let _ = std::fs::remove_file(format!("{}.bak", path.display()));
|
||||
}
|
||||
|
||||
@@ -1,403 +0,0 @@
|
||||
//! Every design of the corpus loads, two builds of it name its faces and edges alike, and a
|
||||
//! name it stores that no longer finds its face or edge fails its feature. The stored names are
|
||||
//! the fillet and chamfer edges, the faces a feature picks, the face a sketch sits on and the
|
||||
//! sheet metal edges; one that finds nothing (`gone`, a sketch's face `lost`) must leave its
|
||||
//! feature failed (it asks for the reference to be picked again, never takes another face or
|
||||
//! edge), and a design that no longer loads fails the test. A build's names are each face's and
|
||||
//! edge's name with where it is (its area and centre, its length and middle); the second build
|
||||
//! must give the same, and its stored names must find the same faces and edges.
|
||||
//!
|
||||
//! Face and edge indices are left out: they are the kernel's order, which a rebuild need not
|
||||
//! keep (a fillet's or a boolean's faces come out in another order from one build to the next).
|
||||
//! How many faces a body has and what each is called is the kernel's business, not this test's.
|
||||
//!
|
||||
//! The corpus is the action matrix's (the old design files in `tests/corpus`, the examples, the
|
||||
//! demo and typical designs in `tests/action_matrix/designs`, the reference recipes), the sample
|
||||
//! designs, the engine's design fixtures and the older designs kept by the hole matrix, taper
|
||||
//! and projection tests.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_engine::Session;
|
||||
use solidcraft_engine::doc::{FaceAttachmentStatus, Feature, FeatureKind, ModelBody, ModelState, PlaneRef, naming};
|
||||
use solidcraft_engine::geom::Vec3;
|
||||
|
||||
fn root() -> PathBuf {
|
||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("../..").canonicalize().unwrap()
|
||||
}
|
||||
|
||||
/// Where a design comes from.
|
||||
enum Source {
|
||||
File(PathBuf),
|
||||
Sample(fn() -> Value),
|
||||
}
|
||||
|
||||
/// Every design of the corpus: (`<group>/<design>`, where it comes from).
|
||||
fn designs() -> Vec<(String, Source)> {
|
||||
let root = root();
|
||||
let mut out = Vec::new();
|
||||
let mut add = |prefix: &str, dir: PathBuf, ext: &str| {
|
||||
let Ok(rd) = std::fs::read_dir(&dir) else { return };
|
||||
let mut v: Vec<PathBuf> = rd.flatten().map(|e| e.path()).filter(|p| p.extension().is_some_and(|x| x == ext)).collect();
|
||||
v.sort();
|
||||
for p in v {
|
||||
let stem = p.file_stem().unwrap_or_default().to_string_lossy().to_string();
|
||||
out.push((format!("{prefix}/{stem}"), Source::File(p)));
|
||||
}
|
||||
};
|
||||
add("corpus", root.join("apps/solidcraft-cli/tests/corpus"), "solidcraft");
|
||||
add("examples", root.join("examples"), "json");
|
||||
add("examples", root.join("examples/acceptance"), "json");
|
||||
add("regressions", root.join("examples/regressions"), "json");
|
||||
add("demo", root.join("examples/demo"), "json");
|
||||
add("typical", root.join("tests/action_matrix/designs"), "json");
|
||||
add("demo", root.join("tests/action_matrix/designs"), "solidcraft");
|
||||
add("fixtures", root.join("crates/engine/src/tests/fixtures"), "solidcraft");
|
||||
add("hole_matrix", root.join("tests/hole_matrix/old-0.9"), "json");
|
||||
add("taper", root.join("tests/taper/before-1014"), "json");
|
||||
add("projection", root.join("tests/projection/before-1508"), "json");
|
||||
let mut cases: Vec<PathBuf> =
|
||||
std::fs::read_dir(root.join("tests/oracle")).unwrap().flatten().map(|e| e.path()).filter(|p| p.join("recipe.json").is_file()).collect();
|
||||
cases.sort();
|
||||
for c in cases {
|
||||
out.push((format!("oracle/{}", c.file_name().unwrap_or_default().to_string_lossy()), Source::File(c.join("recipe.json"))));
|
||||
}
|
||||
for sample in solidcraft_engine::sample::samples() {
|
||||
let stem: String = sample.name.to_lowercase().split_whitespace().collect::<Vec<_>>().join("-");
|
||||
out.push((format!("samples/{stem}"), Source::Sample(sample.script)));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Build a design: a design file (also a design saved as JSON) is opened, a script is run.
|
||||
fn load(source: &Source) -> Result<Session, String> {
|
||||
let mut s = Session::default();
|
||||
let script = match source {
|
||||
Source::Sample(script) => script(),
|
||||
Source::File(path) if path.extension().is_some_and(|x| x == "solidcraft") => {
|
||||
s.execute("doc.open", &json!({"path": path.to_string_lossy()})).map_err(|e| e.to_string())?;
|
||||
return Ok(s);
|
||||
}
|
||||
Source::File(path) if path.file_name().is_some_and(|n| n == "recipe.json") => {
|
||||
let out = Command::new(env!("CARGO_BIN_EXE_solidcraft-cli")).arg("recipe").arg(path).output().map_err(|e| e.to_string())?;
|
||||
if !out.status.success() {
|
||||
return Err(format!("recipe: {}", String::from_utf8_lossy(&out.stderr).trim()));
|
||||
}
|
||||
serde_json::from_slice(&out.stdout).map_err(|e| e.to_string())?
|
||||
}
|
||||
Source::File(path) => {
|
||||
let v: Value = serde_json::from_slice(&std::fs::read(path).map_err(|e| e.to_string())?).map_err(|e| e.to_string())?;
|
||||
if v.get("features").is_some() {
|
||||
s.execute("doc.open", &json!({"path": path.to_string_lossy()})).map_err(|e| e.to_string())?;
|
||||
return Ok(s);
|
||||
}
|
||||
match v.get("steps").and_then(Value::as_array) {
|
||||
// The demo's steps: an MCP `execute` each.
|
||||
Some(steps) => {
|
||||
json!({"commands": steps.iter().map(|st| json!({"command": st["command"], "params": st["params"]})).collect::<Vec<_>>()})
|
||||
}
|
||||
None => v,
|
||||
}
|
||||
}
|
||||
};
|
||||
s.run_script(&script).map_err(|e| e.to_string())?;
|
||||
if s.active_sketch.is_some() {
|
||||
let _ = s.execute("sketch.finish", &json!({}));
|
||||
}
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
/// What decides a name beyond its text: `[geometric]` when it was worked out from geometry, not
|
||||
/// exact history.
|
||||
fn flags(list: &[(bool, &str)]) -> String {
|
||||
let set: Vec<&str> = list.iter().filter(|(on, _)| *on).map(|(_, f)| *f).collect();
|
||||
if set.is_empty() { String::new() } else { format!(" [{}]", set.join(", ")) }
|
||||
}
|
||||
|
||||
/// Where each named face and edge of a body is, by name, sorted: a face's area and centre, an
|
||||
/// edge's length and middle, from the body's mesh. Two builds that name their faces alike give
|
||||
/// the same list.
|
||||
fn placed_names(b: &ModelBody) -> Vec<(String, [f64; 4])> {
|
||||
if b.body.is_mesh() {
|
||||
return Vec::new();
|
||||
}
|
||||
let m = b.mesh();
|
||||
let faces = naming::face_names(b);
|
||||
let mut sums = vec![[0.0f64; 4]; faces.len()];
|
||||
for (t, f) in m.triangles.iter().zip(&m.tri_face) {
|
||||
let [p, q, r] = t.map(|i| m.positions.get(i as usize).copied().unwrap_or(Vec3::ZERO));
|
||||
let area = (q - p).cross(r - p).len() / 2.0;
|
||||
let c = (p + q + r) / 3.0;
|
||||
if let Some(s) = sums.get_mut(*f as usize) {
|
||||
*s = [s[0] + area, s[1] + area * c.x, s[2] + area * c.y, s[3] + area * c.z];
|
||||
}
|
||||
}
|
||||
let mut out: Vec<(String, [f64; 4])> = faces
|
||||
.iter()
|
||||
.zip(&sums)
|
||||
.map(|(n, s)| {
|
||||
let a = s[0].max(1e-300);
|
||||
(format!("{} face {n}", b.name), [s[0], s[1] / a, s[2] / a, s[3] / a])
|
||||
})
|
||||
.collect();
|
||||
for (n, line) in naming::edge_names(b).iter().zip(&m.edges) {
|
||||
let length: f64 = line.windows(2).map(|w| w[1].dist(w[0])).sum();
|
||||
let mid = line.iter().fold(Vec3::ZERO, |acc, p| acc + *p) / (line.len().max(1) as f64);
|
||||
out.push((format!("{} edge {n}", b.name), [length, mid.x, mid.y, mid.z]));
|
||||
}
|
||||
out.sort_by(|x, y| x.0.cmp(&y.0).then(x.1.partial_cmp(&y.1).unwrap_or(std::cmp::Ordering::Equal)));
|
||||
out
|
||||
}
|
||||
|
||||
fn face_label(st: &ModelState, body: usize, face: usize) -> String {
|
||||
match st.bodies.get(body) {
|
||||
Some(b) => format!("{} face {}", b.name, naming::face_names(b).get(face).map(String::as_str).unwrap_or("?")),
|
||||
None => format!("body #{body} face #{face}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Every name feature `f` stores, with what it finds in the model before it, as a feature
|
||||
/// rebuilding finds it, and whether it finds anything.
|
||||
fn record_refs(out: &mut Vec<(String, bool)>, s: &Session, f: &Feature) {
|
||||
let doc = &*s.doc;
|
||||
let who = format!("ref {} ({})", f.name, f.id);
|
||||
let before = s.model.state_before(f.id);
|
||||
// Faces a feature picks by point and stores by name (shell, draft, hole, …).
|
||||
let explicit_emboss = matches!(&f.kind, FeatureKind::Emboss { face: Some(_), .. });
|
||||
if f.face_names.iter().any(|n| !n.is_empty()) {
|
||||
let fst = solidcraft_engine::doc::face_point_state(doc, &before, &f.kind, f.component).unwrap();
|
||||
for (k, (p, n)) in f.kind.face_points().into_iter().zip(&f.face_names).enumerate() {
|
||||
if n.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let (found, ok) = if explicit_emboss {
|
||||
("not checked (a cylindrical emboss finds its face on its own body)".to_string(), true)
|
||||
} else {
|
||||
match naming::locate_face_on_body(&fst, f.kind.face_body(), n, p) {
|
||||
Some(hit) => (format!("{}{}", face_label(&fst, hit.body, hit.face), flags(&[(!hit.verified, "geometric")])), true),
|
||||
None => ("gone".into(), false),
|
||||
}
|
||||
};
|
||||
out.push((format!("{who} face {k} `{n}` -> {found}"), ok));
|
||||
}
|
||||
}
|
||||
// Edges a feature picks by point and stores by name.
|
||||
if f.edge_names.iter().any(|n| !n.is_empty()) {
|
||||
let edge = |out: &mut Vec<(String, bool)>, b: &ModelBody, k: usize, n: &str, p: Vec3| {
|
||||
let (found, ok) = match naming::locate_edge(b, n, p) {
|
||||
Ok(hit) => (
|
||||
format!(
|
||||
"{} edge {}{}",
|
||||
b.name,
|
||||
naming::edge_names(b).get(hit.edge).map(String::as_str).unwrap_or("?"),
|
||||
flags(&[(!hit.verified, "geometric")])
|
||||
),
|
||||
true,
|
||||
),
|
||||
Err(e) => (format!("gone ({e})"), false),
|
||||
};
|
||||
out.push((format!("{who} edge {k} `{n}` -> {found}"), ok));
|
||||
};
|
||||
let named = |k: usize| f.edge_names.get(k).filter(|n| !n.is_empty());
|
||||
match &f.kind {
|
||||
FeatureKind::Fillet { edges, body, .. } | FeatureKind::Chamfer { edges, body, .. } => {
|
||||
let pts: Vec<Vec3> = solidcraft_engine::doc::plain(edges);
|
||||
match solidcraft_engine::doc::blend_target(&before, body, &pts).ok().and_then(|i| before.bodies.get(i)) {
|
||||
Some(b) => {
|
||||
for (k, p) in pts.iter().enumerate() {
|
||||
if let Some(n) = named(k) {
|
||||
edge(out, b, k, n, *p);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => out.push((format!("{who} edges: no body to round"), false)),
|
||||
}
|
||||
}
|
||||
FeatureKind::SheetConvert { body, bends, .. } => match before.body(body) {
|
||||
Some(b) => {
|
||||
for (k, p) in solidcraft_engine::doc::plain::<Vec3, _>(bends).iter().enumerate() {
|
||||
if let Some(n) = named(k) {
|
||||
edge(out, b, k, n, *p);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => out.push((format!("{who} bends: body `{body}` gone"), false)),
|
||||
},
|
||||
// Sheet metal edges are named by the sheet's panels, and found on its sheets.
|
||||
FeatureKind::SheetFlange { .. } | FeatureKind::SheetHem { .. } => {
|
||||
let pts: Vec<Vec3> = match &f.kind {
|
||||
FeatureKind::SheetFlange { edges, .. } => edges.iter().map(|e| e.point.0).collect(),
|
||||
FeatureKind::SheetHem { edges, .. } => solidcraft_engine::doc::plain(edges),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
for (k, p) in pts.iter().enumerate() {
|
||||
let Some(n) = named(k) else { continue };
|
||||
let on: Vec<&str> = before.sheets.iter().filter(|sh| sh.edge_by_name(n, *p).is_some()).map(|sh| sh.body.as_str()).collect();
|
||||
let found = if on.is_empty() { "gone".to_string() } else { format!("sheet edge on {}", on.join(", ")) };
|
||||
out.push((format!("{who} sheet edge {k} `{n}` -> {found}"), !on.is_empty()));
|
||||
}
|
||||
}
|
||||
other => {
|
||||
for (k, n) in f.edge_names.iter().enumerate().filter(|(_, n)| !n.is_empty()) {
|
||||
out.push((format!("{who} edge {k} `{n}` -> not checked ({})", other.type_name()), true));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// The face a sketch sits on.
|
||||
if let FeatureKind::Sketch { plane: PlaneRef::Face { name, anchor, at, .. }, .. } = &f.kind {
|
||||
let mut ok = true;
|
||||
let status = match doc.sketch_face_attachment(f.id, &s.model) {
|
||||
FaceAttachmentStatus::Healthy { target, .. } => {
|
||||
let faces = match before.body(&target.body) {
|
||||
Some(b) => match doc.anchored_face_indices(&before, b, &target) {
|
||||
Ok((indices, note)) => {
|
||||
let names = naming::face_names(b);
|
||||
let mut list: Vec<&str> = indices.iter().map(|i| names.get(*i).map(String::as_str).unwrap_or("?")).collect();
|
||||
list.sort();
|
||||
format!("{} faces {}{}", b.name, list.join(", "), flags(&[(note.is_some(), "changed cap")]))
|
||||
}
|
||||
Err(e) => {
|
||||
ok = false;
|
||||
format!("no faces ({e})")
|
||||
}
|
||||
},
|
||||
None => {
|
||||
ok = false;
|
||||
format!("body `{}` gone", target.body)
|
||||
}
|
||||
};
|
||||
format!("healthy: {faces}")
|
||||
}
|
||||
FaceAttachmentStatus::Lost { message, .. } => {
|
||||
ok = false;
|
||||
format!("lost: {message}")
|
||||
}
|
||||
FaceAttachmentStatus::Legacy { .. } => match name {
|
||||
// The face its name finds, as the sketch finds it.
|
||||
Some(n) => {
|
||||
let fst = solidcraft_engine::doc::state_in_frame(doc, &before, f.component).unwrap();
|
||||
match naming::locate_face(&fst, n, *at) {
|
||||
Some(hit) => format!("by name: {}{}", face_label(&fst, hit.body, hit.face), flags(&[(!hit.verified, "geometric")])),
|
||||
None => {
|
||||
ok = false;
|
||||
"by name: gone".into()
|
||||
}
|
||||
}
|
||||
}
|
||||
None => "by its pick".into(),
|
||||
},
|
||||
FaceAttachmentStatus::Sheet { .. } => "sheet face".into(),
|
||||
FaceAttachmentStatus::Fixed { .. } => "fixed".into(),
|
||||
};
|
||||
let stored = match (anchor, name) {
|
||||
(Some(a), _) => {
|
||||
let kind =
|
||||
a.target.certificate.as_ref().and_then(|c| serde_json::to_value(c).ok()).and_then(|v| v["kind"].as_str().map(String::from));
|
||||
format!("`{}` on {} ({})", a.target.name, a.target.body, kind.unwrap_or_else(|| "no certificate".into()))
|
||||
}
|
||||
(None, Some(n)) => format!("`{n}`"),
|
||||
(None, None) => "by its pick".into(),
|
||||
};
|
||||
out.push((format!("{who} plane {stored} -> {status}"), ok));
|
||||
}
|
||||
}
|
||||
|
||||
/// A build of a design: where its faces and edges are by name, and what every name it stores
|
||||
/// finds.
|
||||
struct Build {
|
||||
names: Vec<(String, [f64; 4])>,
|
||||
refs: Vec<(String, bool)>,
|
||||
}
|
||||
|
||||
fn build(s: &Session) -> Build {
|
||||
let names = s.model.state().bodies.iter().flat_map(placed_names).collect();
|
||||
let mut refs = Vec::new();
|
||||
for f in s.doc.features.iter().filter(|f| !f.suppressed) {
|
||||
let from = refs.len();
|
||||
record_refs(&mut refs, s, f);
|
||||
// A name that finds nothing is sound when its feature fails: it asks for the reference
|
||||
// to be picked again.
|
||||
if s.model.result(f.id).is_some_and(|r| r.error.is_some()) {
|
||||
refs.iter_mut().skip(from).for_each(|(_, ok)| *ok = true);
|
||||
}
|
||||
}
|
||||
Build { names, refs }
|
||||
}
|
||||
|
||||
/// Where two builds name their faces and edges apart (at most `cap` of them): a name only one
|
||||
/// has, or a face or edge of that name somewhere else (1e-6 of its size, to the mesh's rounding).
|
||||
fn differences(a: &Build, b: &Build, cap: usize) -> Vec<String> {
|
||||
let mut out = Vec::new();
|
||||
if a.names.len() != b.names.len() {
|
||||
out.push(format!("{} named faces and edges, then {}", a.names.len(), b.names.len()));
|
||||
}
|
||||
for ((n, x), (m, y)) in a.names.iter().zip(&b.names) {
|
||||
let scale = x.iter().chain(y).fold(1.0f64, |s, v| s.max(v.abs()));
|
||||
if n != m || x.iter().zip(y).any(|(p, q)| (p - q).abs() > 1e-6 * scale) {
|
||||
out.push(format!("{n} at {x:?}, then {m} at {y:?}"));
|
||||
}
|
||||
}
|
||||
for ((r, _), (q, _)) in a.refs.iter().zip(&b.refs) {
|
||||
if r != q {
|
||||
out.push(format!("{r}, then {q}"));
|
||||
}
|
||||
}
|
||||
if a.refs.len() != b.refs.len() {
|
||||
out.push(format!("{} stored names, then {}", a.refs.len(), b.refs.len()));
|
||||
}
|
||||
out.truncate(cap);
|
||||
out
|
||||
}
|
||||
|
||||
/// Designs whose names differ from one build to the next today: (design, why). Their
|
||||
/// differences are printed, not failed on, until the cause is fixed; a stored name that finds
|
||||
/// nothing still fails.
|
||||
const UNSTABLE: [(&str, &str); 1] = [(
|
||||
"regressions/closed_sweep_torus_cut",
|
||||
"the halves of each swept wall lie level along every axis, so which is `#1` and which `#2` follows the order the kernel gives the faces in, which changes from one build to the next",
|
||||
)];
|
||||
|
||||
#[test]
|
||||
fn corpus_designs_name_alike_every_build_and_fail_where_a_reference_is_gone() {
|
||||
let designs = designs();
|
||||
let next = std::sync::atomic::AtomicUsize::new(0);
|
||||
let results = std::sync::Mutex::new(Vec::new());
|
||||
let workers = std::thread::available_parallelism().map_or(4, |n| n.get()).min(designs.len().max(1));
|
||||
std::thread::scope(|scope| {
|
||||
for _ in 0..workers {
|
||||
scope.spawn(|| {
|
||||
while let Some((name, source)) = designs.get(next.fetch_add(1, std::sync::atomic::Ordering::Relaxed)) {
|
||||
let got = load(source).and_then(|first| Ok((build(&first), build(&load(source)?))));
|
||||
results.lock().unwrap_or_else(|e| e.into_inner()).push((name.clone(), got));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
let mut results = results.into_inner().unwrap_or_else(|e| e.into_inner());
|
||||
results.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
let mut problems = Vec::new();
|
||||
for (name, got) in &results {
|
||||
let (first, second) = match got {
|
||||
Ok(builds) => builds,
|
||||
Err(e) => {
|
||||
problems.push(format!("{name}: does not load: {e}"));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let lost: Vec<&str> = first.refs.iter().filter(|(_, ok)| !ok).map(|(r, _)| r.as_str()).collect();
|
||||
if !lost.is_empty() {
|
||||
problems.push(format!("{name}: stored names that find nothing, in features that build:\n {}", lost.join("\n ")));
|
||||
}
|
||||
let apart = differences(first, second, 40);
|
||||
if !apart.is_empty() {
|
||||
match UNSTABLE.iter().find(|(design, _)| design == name) {
|
||||
Some((_, why)) => eprintln!("{name} (not checked: {why}):\n {}", apart.join("\n ")),
|
||||
None => problems.push(format!("{name}: two builds name it apart (first, then second):\n {}", apart.join("\n "))),
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(problems.is_empty(), "{} of {} design(s) of the corpus:\n{}", problems.len(), results.len(), problems.join("\n"));
|
||||
}
|
||||
@@ -31,7 +31,7 @@ struct Topology {
|
||||
|
||||
/// Every solid body's topology, by name.
|
||||
fn topology(s: &Session) -> BTreeMap<String, Topology> {
|
||||
let st = s.world_state().unwrap();
|
||||
let st = s.world_state();
|
||||
st.bodies
|
||||
.iter()
|
||||
.filter(|b| !b.body.is_mesh())
|
||||
@@ -139,14 +139,16 @@ fn a_parameter_edit_rebuilds_like_a_fresh_build_and_reverts_exactly() {
|
||||
assert!(problems.is_empty(), "{}", problems.join("\n"));
|
||||
}
|
||||
|
||||
/// The README demo's Raspberry Pi case (`examples/demo/enclosure_ui_mcp.py --story pi`): after
|
||||
/// each feature, each outer side (the four walls and the bottom) is one face and the case is as
|
||||
/// tall as its height, and a height changed in Parameters rebuilds the same topology as one
|
||||
/// built at that height: (a) built at height 24, (b) built at 24 and changed to 32, (c) built at
|
||||
/// 32. The port cut-outs and the floor vents used to split the long wall (or the end wall at 24)
|
||||
/// into strips along the openings' tops and bottoms.
|
||||
/// The README demo's Raspberry Pi case (`examples/demo/enclosure_ui_mcp.py --story pi`): its
|
||||
/// face count after each feature, and each outer side (the four walls and the bottom) one face
|
||||
/// throughout: (a) built at height 24, (b) built at 24 and changed to 32 in Parameters, (c)
|
||||
/// built at 32. The port cut-outs and the floor vents used to split the long wall (or the end
|
||||
/// wall at 24) into strips along the openings' tops and bottoms.
|
||||
#[test]
|
||||
fn pi_case_sides_stay_whole_at_both_heights() {
|
||||
fn pi_case_faces_per_step_at_both_heights() {
|
||||
// Sketch1 (1) .. RectangularPattern2 (14): the faces once features 1..=k are built.
|
||||
const FACES: [(usize, usize); 13] =
|
||||
[(2, 6), (3, 10), (4, 19), (5, 19), (6, 25), (7, 43), (8, 43), (9, 55), (10, 55), (11, 59), (12, 59), (13, 63), (14, 79)];
|
||||
let dir = scratch_dir("pi");
|
||||
let design: Value = serde_json::from_slice(&std::fs::read(root().join("tests/action_matrix/designs/pi-case.solidcraft")).unwrap()).unwrap();
|
||||
let at = |height: &str, file: &str| {
|
||||
@@ -169,13 +171,12 @@ fn pi_case_sides_stay_whole_at_both_heights() {
|
||||
for (case, s, h) in [("(a) at 24", a, 24.), ("(b) 24 changed to 32", b, 32.), ("(c) at 32", c, 32.)] {
|
||||
assert!(errors(&s).is_empty(), "{case}: features fail: {:?}", errors(&s));
|
||||
let mut steps = Vec::new();
|
||||
// Sketch1 (1) is no body; the box (2) .. RectangularPattern2 (14).
|
||||
for k in 2..=14 {
|
||||
for (k, want) in FACES {
|
||||
let mut t = s.scratch();
|
||||
t.execute("timeline.rollback", &json!({"position": k})).unwrap();
|
||||
let st = t.world_state().unwrap();
|
||||
let st = t.world_state();
|
||||
let body = st.bodies.iter().find(|b| b.name == "Body1").unwrap();
|
||||
assert!(body.body.validity().is_empty(), "{case}: after feature {k}: {:?}", body.body.validity());
|
||||
assert_eq!(body.body.face_count(), want, "{case}: faces after feature {k}");
|
||||
// Each outer side: the faces in its plane.
|
||||
for (n, d) in [([0., -1., 0.], 33.), ([0., 1., 0.], 33.), ([-1., 0., 0.], 47.5), ([1., 0., 0.], 47.5), ([0., 0., -1.], 0.)] {
|
||||
let on = (0..body.body.face_count())
|
||||
|
||||
@@ -22,19 +22,6 @@ impl Scratch {
|
||||
std::fs::write(&path, json!({"commands": [{"command": "solid.box", "params": {"length": 2, "width": 3, "height": 4}}]}).to_string()).unwrap();
|
||||
path
|
||||
}
|
||||
|
||||
fn saving_script(&self) -> (PathBuf, PathBuf) {
|
||||
let path = self.0.join("saving.json");
|
||||
let saved = self.0.join("script-ran.solidcraft");
|
||||
std::fs::write(
|
||||
&path,
|
||||
json!({"commands": [{"command": "solid.box", "params": {"length": 2, "width": 3, "height": 4}},
|
||||
{"command": "file.save_as", "params": {"path": saved}}]})
|
||||
.to_string(),
|
||||
)
|
||||
.unwrap();
|
||||
(path, saved)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Scratch {
|
||||
@@ -56,92 +43,6 @@ fn body_count(path: &Path) -> Value {
|
||||
json_output(cli().arg("eval").arg(path).output().unwrap())["body_count"].clone()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_rejects_invalid_options_before_script_execution_or_output_writes() {
|
||||
let dir = Scratch::new();
|
||||
let (script, script_saved) = dir.saving_script();
|
||||
let cases = [
|
||||
(&["--out"][..], "--out needs a value"),
|
||||
(&["--save"][..], "--save needs a value"),
|
||||
(&["--in"][..], "--in needs a value"),
|
||||
(&["--out", "--quiet"][..], "--out needs a value"),
|
||||
(&["--ouut", "part.step"][..], "doesn't take --ouut"),
|
||||
(&["--unknown"][..], "doesn't take --unknown"),
|
||||
];
|
||||
for existing in [false, true] {
|
||||
let saved = dir.0.join(format!("{existing}.solidcraft"));
|
||||
let exported = dir.0.join(format!("{existing}.step"));
|
||||
let report = dir.0.join(format!("{existing}.json"));
|
||||
let outputs = [&saved, &exported, &report];
|
||||
if existing {
|
||||
for path in outputs {
|
||||
std::fs::write(path, b"existing output").unwrap();
|
||||
}
|
||||
}
|
||||
for (extra, message) in cases {
|
||||
let output = cli()
|
||||
.arg("run")
|
||||
.arg(&script)
|
||||
.arg("--save")
|
||||
.arg(&saved)
|
||||
.arg("--out")
|
||||
.arg(&exported)
|
||||
.arg("--json-out")
|
||||
.arg(&report)
|
||||
.args(extra)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert_eq!(output.status.code(), Some(1), "{extra:?}");
|
||||
assert!(String::from_utf8_lossy(&output.stderr).contains(message), "{extra:?}: {}", String::from_utf8_lossy(&output.stderr));
|
||||
assert!(output.stdout.is_empty());
|
||||
assert!(!script_saved.exists(), "invalid arguments must be rejected before the script runs");
|
||||
for path in outputs {
|
||||
if existing {
|
||||
assert_eq!(std::fs::read(path).unwrap(), b"existing output");
|
||||
} else {
|
||||
assert!(!path.exists());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_and_inspect_reject_invalid_options_before_script_execution_or_reporting() {
|
||||
let dir = Scratch::new();
|
||||
let (script, script_saved) = dir.saving_script();
|
||||
let cases = [
|
||||
(&["--in"][..], "--in needs a value"),
|
||||
(&["--in", "--quiet"][..], "--in needs a value"),
|
||||
(&["--out", "part.step"][..], "doesn't take --out"),
|
||||
(&["--unknown"][..], "doesn't take --unknown"),
|
||||
];
|
||||
for command in ["eval", "inspect"] {
|
||||
for existing in [false, true] {
|
||||
let report = dir.0.join(format!("{command}-{existing}.json"));
|
||||
if existing {
|
||||
std::fs::write(&report, b"existing report").unwrap();
|
||||
}
|
||||
for (extra, message) in cases {
|
||||
let output = cli().arg(command).arg(&script).arg("--json-out").arg(&report).args(extra).output().unwrap();
|
||||
assert_eq!(output.status.code(), Some(1), "{command} {extra:?}");
|
||||
assert!(
|
||||
String::from_utf8_lossy(&output.stderr).contains(message),
|
||||
"{command} {extra:?}: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
assert!(output.stdout.is_empty());
|
||||
assert!(!script_saved.exists(), "invalid arguments must be rejected before the script runs");
|
||||
if existing {
|
||||
assert_eq!(std::fs::read(&report).unwrap(), b"existing report");
|
||||
} else {
|
||||
assert!(!report.exists());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_loads_both_design_encodings_by_contents() {
|
||||
let dir = Scratch::new();
|
||||
@@ -158,84 +59,6 @@ fn cli_loads_both_design_encodings_by_contents() {
|
||||
}
|
||||
}
|
||||
|
||||
/// #1369: a pending concept that did not land in the numbered release must not be silently
|
||||
/// dropped on open/resave, in either encoding or before replacing an existing destination.
|
||||
#[test]
|
||||
fn unknown_pending_concepts_preserve_the_open_design_and_output_files() {
|
||||
use solidcraft_engine::{Session, doc, io};
|
||||
|
||||
let dir = Scratch::new();
|
||||
for version in [33, doc::format::FORMAT] {
|
||||
let mut experimental = doc::Document::new("Experimental");
|
||||
experimental.format = doc::format::format_string(version);
|
||||
experimental.format_pending.push("experimental-card".into());
|
||||
for (extension, bytes) in [("json", io::write_design(&experimental)), ("solidcraft", io::write_design_container(&experimental).unwrap())] {
|
||||
let source = dir.0.join(format!("experimental-{version}.{extension}"));
|
||||
std::fs::write(&source, &bytes).unwrap();
|
||||
let mut session = Session::default();
|
||||
session.execute("solid.box", &json!({"length": 2, "width": 3, "height": 4})).unwrap();
|
||||
let original = session.doc.clone();
|
||||
let original_path = session.path.clone();
|
||||
let undo = session.undo.len();
|
||||
let dirty = session.is_dirty();
|
||||
let err = session.execute("doc.open", &json!({"path": source})).unwrap_err().to_string();
|
||||
assert!(err.contains("experimental-card") && err.contains("update SolidCraft"), "{err}");
|
||||
assert_eq!(session.doc, original);
|
||||
assert_eq!(session.path, original_path);
|
||||
assert_eq!(session.undo.len(), undo);
|
||||
assert_eq!(session.is_dirty(), dirty);
|
||||
|
||||
for existing in [false, true] {
|
||||
let destination = dir.0.join(format!("output-{version}-{extension}-{existing}.{extension}"));
|
||||
let backup = io::backup_path(&destination);
|
||||
if existing {
|
||||
std::fs::write(&destination, b"existing design").unwrap();
|
||||
std::fs::write(&backup, b"existing backup").unwrap();
|
||||
}
|
||||
let output = cli().args(["run", "--in"]).arg(&source).arg("--save").arg(&destination).output().unwrap();
|
||||
assert_eq!(output.status.code(), Some(1));
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(error.contains("experimental-card") && error.contains("update SolidCraft"), "{error}");
|
||||
if existing {
|
||||
assert_eq!(std::fs::read(&destination).unwrap(), b"existing design");
|
||||
assert_eq!(std::fs::read(&backup).unwrap(), b"existing backup");
|
||||
} else {
|
||||
assert!(!destination.exists() && !backup.exists());
|
||||
}
|
||||
assert_eq!(std::fs::read(&source).unwrap(), bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_pending_concepts_and_released_files_round_trip_with_inert_metadata() {
|
||||
use solidcraft_engine::{doc, io};
|
||||
|
||||
let dir = Scratch::new();
|
||||
let current = doc::Document::new("Supported");
|
||||
let mut released = current.clone();
|
||||
released.format = doc::format::format_string(33);
|
||||
released.format_pending.clear();
|
||||
let mut experimental = released.clone();
|
||||
experimental.format_pending.push("reserved-body-names".into());
|
||||
for (index, mut document) in [current, released, experimental].into_iter().enumerate() {
|
||||
document.metadata.0.insert("com.example.tool".into(), json!({"format_pending": ["experimental-card"], "physical_card": {"density": 700}}));
|
||||
for (extension, bytes) in [("json", io::write_design(&document)), ("solidcraft", io::write_design_container(&document).unwrap())] {
|
||||
let source = dir.0.join(format!("supported-{index}.{extension}"));
|
||||
let destination = dir.0.join(format!("resaved-{index}.{extension}"));
|
||||
std::fs::write(&source, &bytes).unwrap();
|
||||
let output = cli().args(["run", "--in"]).arg(&source).arg("--save").arg(&destination).output().unwrap();
|
||||
assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr));
|
||||
let reopened = io::read_design(&std::fs::read(&destination).unwrap()).unwrap();
|
||||
assert_eq!(reopened.metadata, document.metadata);
|
||||
assert_eq!(reopened.name, document.name);
|
||||
assert_eq!(reopened.features, document.features);
|
||||
assert_eq!(std::fs::read(&source).unwrap(), bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn results_still_save_and_export_the_script_once() {
|
||||
let dir = Scratch::new();
|
||||
@@ -267,110 +90,6 @@ fn results_with_base_design_and_quiet_still_report_and_save() {
|
||||
assert_eq!(body_count(&base), 1);
|
||||
}
|
||||
|
||||
/// `eval` and its alias must measure the script applied to the input design, rather than
|
||||
/// silently reporting the input alone (#6861).
|
||||
#[test]
|
||||
fn eval_runs_its_script_on_the_in_design() {
|
||||
let dir = Scratch::new();
|
||||
let base = dir.0.join("base.solidcraft");
|
||||
let script = dir.0.join("add.json");
|
||||
json_output(cli().arg("run").arg(dir.script()).arg("--save").arg(&base).output().unwrap());
|
||||
let original = std::fs::read(&base).unwrap();
|
||||
std::fs::write(
|
||||
&script,
|
||||
json!({"commands": [{"command": "solid.box", "params": {"length": 5, "width": 3, "height": 4, "corner": [10, 0, 0]}}]}).to_string(),
|
||||
)
|
||||
.unwrap();
|
||||
for command in ["eval", "inspect"] {
|
||||
// Both existing one-input forms still measure just the original body.
|
||||
for input in [&base, &dir.script()] {
|
||||
for named_input in [false, true] {
|
||||
let mut c = cli();
|
||||
c.arg(command);
|
||||
if named_input {
|
||||
c.arg("--in");
|
||||
}
|
||||
let measured = json_output(c.arg(input).output().unwrap());
|
||||
assert_eq!(measured["body_count"], 1);
|
||||
assert!((measured["total"]["volume_mm3"].as_f64().unwrap() - 24.0).abs() < 1e-6);
|
||||
}
|
||||
}
|
||||
for input_first in [false, true] {
|
||||
for to_file in [false, true] {
|
||||
let mut c = cli();
|
||||
c.arg(command);
|
||||
if input_first {
|
||||
c.arg("--in").arg(&base).arg(&script);
|
||||
} else {
|
||||
c.arg(&script).arg("--in").arg(&base);
|
||||
}
|
||||
let report = dir.0.join("measurement.json");
|
||||
if to_file {
|
||||
c.arg("--json-out").arg(&report);
|
||||
}
|
||||
let output = c.output().unwrap();
|
||||
let measured = if to_file {
|
||||
assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr));
|
||||
assert!(output.stdout.is_empty());
|
||||
serde_json::from_slice(&std::fs::read(&report).unwrap()).unwrap()
|
||||
} else {
|
||||
json_output(output)
|
||||
};
|
||||
assert_eq!(measured["body_count"], 2, "{command}, input_first={input_first}, to_file={to_file}");
|
||||
assert!((measured["total"]["volume_mm3"].as_f64().unwrap() - 84.0).abs() < 1e-6);
|
||||
assert_eq!(measured["timeline"].as_array().unwrap().len(), 2, "the script runs exactly once");
|
||||
assert_eq!(std::fs::read(&base).unwrap(), original, "evaluation never saves its changes to the input");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_propagates_script_errors_before_writing_a_report() {
|
||||
let dir = Scratch::new();
|
||||
let base = dir.script();
|
||||
let script = dir.0.join("bad.json");
|
||||
for command in ["eval", "inspect"] {
|
||||
for contents in ["{", r#"{"commands":[{"command":"missing.command","params":{}}]}"#] {
|
||||
std::fs::write(&script, contents).unwrap();
|
||||
let report = dir.0.join("report.json");
|
||||
let output = cli().arg(command).arg(&script).arg("--in").arg(&base).arg("--json-out").arg(&report).output().unwrap();
|
||||
assert_eq!(output.status.code(), Some(1));
|
||||
assert!(!output.stderr.is_empty());
|
||||
assert!(output.stdout.is_empty());
|
||||
assert!(!report.exists());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_rejects_unknown_options_and_missing_input_values_before_writing_a_report() {
|
||||
let dir = Scratch::new();
|
||||
let script = dir.script();
|
||||
let report = dir.0.join("report.json");
|
||||
for command in ["eval", "inspect"] {
|
||||
for (options, message) in [(&["--frobnicate", "1"][..], "doesn't take --frobnicate"), (&["--in"][..], "--in needs a value")] {
|
||||
for options_first in [false, true] {
|
||||
std::fs::write(&report, b"previous report").unwrap();
|
||||
let mut c = cli();
|
||||
c.arg(command);
|
||||
if options_first {
|
||||
c.args(options).arg("--json-out").arg(&report).arg(&script);
|
||||
} else {
|
||||
c.arg(&script).args(options).arg("--json-out").arg(&report);
|
||||
}
|
||||
// A following option also proves that --in cannot consume a flag as its value.
|
||||
let output = c.output().unwrap();
|
||||
assert_eq!(output.status.code(), Some(1), "{command}, {options:?}");
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(error.contains(message), "{command}: {error}");
|
||||
assert!(output.stdout.is_empty());
|
||||
assert_eq!(std::fs::read(&report).unwrap(), b"previous report");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn results_propagate_output_errors() {
|
||||
let dir = Scratch::new();
|
||||
@@ -490,175 +209,3 @@ fn timings_and_profile_report_each_command() {
|
||||
assert!(!output.status.success());
|
||||
assert!(String::from_utf8_lossy(&output.stderr).contains("--timings needs a script"));
|
||||
}
|
||||
|
||||
fn render_setup_script(dir: &Scratch, job: Value) -> PathBuf {
|
||||
let path = dir.0.join("render.json");
|
||||
std::fs::write(
|
||||
&path,
|
||||
json!({"commands":[
|
||||
{"command":"solid.box","params":{"length":10,"width":10,"height":10}},
|
||||
{"command":"render.settings.set","params":{"name":"Portable setup","set":{"width":16,"height":16,"job":job}}}
|
||||
]})
|
||||
.to_string(),
|
||||
)
|
||||
.unwrap();
|
||||
path
|
||||
}
|
||||
|
||||
#[cfg(feature = "raytrace-cpu")]
|
||||
#[test]
|
||||
fn render_uses_typed_cpu_setups_from_scripts_and_saved_designs() {
|
||||
for options in [json!({}), json!({"progressive":{"samples_per_pixel":3,"seed":81},"lights":[]})] {
|
||||
let dir = Scratch::new();
|
||||
let script = render_setup_script(&dir, json!({"method":"cpu_raytrace","options":options}));
|
||||
let image = dir.0.join("script.png");
|
||||
let result = json_output(
|
||||
cli()
|
||||
.arg("render")
|
||||
.arg(&script)
|
||||
.args(["--setup", "Portable setup", "--out"])
|
||||
.arg(&image)
|
||||
.env("SOLIDCRAFT_GPU", "must-not-be-read-for-cpu")
|
||||
.output()
|
||||
.unwrap(),
|
||||
);
|
||||
assert_eq!(result["preflight"]["resolved"]["executor"], "cpu-raytrace");
|
||||
assert_eq!(result["export"]["frame"]["executor"], "cpu-raytrace");
|
||||
assert_eq!(result["preflight"]["request"]["integrator"], "raytrace");
|
||||
assert_eq!(result["preflight"]["request"]["raytrace"].get("progressive"), options.get("progressive"));
|
||||
let png = image::load_from_memory(&std::fs::read(&image).unwrap()).unwrap();
|
||||
assert_eq!((png.width(), png.height()), (16, 16));
|
||||
|
||||
let saved = dir.0.join("portable.solidcraft");
|
||||
json_output(cli().arg("run").arg(&script).arg("--save").arg(&saved).output().unwrap());
|
||||
let original = std::fs::read(&saved).unwrap();
|
||||
let overridden = dir.0.join("overridden.png");
|
||||
let result = json_output(
|
||||
cli()
|
||||
.arg("render")
|
||||
.arg(&saved)
|
||||
.args(["--setup", "Portable setup", "--out"])
|
||||
.arg(&overridden)
|
||||
.args(["--width", "12", "--height", "8", "--exposure", "1.25"])
|
||||
.env("SOLIDCRAFT_GPU", "must-not-be-read-for-cpu")
|
||||
.output()
|
||||
.unwrap(),
|
||||
);
|
||||
assert_eq!(result["preflight"]["request"]["width"], 12);
|
||||
assert_eq!(result["preflight"]["request"]["height"], 8);
|
||||
assert_eq!(result["preflight"]["request"]["raytrace"]["exposure_ev"], 1.25);
|
||||
assert_eq!(result["export"]["frame"]["executor"], "cpu-raytrace");
|
||||
assert_eq!(result["preflight"]["request"]["raytrace"].get("progressive"), options.get("progressive"));
|
||||
let png = image::load_from_memory(&std::fs::read(overridden).unwrap()).unwrap();
|
||||
assert_eq!((png.width(), png.height()), (12, 8));
|
||||
assert_eq!(std::fs::read(saved).unwrap(), original, "CLI overrides must not rewrite saved intent");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_preserves_typed_raster_and_legacy_runtime_controls() {
|
||||
for job in [Value::Null, json!({"method":"raster","transparent_background":true})] {
|
||||
let dir = Scratch::new();
|
||||
let script = render_setup_script(&dir, job.clone());
|
||||
let out = dir.0.join("raster.png");
|
||||
let mut command = cli();
|
||||
command
|
||||
.arg("render")
|
||||
.arg(&script)
|
||||
.args(["--setup", "Portable setup", "--out"])
|
||||
.arg(&out)
|
||||
.env("SOLIDCRAFT_GPU", "must-not-be-read-for-raster");
|
||||
if job.is_null() {
|
||||
command.arg("--transparent");
|
||||
}
|
||||
let result = json_output(command.output().unwrap());
|
||||
assert_eq!(result["preflight"]["resolved"]["executor"], "cpu-product");
|
||||
assert_eq!(result["export"]["frame"]["executor"], "cpu-product");
|
||||
assert_eq!(result["preflight"]["request"]["transparent_background"], true);
|
||||
assert!(std::fs::read(out).unwrap().starts_with(b"\x89PNG\r\n\x1a\n"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typed_render_rejects_explicit_runtime_flags_before_gpu_or_option_file_reads() {
|
||||
let dir = Scratch::new();
|
||||
let script = render_setup_script(&dir, json!({"method":"gpu_path","options":{"samples":64,"seed":0}}));
|
||||
let out = dir.0.join("existing.png");
|
||||
std::fs::write(&out, b"previous completed image").unwrap();
|
||||
for flags in [
|
||||
&["--progressive"][..],
|
||||
&["--transparent"][..],
|
||||
&["--samples", "64"][..],
|
||||
&["--max-depth", "8"][..],
|
||||
&["--seed", "0"][..],
|
||||
&["--lighting", "deliberately-missing-lighting.json"][..],
|
||||
&["--tone-map", "reinhard"][..],
|
||||
&["--environment", "deliberately-missing-environment.json"][..],
|
||||
] {
|
||||
let output = cli()
|
||||
.arg("render")
|
||||
.arg(&script)
|
||||
.args(["--setup", "Portable setup", "--out"])
|
||||
.arg(&out)
|
||||
.arg("--overwrite")
|
||||
.args(flags)
|
||||
.env("SOLIDCRAFT_GPU", "must-not-be-read-for-conflicts")
|
||||
.output()
|
||||
.unwrap();
|
||||
assert_eq!(output.status.code(), Some(1));
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(error.contains(&format!("{} cannot accompany a typed saved render setup", flags[0])), "{error}");
|
||||
assert!(output.stdout.is_empty());
|
||||
assert_eq!(std::fs::read(&out).unwrap(), b"previous completed image");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typed_gpu_render_selects_its_host_without_a_progressive_runtime_flag() {
|
||||
// A deliberately invalid policy proves that dispatch reached the GPU host
|
||||
// without depending on an adapter or accepting a CPU fallback. The legacy
|
||||
// --progressive path must still reach that same host-selection boundary.
|
||||
for job in [json!({"method":"gpu_path","options":{"samples":2}}), Value::Null] {
|
||||
let dir = Scratch::new();
|
||||
let script = render_setup_script(&dir, job.clone());
|
||||
let out = dir.0.join("gpu.png");
|
||||
let mut command = cli();
|
||||
command.arg("render").arg(&script).args(["--setup", "Portable setup", "--out"]).arg(&out).env("SOLIDCRAFT_GPU", "not-a-gpu-policy");
|
||||
if job.is_null() {
|
||||
command.arg("--progressive");
|
||||
}
|
||||
let output = command.output().unwrap();
|
||||
assert_eq!(output.status.code(), Some(1));
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(error.contains("SOLIDCRAFT_GPU must be auto, hardware, software or cpu"), "{error}");
|
||||
assert!(!out.exists());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_setup_overrides_obey_shared_validation_before_host_initialization() {
|
||||
let dir = Scratch::new();
|
||||
let script = render_setup_script(&dir, json!({"method":"cpu_raytrace","options":{"progressive":{"samples_per_pixel":64}}}));
|
||||
let out = dir.0.join("invalid.png");
|
||||
// 1024 * 1024 * 64 exceeds the unchanged default Compact camera-sample
|
||||
// budget. Admission must name that envelope before initializing any host.
|
||||
for (flags, message) in [
|
||||
(&["--width", "1024", "--height", "1024"][..], "CPU Compact exceeds its 16777216 camera-sample admission"),
|
||||
(&["--width", "0"][..], "render output must be at least"),
|
||||
(&["--exposure", "NaN"][..], "render exposure must be finite"),
|
||||
] {
|
||||
let output = cli()
|
||||
.arg("render")
|
||||
.arg(&script)
|
||||
.args(["--setup", "Portable setup", "--out"])
|
||||
.arg(&out)
|
||||
.args(flags)
|
||||
.env("SOLIDCRAFT_GPU", "must-not-be-read-for-invalid-settings")
|
||||
.output()
|
||||
.unwrap();
|
||||
assert_eq!(output.status.code(), Some(1));
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(error.contains(message), "{error}");
|
||||
assert!(!out.exists());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,10 +65,7 @@ fn allow_errors_writes_anyway_and_a_saved_design_keeps_failing() {
|
||||
let output = cli().arg("run").arg(dir.broken_script()).arg("--allow-errors").arg("--out").arg(&step).arg("--save").arg(&saved).output().unwrap();
|
||||
assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr));
|
||||
let measured: Value = serde_json::from_slice(&output.stdout).unwrap();
|
||||
// SMLib integrates the volume (a relative accuracy of 1e-4 asked for; a box comes out within
|
||||
// a few units in the last place), so the box's 4000 mm³ is checked to 1e-9, not to the bit.
|
||||
let volume = measured["total"]["volume_mm3"].as_f64().unwrap();
|
||||
assert!((volume - 4000.0).abs() < 4000.0 * 1e-9, "the box without its fillet: {volume}");
|
||||
assert_eq!(measured["total"]["volume_mm3"], json!(4000.0), "the box without its fillet");
|
||||
assert!(std::fs::metadata(&step).unwrap().len() > 0);
|
||||
// The saved design still holds the failing feature: running it fails too, `eval` reports it.
|
||||
let again = dir.0.join("again.step");
|
||||
|
||||
@@ -171,9 +171,9 @@ fn a_saved_view_is_its_camera_and_a_standard_view_is_unchanged() {
|
||||
let top = dir.snapshot(&script, "top.png", &["--view", "top"]);
|
||||
let mut s = Session::default();
|
||||
s.run_script(&serde_json::from_slice(&std::fs::read(&script).unwrap()).unwrap()).unwrap();
|
||||
let mut cam = solidcraft_engine::view::home_camera(&s).unwrap();
|
||||
let mut cam = solidcraft_engine::view::home_camera(&s);
|
||||
cam.set_view(solidcraft_engine::render::StandardView::Top);
|
||||
let scene = solidcraft_engine::view::scene(&s, &cam).unwrap();
|
||||
let scene = solidcraft_engine::view::scene(&s, &cam);
|
||||
assert_same(&top, &solidcraft_engine::capture::png(&scene, &cam, SIZE.0, SIZE.1).unwrap().bytes, "the home camera turned to the top");
|
||||
assert_same(&top, &dir.mcp(&script, "mcp-top.png", json!({"view": "top"})), "the MCP top view");
|
||||
assert_differs(&top, &saved, "top and the saved view");
|
||||
@@ -335,9 +335,9 @@ fn a_forced_cpu_snapshot_is_the_cpu_rasterizers_and_says_so() {
|
||||
assert!(stderr.contains("CPU rasterizer"), "{stderr}");
|
||||
let mut s = Session::default();
|
||||
s.run_script(&serde_json::from_slice(&std::fs::read(&script).unwrap()).unwrap()).unwrap();
|
||||
let mut cam = solidcraft_engine::view::home_camera(&s).unwrap();
|
||||
let mut cam = solidcraft_engine::view::home_camera(&s);
|
||||
cam.set_view(solidcraft_engine::render::StandardView::Top);
|
||||
let scene = solidcraft_engine::view::scene(&s, &cam).unwrap();
|
||||
let scene = solidcraft_engine::view::scene(&s, &cam);
|
||||
let cpu = solidcraft_engine::render::render_png(&scene, &cam, SIZE.0 as usize, SIZE.1 as usize).unwrap();
|
||||
assert_eq!(std::fs::read(dir.0.join("cpu.png")).unwrap(), cpu);
|
||||
}
|
||||
|
||||
@@ -48,11 +48,11 @@ fn key(v: Vec3) -> [i64; 3] {
|
||||
fn straight_edges_are_one_edge_after_many_cuts() {
|
||||
for (what, s) in [("as saved", open(&design())), ("built from scratch", rebuilt())] {
|
||||
assert_eq!(errors(&s), vec![], "{what}: every feature evaluates");
|
||||
let st = s.world_state().unwrap();
|
||||
let st = s.world_state();
|
||||
let bodies: Vec<_> = st.bodies.iter().filter(|b| !b.body.is_mesh()).collect();
|
||||
assert_eq!(bodies.len(), 1, "{what}: one body");
|
||||
let body = &bodies[0].body;
|
||||
let mesh = body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.01)).unwrap();
|
||||
let mesh = body.tessellate(0.01).unwrap();
|
||||
// Every edge is drawn as one polyline; a straight one as one segment.
|
||||
let mut at: HashMap<[i64; 3], Vec<usize>> = HashMap::new();
|
||||
for (i, e) in mesh.edges.iter().enumerate() {
|
||||
|
||||
@@ -13,24 +13,11 @@ workspace = true
|
||||
name = "solidcraft-web"
|
||||
path = "src/main.rs"
|
||||
|
||||
[features]
|
||||
default = ["smlib", "profile"]
|
||||
bundled-render-library = ["solidcraft-engine/bundled-render-library"]
|
||||
# Link SMLib into the kernel (solidcraft-engine/smlib; needs scripts/fetch-smlib.sh
|
||||
# wasm32-unknown-unknown, or the sources and wasi-sdk in WASI_SDK_PATH, crates/smlib/build.rs). SMLib
|
||||
# is the only kernel, so without it the web app models nothing.
|
||||
smlib = ["solidcraft-engine/smlib"]
|
||||
# The profiler, compiled in so any build can say where its time goes (the Profile operations
|
||||
# preference, SOLIDCRAFT_PROFILE=1, diagnostics.profile); it records nothing until asked.
|
||||
profile = ["solidcraft-engine/profile"]
|
||||
|
||||
[dependencies]
|
||||
solidcraft-engine = { workspace = true }
|
||||
solidcraft-ui-egui = { workspace = true }
|
||||
egui = { workspace = true }
|
||||
log = { workspace = true }
|
||||
url = "2"
|
||||
percent-encoding = "2"
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit"] }
|
||||
@@ -43,6 +30,5 @@ web-sys = { version = "0.3", features = [
|
||||
"FileSystemHandle", "FileSystemDirectoryHandle", "FileSystemFileHandle", "FileSystemGetFileOptions", "FileSystemWritableFileStream",
|
||||
"WritableStream", "File", "FileList", "Blob", "BlobPropertyBag", "Url", "HtmlAnchorElement", "HtmlInputElement", "Storage",
|
||||
"IdbFactory", "IdbDatabase", "IdbObjectStore", "IdbOpenDbRequest", "IdbRequest", "IdbTransaction", "IdbTransactionMode",
|
||||
"Headers", "ReadableStream", "ReadableStreamDefaultReader",
|
||||
"BeforeUnloadEvent", "Event", "EventTarget", "KeyboardEvent", "Response", "FormData", "RequestInit", "MediaQueryList", "Gamepad", "Performance",
|
||||
"BeforeUnloadEvent", "Event", "EventTarget", "KeyboardEvent", "Response", "MediaQueryList", "Gamepad", "Performance",
|
||||
] }
|
||||
|
||||
@@ -114,46 +114,6 @@ pub fn opens_design(extensions: &[&str]) -> bool {
|
||||
extensions.is_empty() || extensions.iter().any(|e| solidcraft_engine::io::DESIGN_EXTS.contains(e))
|
||||
}
|
||||
|
||||
/// Where an Open for `extensions` (what the command reads; `&[]`: anything) is answered.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum OpenRoute {
|
||||
/// The browser's open dialog for a design: the file keeps its handle, so Save writes back.
|
||||
Disk,
|
||||
/// The "Files in this browser" window, where the browser can't open a design from the disk.
|
||||
Panel,
|
||||
/// A plain file input that offers these types: the file is read into the engine's files and
|
||||
/// the command runs on it (Insert DXF or SVG, images, meshes, imported settings).
|
||||
Upload,
|
||||
}
|
||||
|
||||
/// Choose how an Open for `extensions` asks for its file; `api`: the browser can open a file
|
||||
/// from the disk and keep its handle.
|
||||
pub fn open_route(api: bool, extensions: &[&str]) -> OpenRoute {
|
||||
match (opens_design(extensions), api) {
|
||||
(true, true) => OpenRoute::Disk,
|
||||
(true, false) => OpenRoute::Panel,
|
||||
(false, _) => OpenRoute::Upload,
|
||||
}
|
||||
}
|
||||
|
||||
/// The `accept` list of a file input for `extensions` (with or without their dot): ".dxf,.svg".
|
||||
pub fn accept_list(extensions: &[&str]) -> String {
|
||||
let list: Vec<String> = extensions
|
||||
.iter()
|
||||
.map(|e| e.trim().trim_start_matches('.'))
|
||||
.filter(|e| !e.is_empty())
|
||||
.map(|e| format!(".{}", e.to_ascii_lowercase()))
|
||||
.collect();
|
||||
list.join(",")
|
||||
}
|
||||
|
||||
/// The largest file an upload for `extensions` reads: images go into a canvas, which has a size
|
||||
/// limit, so a bigger one is refused before it is read. `None`: the importer checks the file.
|
||||
pub fn upload_limit(extensions: &[&str]) -> Option<usize> {
|
||||
let image = |e: &&str| ["png", "jpg", "jpeg"].contains(&e.trim_start_matches('.').to_ascii_lowercase().as_str());
|
||||
(!extensions.is_empty() && extensions.iter().all(image)).then_some(solidcraft_engine::doc::canvas::MAX_CANVAS_BYTES)
|
||||
}
|
||||
|
||||
/// The file type a Save As dialog offers for `name`: (description, MIME type, extension).
|
||||
pub fn save_type(name: &str) -> (&'static str, &'static str, &'static str) {
|
||||
let json = std::path::Path::new(name).extension().is_some_and(|e| e.eq_ignore_ascii_case("json"));
|
||||
@@ -180,7 +140,7 @@ mod web {
|
||||
use wasm_bindgen_futures::{JsFuture, spawn_local};
|
||||
use web_sys::{FileSystemFileHandle, IdbDatabase, IdbObjectStore, IdbOpenDbRequest, IdbRequest, IdbTransactionMode};
|
||||
|
||||
use super::{HANDLE_MAX_AGE_MS, is_abort, make_id, parse, path, plan_prune, save_type};
|
||||
use super::{HANDLE_MAX_AGE_MS, is_abort, make_id, opens_design, parse, path, plan_prune, save_type};
|
||||
use crate::files::{Files, err, read_handle};
|
||||
|
||||
const DB: &str = "solidcraft-disk-handles";
|
||||
@@ -334,7 +294,7 @@ mod web {
|
||||
let options = Object::new();
|
||||
let _ = Reflect::set(&options, &"multiple".into(), &JsValue::FALSE);
|
||||
let extensions = Array::new();
|
||||
for e in [".solidcraft", ".solvecraft", ".json", ".step", ".stp", ".3mf", ".stl", ".obj", ".glb", ".gltf", ".fcstd", ".3dm", ".dxf", ".svg"] {
|
||||
for e in [".solidcraft", ".solvecraft", ".json", ".step", ".stp", ".3mf", ".stl", ".obj", ".glb", ".gltf", ".fcstd", ".3dm"] {
|
||||
extensions.push(&e.into());
|
||||
}
|
||||
let accept = Object::new();
|
||||
@@ -355,6 +315,11 @@ mod web {
|
||||
p
|
||||
}
|
||||
|
||||
/// Does an Open for `extensions` use the browser's open dialog?
|
||||
pub fn handles_open(extensions: &[&str]) -> bool {
|
||||
supported() && opens_design(extensions)
|
||||
}
|
||||
|
||||
// ---- writing ------------------------------------------------------------------------
|
||||
|
||||
/// Queue what a command wrote to a disk path. Writes go one at a time; a newer write of the
|
||||
@@ -656,21 +621,6 @@ mod tests {
|
||||
assert!(!opens_design(&["json"]));
|
||||
}
|
||||
|
||||
/// Insert DXF or SVG, images and other non-design files get a file input that offers their
|
||||
/// types in every browser (#2052); designs keep the disk dialog or the browser's files.
|
||||
#[test]
|
||||
fn opens_of_other_files_use_a_file_input_for_their_types() {
|
||||
assert_eq!(open_route(true, &["dxf"]), OpenRoute::Upload);
|
||||
assert_eq!(open_route(false, &["png", "jpg", "jpeg"]), OpenRoute::Upload);
|
||||
assert_eq!(open_route(true, &[]), OpenRoute::Disk);
|
||||
assert_eq!(open_route(false, &["solidcraft"]), OpenRoute::Panel);
|
||||
assert_eq!(accept_list(&["dxf"]), ".dxf");
|
||||
assert_eq!(accept_list(&["png", ".JPG", "", "jpeg"]), ".png,.jpg,.jpeg");
|
||||
assert_eq!(accept_list(&[]), "");
|
||||
assert!(upload_limit(&["png", "jpg", "jpeg"]).is_some());
|
||||
assert_eq!(upload_limit(&["dxf"]), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_save_dialog_offers_the_type_of_the_name() {
|
||||
assert_eq!(save_type("Plate.solidcraft").2, ".solidcraft");
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
//! Open with EasyPrint in the browser: upload the model to Printables' import endpoint with
|
||||
//! `fetch` (it allows any origin) and answer with the address to open.
|
||||
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
use js_sys::{Array, Uint8Array};
|
||||
use solidcraft_engine::easyprint::{IMPORT_URL, parse_import_response};
|
||||
use wasm_bindgen::JsCast as _;
|
||||
use wasm_bindgen_futures::{JsFuture, spawn_local};
|
||||
|
||||
use crate::files::err;
|
||||
|
||||
/// Start the upload; the answer arrives on `done`.
|
||||
pub fn upload(name: &str, bytes: Vec<u8>, done: Sender<Result<String, String>>) {
|
||||
let name = name.to_string();
|
||||
spawn_local(async move {
|
||||
let _ = done.send(post(&name, &bytes).await);
|
||||
});
|
||||
}
|
||||
|
||||
async fn post(name: &str, bytes: &[u8]) -> Result<String, String> {
|
||||
let window = web_sys::window().ok_or("no window")?;
|
||||
let parts = Array::new();
|
||||
parts.push(&Uint8Array::from(bytes));
|
||||
let blob = web_sys::Blob::new_with_u8_array_sequence(&parts).map_err(err)?;
|
||||
let form = web_sys::FormData::new().map_err(err)?;
|
||||
form.append_with_blob_and_filename("file", &blob, name).map_err(err)?;
|
||||
form.append_with_str("file_name", name).map_err(err)?;
|
||||
let headers = web_sys::Headers::new().map_err(err)?;
|
||||
headers.set("Accept", "application/json").map_err(err)?;
|
||||
let init = web_sys::RequestInit::new();
|
||||
init.set_method("POST");
|
||||
init.set_body(&form);
|
||||
init.set_headers(&headers);
|
||||
let response = JsFuture::from(window.fetch_with_str_and_init(IMPORT_URL, &init)).await.map_err(err)?;
|
||||
let response: web_sys::Response = response.dyn_into().map_err(|_| "response cast failed".to_string())?;
|
||||
let status = response.status();
|
||||
let text = JsFuture::from(response.text().map_err(err)?).await.map_err(err)?;
|
||||
parse_import_response(status, text.as_string().unwrap_or_default().as_bytes())
|
||||
}
|
||||
@@ -448,18 +448,6 @@ pub fn recovery_path() -> String {
|
||||
format!("{OPFS}{RECOVERY}")
|
||||
}
|
||||
|
||||
/// A recovery file lives in OPFS, but its relative links belong to the original design's
|
||||
/// folder (which can be on disk). Resolve them before Open sees the temporary file location.
|
||||
pub fn prepare_recovery(original_path: Option<&str>) -> Result<String, String> {
|
||||
let path = recovery_path();
|
||||
let bytes = vfs::read(&path, solidcraft_engine::io::MAX_DESIGN_BYTES).map_err(|e| e.to_string())?;
|
||||
let mut doc = solidcraft_engine::io::read_design(&bytes).map_err(|e| e.to_string())?;
|
||||
solidcraft_engine::resolve_design_links(&mut doc, original_path);
|
||||
let bytes = solidcraft_engine::io::write_design_container(&doc).map_err(|e| e.to_string())?;
|
||||
vfs::insert(&path, bytes);
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// Pick files on the computer: the File System Access picker where there is one, else an
|
||||
/// `<input type=file>`. Read files arrive in `files.incoming`.
|
||||
pub fn pick_from_disk(files: &Rc<Files>, intent: Intent) {
|
||||
@@ -512,9 +500,8 @@ fn input_picker(files: &Rc<Files>, intent: Intent) {
|
||||
let Ok(input) = doc.create_element("input").map(|e| e.unchecked_into::<web_sys::HtmlInputElement>()) else { return };
|
||||
input.set_type("file");
|
||||
// Everything the browser build imports, from the format registry: cadmpeg is pure Rust, so
|
||||
// FreeCAD and Rhino files are part of it; plus the DXF and SVG drawings Open and Insert take.
|
||||
let accept =
|
||||
solidcraft_engine::io::formats::open_extensions(true).iter().chain(&["dxf", "svg"]).map(|e| format!(".{e}")).collect::<Vec<_>>().join(",");
|
||||
// FreeCAD and Rhino files are part of it.
|
||||
let accept = solidcraft_engine::io::formats::open_extensions(true).iter().map(|e| format!(".{e}")).collect::<Vec<_>>().join(",");
|
||||
input.set_accept(&accept);
|
||||
// Attached (hidden) while it is open: some browsers ignore a click on a detached input.
|
||||
let _ = input.set_attribute("style", "display:none");
|
||||
@@ -540,67 +527,6 @@ fn input_picker(files: &Rc<Files>, intent: Intent) {
|
||||
input.click();
|
||||
}
|
||||
|
||||
/// Ask for one file of these types (`extensions`, e.g. `["dxf"]`) with a plain file input, the
|
||||
/// dialog every browser has. The answer is the file's path in the engine's files (under
|
||||
/// [`UPLOAD`], as for a file picked in the disk dialog), so the command that asked runs on it;
|
||||
/// `None` when the dialog was closed or the file could not be read.
|
||||
pub fn pick_upload(files: &Rc<Files>, extensions: &[&str]) -> impl std::future::Future<Output = Option<String>> + use<> {
|
||||
// Shown now, in the click: a file dialog needs the user's gesture.
|
||||
let chosen = choose_file(&disk::accept_list(extensions));
|
||||
let limit = disk::upload_limit(extensions);
|
||||
let files = files.clone();
|
||||
async move {
|
||||
let read = async {
|
||||
let value = JsFuture::from(chosen?).await.map_err(err)?;
|
||||
// Closing the dialog answers no file: nothing to say.
|
||||
let Ok(file) = value.dyn_into::<web_sys::File>() else { return Ok(None) };
|
||||
let name = file.name();
|
||||
if limit.is_some_and(|max| !file.size().is_finite() || file.size() > max as f64) {
|
||||
return Err(format!("{name}: the file is too large"));
|
||||
}
|
||||
let bytes = blob_bytes(&file).await.map_err(|e| format!("{name}: {e}"))?;
|
||||
let path = format!("{UPLOAD}{name}");
|
||||
vfs::insert(&path, bytes);
|
||||
Ok(Some(path))
|
||||
};
|
||||
match read.await {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
files.say(format!("open: {e}"), true);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Show a hidden `<input type=file>` offering `accept` (call in the click). The promise answers
|
||||
/// the picked `File`, or `null` when the dialog is closed (the input's `cancel` event).
|
||||
fn choose_file(accept: &str) -> Result<js_sys::Promise, String> {
|
||||
let doc = web_sys::window().and_then(|w| w.document()).ok_or("this page can't show a file dialog")?;
|
||||
let input = doc.create_element("input").map_err(err)?.dyn_into::<web_sys::HtmlInputElement>().map_err(|_| "no file input".to_string())?;
|
||||
input.set_type("file");
|
||||
input.set_accept(accept);
|
||||
// Attached (hidden) while it is open: some browsers ignore a click on a detached input.
|
||||
input.set_attribute("style", "display:none").map_err(err)?;
|
||||
doc.body().ok_or("this page has no body")?.append_child(&input).map_err(err)?;
|
||||
let promise = js_sys::Promise::new(&mut |resolve: Function, _reject: Function| {
|
||||
// One answer for a pick or a cancel; read from the event, so it does not keep the input.
|
||||
let done = Closure::once_into_js(move |event: web_sys::Event| {
|
||||
let file = event.target().and_then(|t| t.dyn_into::<web_sys::HtmlInputElement>().ok()).and_then(|input| {
|
||||
input.set_onchange(None);
|
||||
input.set_oncancel(None);
|
||||
input.remove();
|
||||
input.files().and_then(|list| list.get(0))
|
||||
});
|
||||
let _ = resolve.call1(&JsValue::UNDEFINED, &file.map_or(JsValue::NULL, JsValue::from));
|
||||
});
|
||||
input.set_onchange(Some(done.unchecked_ref()));
|
||||
input.set_oncancel(Some(done.unchecked_ref()));
|
||||
});
|
||||
input.click();
|
||||
Ok(promise)
|
||||
}
|
||||
|
||||
/// Read files dropped on the page.
|
||||
pub fn take_drops(files: &Rc<Files>, raw: &mut egui::RawInput) {
|
||||
for f in std::mem::take(&mut raw.dropped_files) {
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
//! Bounded HTTPS or same-origin downloads, handed to the normal design-open command as unsaved copies.
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use js_sys::{Reflect, Uint8Array};
|
||||
use solidcraft_engine::io::vfs;
|
||||
use solidcraft_ui_egui::{SolveApp, ui_kit};
|
||||
use wasm_bindgen::{JsCast as _, JsValue};
|
||||
use wasm_bindgen_futures::{JsFuture, spawn_local};
|
||||
|
||||
use crate::{check_download_size, design_url};
|
||||
|
||||
type Reply = Result<(String, Vec<u8>), String>;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Download {
|
||||
reply: Rc<RefCell<Option<Reply>>>,
|
||||
pending: Option<(String, Vec<u8>)>,
|
||||
}
|
||||
|
||||
impl Download {
|
||||
pub fn start(url: Option<&str>, ctx: &egui::Context) -> Self {
|
||||
let download = Self::default();
|
||||
if let Some(url) = url {
|
||||
let url = url.to_owned();
|
||||
let slot = download.reply.clone();
|
||||
let ctx = ctx.clone();
|
||||
spawn_local(async move {
|
||||
*slot.borrow_mut() = Some(fetch(&url).await);
|
||||
ctx.request_repaint();
|
||||
});
|
||||
}
|
||||
download
|
||||
}
|
||||
|
||||
pub fn tick(&mut self, app: &mut SolveApp, ctx: &egui::Context) {
|
||||
if let Some(reply) = self.reply.borrow_mut().take() {
|
||||
match reply {
|
||||
Ok(file) => self.pending = Some(file),
|
||||
Err(error) => app.set_status(format!("Could not open design link: {error}"), true),
|
||||
}
|
||||
}
|
||||
if self.pending.is_none() || app.file_picker_busy() {
|
||||
return;
|
||||
}
|
||||
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"))
|
||||
.required()
|
||||
.show_body(ctx, |ui| {
|
||||
ui.label(solidcraft_ui_egui::i18n::tr("web.open_link_confirm"));
|
||||
ui.horizontal(|ui| {
|
||||
if ui_kit::button(ui, solidcraft_ui_egui::i18n::tr("ui.open"), ui_kit::Button::Normal).clicked() {
|
||||
answer = Some(true);
|
||||
}
|
||||
if ui_kit::button(ui, solidcraft_ui_egui::i18n::tr("ui.cancel"), ui_kit::Button::Normal).clicked() {
|
||||
answer = Some(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
match answer {
|
||||
Some(true) => {}
|
||||
Some(false) => {
|
||||
self.pending = None;
|
||||
return;
|
||||
}
|
||||
None if closed => {
|
||||
self.pending = None;
|
||||
return;
|
||||
}
|
||||
None => return,
|
||||
}
|
||||
}
|
||||
let Some((name, bytes)) = self.pending.take() else { return };
|
||||
// The extension selects the design loader even when the URL has no extension.
|
||||
let path = "/linked/design.solidcraft";
|
||||
vfs::insert(path, bytes);
|
||||
app.open_path(path);
|
||||
if app.session.path.as_deref() == Some(path) {
|
||||
app.session.path = None;
|
||||
app.session.doc_mut().name = name;
|
||||
app.session.mark_unsaved();
|
||||
} else {
|
||||
app.set_status("Could not open design link: the downloaded file is not a valid SolidCraft design", true);
|
||||
}
|
||||
let _ = vfs::remove(path);
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch(value: &str) -> Reply {
|
||||
let window = web_sys::window().ok_or("No browser window")?;
|
||||
let page_origin = window.location().origin().map_err(|_| "Cannot read page origin")?;
|
||||
let url = design_url(value, &page_origin)?;
|
||||
let name = url.path_segments().and_then(|mut segments| segments.next_back()).filter(|name| !name.is_empty()).unwrap_or("Design.solidcraft");
|
||||
let decoded = js_sys::decode_uri_component(name).ok().and_then(|name| name.as_string()).unwrap_or_else(|| name.to_owned());
|
||||
let name: String = decoded.strip_suffix(".solidcraft").unwrap_or(&decoded).chars().take(255).collect();
|
||||
let response = JsFuture::from(window.fetch_with_str(url.as_str())).await.map_err(|_| "Network request failed or the host refused CORS access")?;
|
||||
let response: web_sys::Response = response.dyn_into().map_err(|_| "Invalid download response")?;
|
||||
design_url(&response.url(), &page_origin).map_err(|_| "Design redirected to a URL that does not use HTTPS or the page's own origin")?;
|
||||
if !response.ok() {
|
||||
return Err(format!("HTTP {} {}", response.status(), response.status_text()));
|
||||
}
|
||||
let length = response
|
||||
.headers()
|
||||
.get("Content-Length")
|
||||
.map_err(|_| "Cannot read download size")?
|
||||
.map(|value| value.parse::<u64>().map_err(|_| "Invalid Content-Length"))
|
||||
.transpose()?;
|
||||
if let Some(length) = length {
|
||||
check_download_size(0, length)?;
|
||||
}
|
||||
let stream = response.body().ok_or("Download has no body")?;
|
||||
let reader: web_sys::ReadableStreamDefaultReader = stream.get_reader().dyn_into().map_err(|_| "Cannot read download stream")?;
|
||||
let result = read(&reader).await;
|
||||
if result.is_err() {
|
||||
let _ = JsFuture::from(reader.cancel()).await;
|
||||
}
|
||||
// Fetch rejects interrupted HTTP bodies and decodes Content-Encoding. Content-Length
|
||||
// can describe compressed bytes, so the streamed limit also checks the decoded size.
|
||||
let bytes = result?;
|
||||
Ok((name, bytes))
|
||||
}
|
||||
|
||||
async fn read(reader: &web_sys::ReadableStreamDefaultReader) -> Result<Vec<u8>, String> {
|
||||
let mut bytes = Vec::new();
|
||||
// Bound even a stream which keeps returning empty chunks.
|
||||
for _ in 0..1_000_000 {
|
||||
let chunk = JsFuture::from(reader.read()).await.map_err(|_| "Download interrupted")?;
|
||||
if Reflect::get(&chunk, &JsValue::from_str("done")).map_err(|_| "Invalid download stream")?.as_bool() == Some(true) {
|
||||
return Ok(bytes);
|
||||
}
|
||||
let value = Reflect::get(&chunk, &JsValue::from_str("value")).map_err(|_| "Invalid download chunk")?;
|
||||
let chunk: Uint8Array = value.dyn_into().map_err(|_| "Invalid download bytes")?;
|
||||
check_download_size(bytes.len(), u64::from(chunk.length()))?;
|
||||
bytes.extend_from_slice(&chunk.to_vec());
|
||||
}
|
||||
Err("Download returned too many chunks".into())
|
||||
}
|
||||
+25
-262
@@ -11,13 +11,9 @@ mod api;
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
mod disk;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod easyprint;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod files;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod install;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod linked;
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
mod space_mouse;
|
||||
|
||||
@@ -29,89 +25,6 @@ fn browser_switches_tabs(key: &str, ctrl: bool) -> bool {
|
||||
ctrl && key == "Tab"
|
||||
}
|
||||
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
fn query_flag(query: &str, name: &str) -> bool {
|
||||
url::form_urlencoded::parse(query.trim_start_matches('?').as_bytes()).any(|(key, _)| key == name)
|
||||
}
|
||||
|
||||
/// Launch options use exact query keys; URL values are decoded only once.
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
#[derive(Default, Debug)]
|
||||
struct Launch {
|
||||
open: Option<String>,
|
||||
embed: bool,
|
||||
}
|
||||
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
impl Launch {
|
||||
fn parse(query: &str) -> Self {
|
||||
let mut launch = Self::default();
|
||||
let mut embed = false;
|
||||
for (key, value) in url::form_urlencoded::parse(query.trim_start_matches('?').as_bytes()) {
|
||||
match key.as_ref() {
|
||||
"open" if launch.open.is_none() => launch.open = Some(value.into_owned()),
|
||||
"embed" => embed = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
launch.embed = embed && launch.open.is_some();
|
||||
launch
|
||||
}
|
||||
|
||||
fn persists(&self) -> bool {
|
||||
!self.embed
|
||||
}
|
||||
|
||||
fn full_app_url(&self, mut url: url::Url) -> String {
|
||||
url.set_query(None);
|
||||
url.set_fragment(None);
|
||||
if let Some(open) = &self.open {
|
||||
url.query_pairs_mut().append_pair("open", open);
|
||||
}
|
||||
url.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
fn design_url(value: &str, page_origin: &str) -> Result<url::Url, String> {
|
||||
if value.len() > 16_384 {
|
||||
return Err("Design URL is too long".into());
|
||||
}
|
||||
let decoded = percent_encoding::percent_decode_str(value).decode_utf8().map_err(|_| "Invalid design URL encoding")?;
|
||||
if decoded.chars().any(|ch| ch.is_control() || ch == '\\') {
|
||||
return Err("Design links must not contain backslashes or control characters".into());
|
||||
}
|
||||
// Check before URL parsing normalizes away dot segments. Decode escaped slashes too.
|
||||
let path = value.split(['?', '#']).next().unwrap_or_default();
|
||||
let path = percent_encoding::percent_decode_str(path).collect::<Vec<_>>();
|
||||
if path.split(|byte| *byte == b'/').any(|segment| segment == b"..") {
|
||||
return Err("Design links must not contain parent path segments".into());
|
||||
}
|
||||
let page = url::Url::parse(page_origin).map_err(|_| "Invalid page origin")?;
|
||||
let url = if value.starts_with('/') && !value.starts_with("//") {
|
||||
page.join(value).map_err(|_| "Invalid design path")?
|
||||
} else {
|
||||
url::Url::parse(value).map_err(|_| "Invalid design URL")?
|
||||
};
|
||||
if (url.scheme() != "https" && url.origin() != page.origin())
|
||||
|| url.host_str().is_none()
|
||||
|| !url.username().is_empty()
|
||||
|| url.password().is_some()
|
||||
{
|
||||
return Err("Design links must use HTTPS or the page's own origin without credentials".into());
|
||||
}
|
||||
Ok(url)
|
||||
}
|
||||
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
fn check_download_size(current: usize, incoming: u64) -> Result<(), String> {
|
||||
let cap = solidcraft_engine::io::MAX_DESIGN_BYTES as u64;
|
||||
if current as u64 > cap || incoming > cap.saturating_sub(current as u64) {
|
||||
return Err(format!("Design exceeds the {} MB size limit", cap / (1024 * 1024)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod web {
|
||||
use std::rc::Rc;
|
||||
@@ -199,9 +112,6 @@ mod web {
|
||||
|
||||
struct Shell {
|
||||
app: SolveApp,
|
||||
launch: crate::Launch,
|
||||
linked: crate::linked::Download,
|
||||
full_url: Option<String>,
|
||||
api: Api,
|
||||
files: Rc<Files>,
|
||||
/// When the last autosave was (seconds), and the revision it holds (none stored: `None`).
|
||||
@@ -227,9 +137,6 @@ mod web {
|
||||
impl Shell {
|
||||
/// Files from the computer, writes to store, messages and the autosave.
|
||||
fn files_tick(&mut self, ctx: &egui::Context) {
|
||||
if !self.launch.persists() {
|
||||
return;
|
||||
}
|
||||
if self.files.ctx.borrow().is_none() {
|
||||
*self.files.ctx.borrow_mut() = Some(ctx.clone());
|
||||
}
|
||||
@@ -323,15 +230,13 @@ mod web {
|
||||
fn sync_recents(&mut self) {
|
||||
let mut entries = self.files.entries.borrow().clone();
|
||||
entries.sort_by(|a, b| b.modified.total_cmp(&a.modified));
|
||||
let forgotten = self.app.home.forgotten.clone();
|
||||
let recent = &mut self.app.home.recent;
|
||||
let known = self.files.disk_known.borrow().clone();
|
||||
// A design on the computer's disk stays listed while its file is remembered.
|
||||
recent.retain(|p| vfs::exists(p) || disk::parse(p).is_some_and(|(id, _)| known.contains(id)));
|
||||
for e in entries.iter().filter(|e| solidcraft_engine::io::is_design_path(&e.name)) {
|
||||
let p = format!("{}{}", files::OPFS, e.name);
|
||||
// A design the user removed from the list is not added back while it is stored.
|
||||
if !recent.contains(&p) && !forgotten.contains(&p) {
|
||||
if !recent.contains(&p) {
|
||||
recent.push(p);
|
||||
}
|
||||
}
|
||||
@@ -340,10 +245,6 @@ mod web {
|
||||
let recovered = self.files.recovery.borrow().as_ref().and_then(|(_, p)| p.clone());
|
||||
let keep = recent.iter().chain(recovered.as_ref()).filter_map(|p| disk::parse(p)).map(|(id, _)| id.to_string()).collect();
|
||||
disk::prune(keep);
|
||||
// Drop forgotten entries whose stored design is gone, so the set does not grow without bound.
|
||||
let stored: std::collections::BTreeSet<String> =
|
||||
entries.iter().filter(|e| solidcraft_engine::io::is_design_path(&e.name)).map(|e| format!("{}{}", files::OPFS, e.name)).collect();
|
||||
self.app.home.forgotten.retain(|p| stored.contains(p));
|
||||
}
|
||||
|
||||
/// The stored designs: open, insert, rename, delete, download; open from the computer.
|
||||
@@ -360,7 +261,7 @@ mod web {
|
||||
files::pick_from_disk(&self.files, Intent::Open);
|
||||
}
|
||||
if ui_kit::button(ui, "Insert from computer…", ui_kit::Button::Normal)
|
||||
.on_hover_text(solidcraft_ui_egui::i18n::tr("web.files.insert_drawings_hint"))
|
||||
.on_hover_text("STEP, 3MF or STL into the open design")
|
||||
.clicked()
|
||||
{
|
||||
files::pick_from_disk(&self.files, Intent::Insert);
|
||||
@@ -413,7 +314,7 @@ mod web {
|
||||
let design = solidcraft_engine::io::is_design_path(&e.name);
|
||||
if !design
|
||||
&& ui_kit::button(ui, "Insert", ui_kit::Button::Small)
|
||||
.on_hover_text(solidcraft_ui_egui::i18n::tr("web.files.insert_stored_drawings_hint"))
|
||||
.on_hover_text("Into the open design (STEP, 3MF, STL)")
|
||||
.clicked()
|
||||
{
|
||||
act = Some(("insert", e.name.clone()));
|
||||
@@ -482,8 +383,6 @@ mod web {
|
||||
"delete" => {
|
||||
files::delete(&self.files, &name);
|
||||
self.app.home.recent.retain(|r| *r != path);
|
||||
// The file is gone, so there is nothing left to keep forgotten.
|
||||
self.app.home.forgotten.remove(&path);
|
||||
}
|
||||
"rename_cancel" => self.renaming = None,
|
||||
"rename" => {
|
||||
@@ -497,10 +396,6 @@ mod web {
|
||||
*r = new.clone();
|
||||
}
|
||||
}
|
||||
// A forgotten design follows its new name.
|
||||
if self.app.home.forgotten.remove(&old) {
|
||||
self.app.home.forgotten.insert(new);
|
||||
}
|
||||
}
|
||||
Err(e) => self.app.set_status(e, true),
|
||||
}
|
||||
@@ -528,25 +423,15 @@ mod web {
|
||||
});
|
||||
});
|
||||
let Some(recover) = answer else { return };
|
||||
*self.files.recovery.borrow_mut() = None;
|
||||
if recover {
|
||||
let recovery = match files::prepare_recovery(path.as_deref()) {
|
||||
Ok(recovery) => recovery,
|
||||
Err(error) => {
|
||||
self.app.set_status(error, true);
|
||||
return;
|
||||
}
|
||||
};
|
||||
self.app.open_path(&recovery);
|
||||
if self.app.session.path.as_deref() != Some(recovery.as_str()) {
|
||||
return;
|
||||
}
|
||||
self.app.open_path(&files::recovery_path());
|
||||
// It belongs to its own file again (saving goes there); it is unsaved.
|
||||
self.app.session.path = path;
|
||||
self.app.session.doc_mut().name = name;
|
||||
self.app.session.mark_unsaved();
|
||||
self.app.home.open = false;
|
||||
}
|
||||
*self.files.recovery.borrow_mut() = None;
|
||||
files::autosave(&self.files, None);
|
||||
}
|
||||
|
||||
@@ -629,42 +514,19 @@ mod web {
|
||||
if let Some(delay) = self.wheel_gestures.update(raw, now) {
|
||||
ctx.request_repaint_after(delay);
|
||||
}
|
||||
if self.launch.persists() {
|
||||
files::take_drops(&self.files, raw);
|
||||
} else {
|
||||
raw.dropped_files.clear();
|
||||
}
|
||||
files::take_drops(&self.files, raw);
|
||||
// Before the app's own hook, which adds the control channel's events.
|
||||
self.space_mouse.raw_input_hook(ctx, self.app.preferences.space_mouse_enabled, raw);
|
||||
self.app.raw_input_hook(raw);
|
||||
}
|
||||
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
||||
self.linked.tick(&mut self.app, ui.ctx());
|
||||
UNSAVED.with(|u| u.set(self.launch.persists() && solidcraft_ui_egui::documents::any_dirty(&self.app)));
|
||||
if self.launch.embed {
|
||||
ui_kit::Container::new(ui.ctx(), ui_kit::Surface::Chrome).edge("embed_header", ui_kit::Edge::Top).show(ui, |ui| {
|
||||
if ui_kit::button(ui, solidcraft_ui_egui::i18n::tr("web.open_full"), ui_kit::Button::Normal).clicked()
|
||||
&& let (Some(window), Some(url)) = (web_sys::window(), &self.full_url)
|
||||
{
|
||||
let _ = window.open_with_url_and_target_and_features(url, "_blank", "noopener");
|
||||
}
|
||||
});
|
||||
}
|
||||
self.app.ui(ui);
|
||||
let ctx = ui.ctx().clone();
|
||||
if self.launch.persists() {
|
||||
self.file_panel(&ctx);
|
||||
self.recovery_prompt(&ctx);
|
||||
}
|
||||
self.file_panel(&ctx);
|
||||
self.recovery_prompt(&ctx);
|
||||
self.watch_text_for_font(&ctx);
|
||||
}
|
||||
fn persist_egui_memory(&self) -> bool {
|
||||
self.launch.persists()
|
||||
}
|
||||
fn save(&mut self, storage: &mut dyn eframe::Storage) {
|
||||
if !self.launch.persists() {
|
||||
return;
|
||||
}
|
||||
// Persist the user's own language, not a session-only English fallback.
|
||||
let saved = self.font.persisted_language(&self.app.preferences.language).to_owned();
|
||||
let live = std::mem::replace(&mut self.app.preferences.language, saved);
|
||||
@@ -683,15 +545,15 @@ mod web {
|
||||
|
||||
/// Render with WebGL2: asked for now (`?webgl`, remembered) or before (until `?webgpu`).
|
||||
fn webgl() -> bool {
|
||||
let storage = crate::Launch::parse(&query()).persists().then(|| web_sys::window().and_then(|w| w.local_storage().ok().flatten())).flatten();
|
||||
let storage = web_sys::window().and_then(|w| w.local_storage().ok().flatten());
|
||||
let q = query();
|
||||
if crate::query_flag(&q, "webgl") {
|
||||
if q.contains("webgl") {
|
||||
if let Some(s) = &storage {
|
||||
let _ = s.set_item(WEBGL_KEY, "1");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if crate::query_flag(&q, "webgpu") {
|
||||
if q.contains("webgpu") {
|
||||
if let Some(s) = &storage {
|
||||
let _ = s.remove_item(WEBGL_KEY);
|
||||
}
|
||||
@@ -717,10 +579,7 @@ mod web {
|
||||
eframe::WebLogger::init(log::LevelFilter::Info).ok();
|
||||
wasm_bindgen_futures::spawn_local(async {
|
||||
let Some(window) = web_sys::window() else { return };
|
||||
let launch = crate::Launch::parse(&query());
|
||||
if launch.persists() {
|
||||
ask_before_unload(&window);
|
||||
}
|
||||
ask_before_unload(&window);
|
||||
leave_tab_switching_to_browser(&window);
|
||||
let Some(document) = window.document() else { return };
|
||||
let Some(canvas) = document.get_element_by_id(CANVAS_ID).and_then(|e| e.dyn_into::<web_sys::HtmlCanvasElement>().ok()) else {
|
||||
@@ -740,24 +599,19 @@ mod web {
|
||||
Box::new(move |cc| {
|
||||
solidcraft_ui_egui::fonts::setup_fonts(&cc.egui_ctx);
|
||||
let files = Rc::new(Files::default());
|
||||
if launch.persists() {
|
||||
files::start(&files);
|
||||
}
|
||||
files::start(&files);
|
||||
let (panel, opener, saver, shown) = (files.clone(), files.clone(), files.clone(), files.clone());
|
||||
let (restorable, restorer) = (files.clone(), files.clone());
|
||||
let services = if launch.embed { Services::default() } else { Services {
|
||||
let services = Services {
|
||||
// Open shows the browser's open dialog for a design where it has one
|
||||
// (the file keeps its handle, so Save writes back to it), else the
|
||||
// browser's files window. A command that reads another kind of file
|
||||
// (Insert DXF or SVG, an image, a mesh, settings) gets a file dialog
|
||||
// offering that kind, and runs on the picked file.
|
||||
pick_open: Some(Box::new(move |exts: &[&str]| match disk::open_route(disk::supported(), exts) {
|
||||
disk::OpenRoute::Disk => Box::pin(disk::pick_open(&opener)),
|
||||
disk::OpenRoute::Upload => Box::pin(files::pick_upload(&opener, exts)),
|
||||
disk::OpenRoute::Panel => {
|
||||
panel.panel.set(true);
|
||||
Box::pin(std::future::ready(None))
|
||||
// (the file keeps its handle, so Save writes back to it); otherwise,
|
||||
// and for inserting STEP or meshes, the browser's files window.
|
||||
pick_open: Some(Box::new(move |exts: &[&str]| {
|
||||
if disk::handles_open(exts) {
|
||||
return Box::pin(disk::pick_open(&opener));
|
||||
}
|
||||
panel.panel.set(true);
|
||||
Box::pin(std::future::ready(None))
|
||||
})),
|
||||
// A design is saved to a file the user picks, or kept in the browser
|
||||
// where the browser can't pick one; everything else is downloaded.
|
||||
@@ -779,31 +633,29 @@ mod web {
|
||||
})),
|
||||
restore_file: Some(Box::new(move |path: &str| disk::restore(&restorer, path))),
|
||||
download_design: Some(Box::new(files::download)),
|
||||
easyprint_upload: Some(std::sync::Arc::new(crate::easyprint::upload)),
|
||||
browser_files: Some(Box::new(move || shown.panel.set(true))),
|
||||
install_app: Some(crate::install::service(&cc.egui_ctx)),
|
||||
..Default::default()
|
||||
}};
|
||||
};
|
||||
// Thumbnails of saves and start-page cards.
|
||||
solidcraft_ui_egui::gpu::install_cpu_picture_renderer();
|
||||
let (sender, receiver) = std::sync::mpsc::channel();
|
||||
let mut app = SolveApp::new(Session::default(), services).with_control(receiver);
|
||||
app.embedded = launch.embed;
|
||||
if launch.persists() && let Some(p) = cc.storage.and_then(|s| solidcraft_ui_egui::prefs::stored(|k| s.get_string(k))) {
|
||||
if let Some(p) = cc.storage.and_then(|s| solidcraft_ui_egui::prefs::stored(|k| s.get_string(k))) {
|
||||
app.load_prefs(&p);
|
||||
}
|
||||
if let Some(rs) = &cc.wgpu_render_state {
|
||||
// eframe's web painter has no stencil buffer.
|
||||
app.set_wgpu(rs, DEPTH_BITS, 0, 1);
|
||||
}
|
||||
if launch.open.is_none() && crate::query_flag(&query(), "sample") {
|
||||
if query().contains("sample") {
|
||||
if let Err(e) = app.session.run_script(&solidcraft_engine::sample::script()) {
|
||||
app.set_status(format!("sample: {e}"), true);
|
||||
}
|
||||
app.session.doc_mut().name = "Sample Plate".into();
|
||||
app.session.mark_saved();
|
||||
}
|
||||
if launch.open.is_none() && !crate::query_flag(&query(), "sample") {
|
||||
if !query().contains("sample") {
|
||||
// The empty design takes Units of new designs (#1205). The sample
|
||||
// part is built in millimetres and is left that way.
|
||||
solidcraft_ui_egui::prefs::apply_new_design(&mut app);
|
||||
@@ -812,13 +664,8 @@ mod web {
|
||||
}
|
||||
let api = Api::install(sender, cc.egui_ctx.clone()).map_err(|e| format!("SolidCraft browser API: {e:?}"))?;
|
||||
let font_language = app.preferences.language.clone();
|
||||
let linked = crate::linked::Download::start(launch.open.as_deref(), &cc.egui_ctx);
|
||||
let full_url = window.location().href().ok().and_then(|value| url::Url::parse(&value).ok()).map(|url| launch.full_app_url(url));
|
||||
Ok(Box::new(Shell {
|
||||
app,
|
||||
launch,
|
||||
linked,
|
||||
full_url,
|
||||
api,
|
||||
files,
|
||||
autosaved_at: f64::NEG_INFINITY,
|
||||
@@ -862,90 +709,6 @@ fn main() {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn link_query_decodes_once_and_embed_disables_persistence() {
|
||||
assert!(!query_flag("?open=https://example.org/webgl?sample", "webgl"));
|
||||
assert!(!query_flag("?open=https://example.org/webgl?sample", "sample"));
|
||||
assert!(query_flag("?embed&webgl", "webgl"));
|
||||
let launch = Launch::parse("?embed&open=https%3A%2F%2Fexample.org%2Farm.solidcraft%3Fx%3Da%2520b%26y%3D1");
|
||||
assert_eq!(launch.open.as_deref(), Some("https://example.org/arm.solidcraft?x=a%20b&y=1"));
|
||||
assert!(launch.embed);
|
||||
assert!(!launch.persists());
|
||||
assert!(!Launch::parse("?embed").embed);
|
||||
assert!(!Launch::parse("?other=embed&reopen=https://example.org").embed);
|
||||
assert!(Launch::parse("?open=https://example.org/a").persists());
|
||||
assert_eq!(Launch::parse("?open=first&open=second").open.as_deref(), Some("first"));
|
||||
let launch = Launch::parse("?embed&open=/dutchman/jobs/abc/part.solidcraft%3Fx%3Da%2520b");
|
||||
assert!(launch.embed);
|
||||
assert!(!launch.persists());
|
||||
let page = url::Url::parse("http://solidcraft.io/?embed#view").expect("synthetic page");
|
||||
let full = url::Url::parse(&launch.full_app_url(page)).expect("full app URL");
|
||||
let reopened = Launch::parse(full.query().unwrap_or_default());
|
||||
assert_eq!(reopened.open, launch.open);
|
||||
assert_eq!(reopened.open.as_deref(), Some("/dutchman/jobs/abc/part.solidcraft?x=a%20b"));
|
||||
assert!(!reopened.embed);
|
||||
assert!(reopened.persists());
|
||||
assert!(full.fragment().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn design_links_refuse_hostile_schemes_credentials_and_sizes() {
|
||||
assert!(design_url("https://example.org/arm.solidcraft?download=1", "http://solidcraft.io").is_ok());
|
||||
for value in [
|
||||
"http://example.org/a",
|
||||
"data:application/json,{}",
|
||||
"blob:https://example.org/a",
|
||||
"javascript:alert(1)",
|
||||
"https://user:secret@example.org/a",
|
||||
"//evil.example/x",
|
||||
"/../x",
|
||||
"/%2e%2e/x",
|
||||
"/a/%2E./x",
|
||||
"/a%2f..%2fx",
|
||||
"/a\\x",
|
||||
"/a%5cx",
|
||||
"/a\nx",
|
||||
"/a%00x",
|
||||
"/a%7fx",
|
||||
"/a%C2%85x",
|
||||
"/a%3f/../x",
|
||||
"/a/..#view",
|
||||
"/a/..?download=1",
|
||||
"https://solidcraft.io/a/../x",
|
||||
"http://user:secret@solidcraft.io/x",
|
||||
"http://solidcraft.io.evil.example/x",
|
||||
"http://solidcraft.io:8080/x",
|
||||
"relative.solidcraft",
|
||||
"",
|
||||
] {
|
||||
assert!(design_url(value, "http://solidcraft.io").is_err(), "{value}");
|
||||
}
|
||||
assert!(design_url(&format!("https://example.org/{}", "x".repeat(16_384)), "http://solidcraft.io").is_err());
|
||||
assert!(design_url("http://redirect.example.org/a", "http://solidcraft.io").is_err());
|
||||
|
||||
let cap = solidcraft_engine::io::MAX_DESIGN_BYTES;
|
||||
assert!(check_download_size(0, cap as u64).is_ok());
|
||||
assert!(check_download_size(0, cap as u64 + 1).is_err());
|
||||
assert!(check_download_size(cap - 2, 2).is_ok());
|
||||
assert!(check_download_size(cap - 2, 3).is_err());
|
||||
assert!(check_download_size(0, u64::MAX).is_err());
|
||||
assert!(check_download_size(cap, 1).is_err());
|
||||
assert!(check_download_size(usize::MAX, 0).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn design_links_allow_same_origin_paths_and_redirects() {
|
||||
for origin in ["http://solidcraft.io", "https://solidcraft.io", "http://solidcraft.io:8080"] {
|
||||
assert_eq!(design_url("/a/b.solidcraft", origin).map(|url| url.to_string()), Ok(format!("{origin}/a/b.solidcraft")));
|
||||
assert!(design_url(&format!("{origin}/x"), origin).is_ok());
|
||||
// Final response URLs follow the same origin and scheme rules as the request.
|
||||
assert!(design_url("https://docs.solidcraft.io/x", origin).is_ok());
|
||||
assert!(design_url("http://other/x", origin).is_err());
|
||||
}
|
||||
assert!(design_url("http://solidcraft.io/x", "https://solidcraft.io").is_err());
|
||||
assert!(design_url("http://SOLIDCRAFT.io:80/x", "http://solidcraft.io").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_ctrl_tab_is_left_to_the_browser() {
|
||||
// Ctrl+Tab and Ctrl+Shift+Tab (the event's key is "Tab" either way) switch browser tabs.
|
||||
|
||||
@@ -7,21 +7,9 @@ license.workspace = true
|
||||
publish = false
|
||||
default-run = "solidcraft"
|
||||
|
||||
[features]
|
||||
default = ["raytrace-cpu", "bundled-render-library", "smlib", "profile"]
|
||||
bundled-render-library = ["solidcraft-engine/bundled-render-library"]
|
||||
raytrace-cpu = ["solidcraft-engine/raytrace-cpu"]
|
||||
# Link SMLib into the kernel (solidcraft-engine/smlib; needs scripts/fetch-smlib.sh). SMLib is the
|
||||
# only kernel, so without it the app models nothing.
|
||||
smlib = ["solidcraft-engine/smlib"]
|
||||
# The profiler, compiled in so any build can say where its time goes (the Profile operations
|
||||
# preference, SOLIDCRAFT_PROFILE=1, diagnostics.profile); it records nothing until asked.
|
||||
profile = ["solidcraft-engine/profile"]
|
||||
|
||||
# The workspace lints, except unsafe_code: denied, not forbidden, so that src/open_documents.rs
|
||||
# (macOS file-open events), src/native_crash.rs (Windows exception filter), src/native_crash_macos.rs
|
||||
# (macOS crash signal handler) and src/native_frame.rs (Windows resize border, taskbar icon) can
|
||||
# allow it: none has a safe API. Keep the rest in sync with [workspace.lints].
|
||||
# (macOS file-open events), src/native_crash.rs (Windows exception filter) and src/native_frame.rs
|
||||
# (Windows resize border) can allow it: none has a safe API. Keep the rest in sync with [workspace.lints].
|
||||
[lints.rust]
|
||||
unsafe_code = "deny"
|
||||
|
||||
@@ -38,7 +26,6 @@ solidcraft-ui-egui = { workspace = true }
|
||||
solidcraft-space-mouse = { workspace = true }
|
||||
eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit", "wayland", "x11", "links"] }
|
||||
egui = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
log = { workspace = true }
|
||||
rfd = { workspace = true }
|
||||
@@ -62,8 +49,7 @@ hidapi = { version = "2.6.7", default-features = false, features = ["windows-nat
|
||||
# The main window's handle for src/native_frame.rs (already in the tree through eframe and winit).
|
||||
raw-window-handle = "0.6"
|
||||
# Native crash capture (src/native_crash.rs): the exception filter, the faulting module, a minidump.
|
||||
# The window's resize border, DPI size and class icon (src/native_frame.rs): the window procedure, hit
|
||||
# test, DPI, the exe's icon.
|
||||
# The window's resize border and DPI size (src/native_frame.rs): the window procedure, hit test, DPI.
|
||||
# Finding 3D mice through Plug and Play without asking other devices anything (src/space_mouse/windows_hid.rs).
|
||||
windows-sys = { version = "0.61", features = [
|
||||
"Win32_Devices_DeviceAndDriverInstallation",
|
||||
@@ -90,15 +76,11 @@ solidcraft-3dx-client = { workspace = true }
|
||||
# The 3D mouse (src/space_mouse.rs), in the shared-device open mode so that a running 3DxWare
|
||||
# driver does not shut the app out.
|
||||
hidapi = { version = "2.6.7", default-features = false, features = ["macos-shared-device"] }
|
||||
# Files opened from Finder and the Dock (src/open_documents.rs), and the fullscreen title-bar
|
||||
# inset (src/main.rs); all already come with eframe and rfd.
|
||||
# Files opened from Finder and the Dock (src/open_documents.rs); all already come with eframe and rfd.
|
||||
objc2 = "0.6"
|
||||
objc2-app-kit = { version = "0.3", default-features = false, features = ["std", "NSApplication", "NSResponder", "NSWindow"] }
|
||||
objc2-app-kit = { version = "0.3", default-features = false, features = ["std", "NSApplication", "NSResponder"] }
|
||||
objc2-foundation = { version = "0.3", default-features = false, features = ["std", "block2", "NSArray", "NSNotification", "NSOperation", "NSProcessInfo", "NSString", "NSURL"] }
|
||||
block2 = "0.6"
|
||||
# Native crash capture (src/native_crash_macos.rs): the signal handler, the faulting module
|
||||
# (already in the tree through many crates).
|
||||
libc = "0.2"
|
||||
|
||||
[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies]
|
||||
winit = { version = "0.30", default-features = false, features = ["x11"] }
|
||||
|
||||
@@ -13,15 +13,11 @@ use solidcraft_engine::MAX_LINE;
|
||||
use solidcraft_engine::control_auth;
|
||||
use solidcraft_engine::control_pending;
|
||||
use solidcraft_ui_egui::ControlRequest;
|
||||
use solidcraft_ui_egui::control::{DESIGN_WATCH, DesignWatch, Subscriber};
|
||||
|
||||
/// Connections served at once, so a flood must not be able to spawn threads without end.
|
||||
const MAX_CONNECTIONS: usize = 16;
|
||||
/// Lines that are not JSON before the connection is closed, so garbage cannot loop forever.
|
||||
const MAX_BAD_LINES: usize = 8;
|
||||
/// How long a subscribed connection waits after a `design.changed` line before the next: what
|
||||
/// changes meanwhile (a parameter dragged) goes out as one line, the latest (#6985).
|
||||
const EVENT_GAP: Duration = Duration::from_millis(100);
|
||||
|
||||
static OPEN: AtomicUsize = AtomicUsize::new(0);
|
||||
/// The token files this launch wrote (its control port, the agent chat's endpoint), removed
|
||||
@@ -39,7 +35,7 @@ fn new_token() -> Option<String> {
|
||||
pub fn remove_token_file() {
|
||||
if let Ok(files) = TOKEN_FILES.lock() {
|
||||
for p in files.iter() {
|
||||
control_auth::remove_token_file(p);
|
||||
let _ = std::fs::remove_file(p);
|
||||
}
|
||||
}
|
||||
if let Some(port) = APP_PORT.lock().ok().and_then(|p| *p) {
|
||||
@@ -131,7 +127,7 @@ fn listen_on_listener(listener: TcpListener, token: String, tx: Sender<ControlRe
|
||||
if OPEN.fetch_add(1, Ordering::Relaxed) >= MAX_CONNECTIONS {
|
||||
OPEN.fetch_sub(1, Ordering::Relaxed);
|
||||
let mut refused = stream;
|
||||
let _ = send(&mut refused, &json!({"ok": false, "error": "too many connections"}));
|
||||
let _ = writeln!(refused, "{}", json!({"ok": false, "error": "too many connections"}));
|
||||
continue;
|
||||
}
|
||||
let tx = tx.clone();
|
||||
@@ -178,44 +174,6 @@ fn read_line(r: &mut impl BufRead, max: usize) -> std::io::Result<Option<Vec<u8>
|
||||
}
|
||||
}
|
||||
|
||||
/// A reply line, written to the socket at once. `writeln!` with a JSON value hands the socket each
|
||||
/// of its pieces apart, a system call for every string, number and comma: half a second for a
|
||||
/// design's component tree when it has thousands of components.
|
||||
fn send(out: &mut impl Write, reply: &Value) -> std::io::Result<()> {
|
||||
let mut line = serde_json::to_vec(reply).map_err(std::io::Error::other)?;
|
||||
line.push(b'\n');
|
||||
out.write_all(&line)
|
||||
}
|
||||
|
||||
/// The `"token"` of a request line that is a JSON object, read without building the rest of it.
|
||||
/// Until a connection shows the token, a line costs only its own bytes: parsed whole, a 16 MiB
|
||||
/// line of `[0,0,…]` is hundreds of MB, and 16 such connections could exhaust memory before any
|
||||
/// token was checked.
|
||||
fn token_of(line: &str) -> Option<String> {
|
||||
struct Token;
|
||||
impl<'de> serde::de::Visitor<'de> for Token {
|
||||
type Value = Option<String>;
|
||||
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
f.write_str("a request object")
|
||||
}
|
||||
fn visit_map<A: serde::de::MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
|
||||
let mut token = None;
|
||||
while let Some(key) = map.next_key::<String>()? {
|
||||
if key == "token" {
|
||||
token = Some(map.next_value::<String>()?);
|
||||
} else {
|
||||
map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
}
|
||||
Ok(token)
|
||||
}
|
||||
}
|
||||
let mut de = serde_json::Deserializer::from_str(line);
|
||||
let token = serde::Deserializer::deserialize_map(&mut de, Token).ok()?;
|
||||
de.end().ok()?;
|
||||
token
|
||||
}
|
||||
|
||||
/// The reply to a connection that didn't show the token; the connection is then closed.
|
||||
fn unauthorized() -> Value {
|
||||
json!({"ok": false, "error": "unauthorized: send the control token as \"token\" (see docs/control-protocol.md)"})
|
||||
@@ -226,7 +184,7 @@ fn unauthorized() -> Value {
|
||||
/// the connection, and a reset can reach the client before it has read the refusal.
|
||||
fn refuse(out: &mut TcpStream, reader: &mut BufReader<TcpStream>) {
|
||||
use std::io::Read;
|
||||
let _ = send(out, &unauthorized());
|
||||
let _ = writeln!(out, "{}", unauthorized());
|
||||
let _ = out.shutdown(std::net::Shutdown::Write);
|
||||
let _ = reader.get_ref().set_read_timeout(Some(Duration::from_millis(500)));
|
||||
let _ = std::io::copy(&mut reader.by_ref().take(64 * 1024), &mut std::io::sink());
|
||||
@@ -398,91 +356,10 @@ fn busy_message(p: &Pending, method: &str) -> String {
|
||||
)
|
||||
}
|
||||
|
||||
/// A subscribed connection's socket, for its replies and its events: each line is written whole
|
||||
/// and alone, so an event never lands inside a reply (#6985).
|
||||
struct Lines {
|
||||
socket: TcpStream,
|
||||
turn: Mutex<()>,
|
||||
}
|
||||
|
||||
impl Lines {
|
||||
fn send(&self, line: &Value) -> std::io::Result<()> {
|
||||
let _turn = self.turn.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
send(&mut &self.socket, line)
|
||||
}
|
||||
}
|
||||
|
||||
/// A connection's `control.subscribe` (#6985): its `design.changed` lines, sent from a thread of
|
||||
/// their own until the connection ends.
|
||||
struct Subscription {
|
||||
lines: Arc<Lines>,
|
||||
stop: Arc<AtomicBool>,
|
||||
_counted: Subscriber,
|
||||
}
|
||||
|
||||
impl Subscription {
|
||||
/// Write `reply`, the window's answer to `control.subscribe` naming the design `seen`, on
|
||||
/// `out`, then send `design`'s changes after it: the reply goes out before any event. `None`,
|
||||
/// with nothing written, when no thread or socket was left for the events.
|
||||
fn start(
|
||||
design: &'static DesignWatch,
|
||||
counted: Subscriber,
|
||||
out: &TcpStream,
|
||||
seen: (String, u64),
|
||||
reply: &Value,
|
||||
) -> Option<(Self, std::io::Result<()>)> {
|
||||
let lines = Arc::new(Lines { socket: out.try_clone().ok()?, turn: Mutex::new(()) });
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
let first = lines.turn.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
let (l, s) = (lines.clone(), stop.clone());
|
||||
std::thread::Builder::new().stack_size(64 * 1024).spawn(move || send_events(design, &l, &s, seen)).ok()?;
|
||||
let sent = send(&mut &lines.socket, reply);
|
||||
drop(first);
|
||||
Some((Subscription { lines, stop, _counted: counted }, sent))
|
||||
}
|
||||
}
|
||||
|
||||
/// The design a `control.subscribe` reply names.
|
||||
fn design_of(reply: &Value) -> Option<(String, u64)> {
|
||||
let result = reply.get("result")?;
|
||||
Some((result.get("document_id")?.as_str()?.to_string(), result.get("revision")?.as_u64()?))
|
||||
}
|
||||
|
||||
impl Drop for Subscription {
|
||||
fn drop(&mut self) {
|
||||
self.stop.store(true, Ordering::Relaxed);
|
||||
// The socket closes now, not when the thread next looks; a line blocked on a client that
|
||||
// stopped reading ends too.
|
||||
let _ = self.lines.socket.shutdown(std::net::Shutdown::Both);
|
||||
}
|
||||
}
|
||||
|
||||
/// The `design.changed` lines of a subscribed connection, each time the design differs from the
|
||||
/// last one it was told of (`seen`), until `stop`.
|
||||
fn send_events(design: &DesignWatch, lines: &Lines, stop: &AtomicBool, mut seen: (String, u64)) {
|
||||
while !stop.load(Ordering::Relaxed) {
|
||||
let Some(now) = design.wait(&seen, stop, Duration::from_secs(1)).filter(|now| *now != seen) else { continue };
|
||||
if lines.send(&json!({"event": "design.changed", "params": {"document_id": now.0, "revision": now.1}})).is_err() {
|
||||
break;
|
||||
}
|
||||
seen = now;
|
||||
std::thread::sleep(EVENT_GAP);
|
||||
}
|
||||
}
|
||||
|
||||
/// Write `reply` on the connection: through its subscription once it has one.
|
||||
fn answer(out: &mut TcpStream, events: Option<&Subscription>, reply: &Value) -> std::io::Result<()> {
|
||||
match events {
|
||||
Some(e) => e.lines.send(reply),
|
||||
None => send(out, reply),
|
||||
}
|
||||
}
|
||||
|
||||
fn serve(stream: TcpStream, token: &str, tx: Sender<ControlRequest>, ctx: egui::Context, patient: bool) {
|
||||
serve_with(stream, token, tx, ctx, patient, WAITS, &RUNNING, &DESIGN_WATCH);
|
||||
serve_with(stream, token, tx, ctx, patient, WAITS, &RUNNING);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn serve_with(
|
||||
stream: TcpStream,
|
||||
token: &str,
|
||||
@@ -491,7 +368,6 @@ fn serve_with(
|
||||
patient: bool,
|
||||
waits: Waits,
|
||||
running: &'static Running,
|
||||
design: &'static DesignWatch,
|
||||
) {
|
||||
let connection = running.connections.fetch_add(1, Ordering::Relaxed) + 1;
|
||||
let Ok(read) = stream.try_clone() else { return };
|
||||
@@ -500,33 +376,30 @@ fn serve_with(
|
||||
let mut bad = 0usize;
|
||||
let mut authenticated = false;
|
||||
let mut pending: Option<Pending> = None;
|
||||
let mut events: Option<Subscription> = None;
|
||||
loop {
|
||||
// A first `control.subscribe` on its way to the window (#6985).
|
||||
let mut joining = None;
|
||||
let line = match read_line(&mut reader, MAX_LINE) {
|
||||
Ok(Some(l)) => String::from_utf8_lossy(&l).into_owned(),
|
||||
Ok(None) => break,
|
||||
Err(e) => {
|
||||
let _ = answer(&mut out, events.as_ref(), &json!({"ok": false, "error": e.to_string()}));
|
||||
let _ = writeln!(out, "{}", json!({"ok": false, "error": e.to_string()}));
|
||||
break;
|
||||
}
|
||||
};
|
||||
if line.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
// The first request must carry the token; later ones on the same connection may.
|
||||
if !authenticated {
|
||||
match token_of(&line) {
|
||||
Some(t) if control_auth::tokens_match(&t, token) => authenticated = true,
|
||||
_ => {
|
||||
refuse(&mut out, &mut reader);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let reply = match serde_json::from_str::<Value>(&line) {
|
||||
Ok(msg) => {
|
||||
// The first request must carry the token; later ones on the same connection may.
|
||||
if !authenticated {
|
||||
match msg.get("token").and_then(Value::as_str) {
|
||||
Some(t) if control_auth::tokens_match(t, token) => authenticated = true,
|
||||
_ => {
|
||||
refuse(&mut out, &mut reader);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let id = msg.get("id").cloned().unwrap_or(Value::Null);
|
||||
let method = msg.get("method").and_then(Value::as_str).unwrap_or("").to_string();
|
||||
let params = msg.get("params").cloned().unwrap_or(json!({}));
|
||||
@@ -552,14 +425,6 @@ fn serve_with(
|
||||
} else if let Some(p) = pending.as_ref().filter(|_| late.is_none()) {
|
||||
// Nothing more is queued behind it, to run unseen when it ends.
|
||||
json!({"ok": false, "busy": true, "error": busy_message(p, &method)})
|
||||
} else if method == "control.subscribe" && events.is_some() {
|
||||
if late.is_some() {
|
||||
// As below: finished, and the client moved on without asking.
|
||||
pending = None;
|
||||
}
|
||||
// Its events already say how the design moves; a second answer could only
|
||||
// arrive behind a newer one.
|
||||
json!({"ok": false, "error": "already subscribed: this connection is sent design.changed lines; ask document.revision for the design now"})
|
||||
} else {
|
||||
if late.is_some() {
|
||||
// Finished, and the client moved on without asking: it is dropped.
|
||||
@@ -569,9 +434,6 @@ fn serve_with(
|
||||
let wait = if patient || method == "engine.batch" { waits.long } else { waits.short };
|
||||
let what = control_pending::describe(&method, ¶ms);
|
||||
let effect = control_pending::effect(&method, ¶ms);
|
||||
// Counted before the window answers, so it publishes every change from its
|
||||
// answer on.
|
||||
joining = (method == "control.subscribe").then(|| design.join());
|
||||
let (req, rrx) = ControlRequest::new(method.clone(), params);
|
||||
let ticket = running.add(connection, &id, &req);
|
||||
if tx.send(req).is_err() {
|
||||
@@ -592,25 +454,16 @@ fn serve_with(
|
||||
}
|
||||
r
|
||||
}
|
||||
Err(_) if !authenticated => {
|
||||
refuse(&mut out, &mut reader);
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
bad += 1;
|
||||
json!({"ok": false, "error": format!("bad JSON: {e}")})
|
||||
}
|
||||
};
|
||||
// Subscribed once the window named the design (not when it refused or timed out).
|
||||
let sent = match joining.and_then(|counted| design_of(&reply).map(|seen| (counted, seen))) {
|
||||
Some((counted, seen)) => match Subscription::start(design, counted, &out, seen, &reply) {
|
||||
Some((subscription, sent)) => {
|
||||
events = Some(subscription);
|
||||
sent
|
||||
}
|
||||
None => {
|
||||
send(&mut out, &json!({"id": reply.get("id"), "ok": false, "error": "not subscribed: no thread or socket left for the events"}))
|
||||
}
|
||||
},
|
||||
None => answer(&mut out, events.as_ref(), &reply),
|
||||
};
|
||||
if sent.is_err() {
|
||||
if writeln!(out, "{reply}").is_err() {
|
||||
break;
|
||||
}
|
||||
if bad >= MAX_BAD_LINES {
|
||||
@@ -650,7 +503,7 @@ mod tests {
|
||||
static RUNNING: Running = Running::new();
|
||||
std::thread::spawn(move || {
|
||||
if let Some(stream) = listener.incoming().flatten().next() {
|
||||
serve_with(stream, token, tx, egui::Context::default(), false, waits, &RUNNING, &DESIGN_WATCH);
|
||||
serve_with(stream, token, tx, egui::Context::default(), false, waits, &RUNNING);
|
||||
}
|
||||
});
|
||||
let s = TcpStream::connect(addr).unwrap();
|
||||
@@ -716,7 +569,7 @@ mod tests {
|
||||
std::thread::spawn(move || {
|
||||
for stream in listener.incoming().flatten() {
|
||||
let tx = tx.clone();
|
||||
std::thread::spawn(move || serve_with(stream, token, tx, egui::Context::default(), false, waits, &RUNNING, &DESIGN_WATCH));
|
||||
std::thread::spawn(move || serve_with(stream, token, tx, egui::Context::default(), false, waits, &RUNNING));
|
||||
}
|
||||
});
|
||||
let open = || {
|
||||
@@ -786,95 +639,6 @@ mod tests {
|
||||
assert_eq!((&r["result"]["pending"], &r["result"]["done"]), (&json!(7), &json!(true)), "{r}");
|
||||
}
|
||||
|
||||
/// #6985: a connection that sends `control.subscribe` gets the design in the reply, then a
|
||||
/// `design.changed` line each time the window publishes another: never before the reply,
|
||||
/// none for the same design, the latest after a burst, each line whole between its replies.
|
||||
/// A second subscribe is refused without reaching the window. A connection that did not
|
||||
/// subscribe gets no events, and one that closed is no longer counted.
|
||||
#[test]
|
||||
fn subscribed_connections_are_told_of_design_changes() {
|
||||
static RUNNING: Running = Running::new();
|
||||
static WATCH: DesignWatch = DesignWatch::new();
|
||||
let token = "0123456789abcdef0123456789abcdef";
|
||||
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
let (tx, rx) = channel::<ControlRequest>();
|
||||
// The window: it answers `control.subscribe` as the app does, publishing the design first;
|
||||
// then the design changes at once, before the reply may have been written.
|
||||
std::thread::spawn(move || {
|
||||
for req in rx {
|
||||
if req.method == "control.subscribe" {
|
||||
WATCH.publish("d-1", 3);
|
||||
let _ = req.reply.send(json!({"ok": true, "result": {"document_id": "d-1", "revision": 3}}));
|
||||
WATCH.publish("d-1", 4);
|
||||
} else {
|
||||
let _ = req.reply.send(json!({"ok": true, "result": req.method}));
|
||||
}
|
||||
}
|
||||
});
|
||||
std::thread::spawn(move || {
|
||||
for stream in listener.incoming().flatten() {
|
||||
let tx = tx.clone();
|
||||
std::thread::spawn(move || serve_with(stream, token, tx, egui::Context::default(), false, WAITS, &RUNNING, &WATCH));
|
||||
}
|
||||
});
|
||||
let open = || {
|
||||
let s = TcpStream::connect(addr).unwrap();
|
||||
s.set_read_timeout(Some(Duration::from_secs(10))).unwrap();
|
||||
(s.try_clone().unwrap(), BufReader::new(s))
|
||||
};
|
||||
let next = |lines: &mut BufReader<TcpStream>| -> Value {
|
||||
let mut l = String::new();
|
||||
lines.read_line(&mut l).unwrap();
|
||||
serde_json::from_str(&l).unwrap()
|
||||
};
|
||||
let changed = |id: &str, revision: u64| json!({"event": "design.changed", "params": {"document_id": id, "revision": revision}});
|
||||
|
||||
let (mut quiet_out, mut quiet) = open();
|
||||
writeln!(quiet_out, "{}", json!({"id": 1, "method": "ui.view", "token": token})).unwrap();
|
||||
assert_eq!(next(&mut quiet)["id"], 1);
|
||||
|
||||
let (mut out, mut lines) = open();
|
||||
writeln!(out, "{}", json!({"id": 1, "method": "control.subscribe", "token": token})).unwrap();
|
||||
assert_eq!(next(&mut lines), json!({"id": 1, "ok": true, "result": {"document_id": "d-1", "revision": 3}}));
|
||||
assert_eq!(next(&mut lines), changed("d-1", 4), "the change after the window's answer comes after the reply");
|
||||
assert!(WATCH.subscribed());
|
||||
// The design it was told of is no news; another revision is.
|
||||
WATCH.publish("d-1", 4);
|
||||
WATCH.publish("d-1", 5);
|
||||
assert_eq!(next(&mut lines), changed("d-1", 5));
|
||||
// A burst ends with its latest.
|
||||
for revision in 6..=10 {
|
||||
WATCH.publish("d-1", revision);
|
||||
}
|
||||
let mut last = 5;
|
||||
while last < 10 {
|
||||
let e = next(&mut lines);
|
||||
let revision = e["params"]["revision"].as_u64().unwrap();
|
||||
assert!(e["event"] == "design.changed" && revision > last, "{e} after {last}");
|
||||
last = revision;
|
||||
}
|
||||
// Another tab: another id, whatever its revision.
|
||||
WATCH.publish("d-2", 2);
|
||||
assert_eq!(next(&mut lines), changed("d-2", 2));
|
||||
// Subscribed already: refused here (the window would have published d-1 again).
|
||||
writeln!(out, "{}", json!({"id": 2, "method": "control.subscribe"})).unwrap();
|
||||
let r = next(&mut lines);
|
||||
assert!(r["id"] == 2 && r["ok"] == false && r["error"].as_str().is_some_and(|e| e.starts_with("already subscribed")), "{r}");
|
||||
// Replies still come, whole, and nothing else was sent meanwhile.
|
||||
writeln!(out, "{}", json!({"id": 3, "method": "ui.view"})).unwrap();
|
||||
assert_eq!(next(&mut lines), json!({"id": 3, "ok": true, "result": "ui.view"}));
|
||||
writeln!(quiet_out, "{}", json!({"id": 2, "method": "ui.view"})).unwrap();
|
||||
assert_eq!(next(&mut quiet), json!({"id": 2, "ok": true, "result": "ui.view"}), "no events without subscribing");
|
||||
// Closed, it is no longer counted.
|
||||
drop((out, lines));
|
||||
let deadline = Instant::now() + Duration::from_secs(10);
|
||||
while WATCH.subscribed() && Instant::now() < deadline {
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
assert!(!WATCH.subscribed());
|
||||
}
|
||||
|
||||
/// A timed-out request that only reads is not said to "still apply", nor is one that only
|
||||
/// writes a file (an export, #1989); a change is, by name.
|
||||
#[test]
|
||||
@@ -894,19 +658,6 @@ mod tests {
|
||||
assert!(render.contains("will still write the file"), "{render}");
|
||||
}
|
||||
|
||||
/// The token is read from a request object without building the rest of it: other fields,
|
||||
/// however large or nested, are only skipped. Anything but an object with a string token is
|
||||
/// no token.
|
||||
#[test]
|
||||
fn token_is_read_without_parsing_the_request() {
|
||||
assert_eq!(token_of(r#"{"id":1,"token":"abc","params":{"a":[1,[2,{"b":null}]]}}"#).as_deref(), Some("abc"));
|
||||
let flood = format!(r#"{{"params":[{}0],"token":"t"}}"#, "0,".repeat(1 << 20));
|
||||
assert_eq!(token_of(&flood).as_deref(), Some("t"));
|
||||
for no in [r#"["abc"]"#, r#"{"token":5}"#, r#"{"token":"a"} x"#, r#"{"id":1}"#, "GET / HTTP/1.1", r#""token""#] {
|
||||
assert_eq!(token_of(no), None, "{no}");
|
||||
}
|
||||
}
|
||||
|
||||
/// #205: a connection runs nothing until a request carries the launch's token; a wrong or
|
||||
/// missing token, or garbage, is refused and the connection closed. Once shown, the token
|
||||
/// need not be repeated on that connection.
|
||||
@@ -999,31 +750,4 @@ mod tests {
|
||||
let mut r = BufReader::with_capacity(64, text.as_slice());
|
||||
assert!(read_line(&mut r, 1000).is_err());
|
||||
}
|
||||
|
||||
/// A large reply reaches the socket in one write, not one per JSON piece (a component tree of
|
||||
/// thousands of components took half a second to send), and reads back as it was.
|
||||
#[test]
|
||||
fn a_large_reply_is_written_at_once() {
|
||||
struct Counted {
|
||||
writes: usize,
|
||||
bytes: Vec<u8>,
|
||||
}
|
||||
impl Write for Counted {
|
||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
self.writes += 1;
|
||||
self.bytes.extend_from_slice(buf);
|
||||
Ok(buf.len())
|
||||
}
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
let components: Vec<Value> = (0..2000).map(|i| json!({"id": i, "name": format!("Part {i}"), "bodies": ["a", "b"], "parent": 0})).collect();
|
||||
let reply = json!({"ok": true, "id": 7, "result": {"components": components}});
|
||||
let mut out = Counted { writes: 0, bytes: Vec::new() };
|
||||
send(&mut out, &reply).unwrap();
|
||||
assert_eq!(out.writes, 1);
|
||||
assert_eq!(out.bytes.last(), Some(&b'\n'));
|
||||
assert_eq!(serde_json::from_slice::<Value>(&out.bytes).unwrap(), reply);
|
||||
}
|
||||
}
|
||||
|
||||
+44
-337
@@ -8,7 +8,6 @@
|
||||
//! A `.step`/`.stp` file opens as a new design holding the file's bodies (an Import feature); a
|
||||
//! `.3mf`/`.stl`/`.obj`/`.glb`/`.gltf` file as a new design holding mesh bodies.
|
||||
//!
|
||||
//! `SOLIDCRAFT_SETTINGS_DIR` selects an explicit absolute settings directory without legacy adoption.
|
||||
//! `--control <port>` (or `SOLIDCRAFT_CONTROL_PORT`) starts a localhost JSON-lines control
|
||||
//! server: `{"id":1,"method":"engine.execute","params":{"command":"solid.box","params":{…}}}`.
|
||||
//! Without them the channel still opens, on the default port, unless Preferences turns it off.
|
||||
@@ -24,13 +23,10 @@ mod control_server;
|
||||
mod device_lost;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod native_crash;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod native_crash_macos;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod native_frame;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod native_menu;
|
||||
mod nav_bridge;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod open_documents;
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
@@ -90,8 +86,6 @@ struct App {
|
||||
space_mouse: space_mouse::SpaceMouse,
|
||||
#[cfg(target_os = "macos")]
|
||||
native_menu: Option<native_menu::NativeMenu>,
|
||||
/// A device's own app moving the view (Preferences › Navigation, off by default).
|
||||
nav_bridge: nav_bridge::NavBridge,
|
||||
}
|
||||
|
||||
impl eframe::App for App {
|
||||
@@ -116,13 +110,6 @@ impl eframe::App for App {
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::Focus);
|
||||
}
|
||||
solidcraft_engine::crash::phase(solidcraft_engine::crash::RUNNING);
|
||||
// A DMG install has no installer step: put solidcraft-cli on PATH, quietly.
|
||||
#[cfg(target_os = "macos")]
|
||||
std::thread::spawn(solidcraft_ui_egui::cli_link::install_on_launch);
|
||||
// Automation runs (a control port was asked for) leave the user's AI apps alone.
|
||||
if !self.take_focus_unwanted {
|
||||
solidcraft_ui_egui::ai_connect::begin(&mut self.solve);
|
||||
}
|
||||
if let Some(dir) = self.starting.take() {
|
||||
startup::started(&dir);
|
||||
}
|
||||
@@ -150,8 +137,6 @@ impl eframe::App for App {
|
||||
if !self.solve.file_picker_busy() {
|
||||
self.space_mouse.tick(&mut self.solve, ctx);
|
||||
}
|
||||
// Every frame, so switching the bridge off reaches the reader while a picker is open too.
|
||||
self.nav_bridge.tick(&mut self.solve, ctx);
|
||||
if self.solve.quit_requested {
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
||||
}
|
||||
@@ -161,8 +146,6 @@ impl eframe::App for App {
|
||||
if let Some(menu) = &mut self.native_menu {
|
||||
menu.raw_input_hook(&mut self.solve, ctx, raw);
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
macos_fullscreen_content(ctx, raw);
|
||||
// Before the app's own hook, which adds the control channel's events.
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
self.space_mouse.raw_input_hook(self.solve.preferences.space_mouse_enabled, raw);
|
||||
@@ -172,7 +155,7 @@ impl eframe::App for App {
|
||||
self.solve.raw_input_hook(raw);
|
||||
}
|
||||
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
||||
content_ui(&mut self.solve, ui);
|
||||
self.solve.ui(ui);
|
||||
}
|
||||
fn save(&mut self, storage: &mut dyn eframe::Storage) {
|
||||
storage.set_string(solidcraft_ui_egui::prefs::STORAGE_KEY, self.solve.prefs());
|
||||
@@ -182,17 +165,7 @@ impl eframe::App for App {
|
||||
solidcraft_ui_egui::agent_chat::shutdown(&mut self.solve);
|
||||
// A save still being written ends before the app does.
|
||||
solidcraft_ui_egui::save_job::tick(&mut self.solve, true);
|
||||
// Pictures still being drawn off the window thread (Home cards, file previews) end
|
||||
// before the GPU is torn down: one drawn while the process exits can crash the driver.
|
||||
if !solidcraft_engine::capture::close(std::time::Duration::from_secs(5)) {
|
||||
log::warn!("a picture was still being drawn as the app closed");
|
||||
}
|
||||
solidcraft_ui_egui::recovery_ui::close(&mut self.solve);
|
||||
solidcraft_ui_egui::open_elsewhere::close(&mut self.solve);
|
||||
// Before a restart below starts the next instance, which would otherwise get nothing from
|
||||
// the 3D mouse driver (`solidcraft_3dx_client::stop`).
|
||||
#[cfg(target_os = "macos")]
|
||||
solidcraft_3dx_client::stop();
|
||||
solidcraft_engine::crash::end_session();
|
||||
if let Some(lost_at) = self.gpu.as_ref().and_then(|g| g.lost_at) {
|
||||
device_lost::restart(self.gpu.as_ref().is_some_and(|g| g.restart_on_software), lost_at);
|
||||
@@ -258,37 +231,6 @@ fn case_variants(exts: &[&str]) -> Vec<String> {
|
||||
all
|
||||
}
|
||||
|
||||
/// egui's root Ui spans the whole viewport, including native safe areas. Lay out the app
|
||||
/// inside the unobscured rectangle so its application bar can receive native pointer input.
|
||||
fn content_ui(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
let content = ui.ctx().content_rect();
|
||||
let above = egui::Rect::from_min_max(ui.max_rect().min, egui::pos2(ui.max_rect().right(), content.top()));
|
||||
ui.painter().rect_filled(above, 0.0, solidcraft_ui_egui::theme::Tokens::get().app_bar);
|
||||
ui.scope_builder(egui::UiBuilder::new().max_rect(content), |ui| app.ui(ui));
|
||||
}
|
||||
|
||||
/// A transparent macOS title bar still intercepts clicks at the top in fullscreen (#3643).
|
||||
/// `contentLayoutRect` reports the whole content view once that bar auto-hides, so reserve
|
||||
/// the native titled-window height even then. AppKit supplies the height in logical points;
|
||||
/// egui's UI zoom is separate from the display's Retina scale.
|
||||
#[cfg(target_os = "macos")]
|
||||
fn macos_fullscreen_content(ctx: &egui::Context, raw: &mut egui::RawInput) {
|
||||
let Some(mtm) = objc2::MainThreadMarker::new() else { return };
|
||||
let frame = objc2_app_kit::NSWindow::frameRectForContentRect_styleMask(Default::default(), objc2_app_kit::NSWindowStyleMask::Titled, mtm);
|
||||
fullscreen_content_insets(raw, frame.size.height as f32, ctx.zoom_factor());
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "macos", test))]
|
||||
fn fullscreen_content_insets(raw: &mut egui::RawInput, title_height: f32, zoom: f32) {
|
||||
let fullscreen = raw.viewports.get(&raw.viewport_id).and_then(|v| v.fullscreen).unwrap_or(false);
|
||||
let mut insets = raw.safe_area_insets.unwrap_or_default();
|
||||
if fullscreen && title_height.is_finite() && title_height > 0.0 && zoom.is_finite() && zoom > 0.0 {
|
||||
insets.0.top = insets.0.top.max(title_height / zoom);
|
||||
}
|
||||
// Explicitly reset on leaving fullscreen: None would retain the preceding frame's inset.
|
||||
raw.safe_area_insets = Some(insets);
|
||||
}
|
||||
|
||||
fn services(cc: &eframe::CreationContext<'_>) -> Services {
|
||||
let parent = rfd::AsyncFileDialog::new().set_parent(cc);
|
||||
let open_parent = parent.clone();
|
||||
@@ -303,9 +245,7 @@ fn services(cc: &eframe::CreationContext<'_>) -> Services {
|
||||
dialog = dialog.add_filter(label, &case_variants(first));
|
||||
}
|
||||
// The groups come from the format registry, so a new format joins this dialog by itself.
|
||||
let mut everything = io::formats::open_extensions(true);
|
||||
everything.extend(["dxf", "svg"]);
|
||||
dialog = dialog.add_filter(solidcraft_ui_egui::i18n::tr("file.open.drawing_filter"), &case_variants(&everything));
|
||||
dialog = dialog.add_filter("Designs, CAD files and meshes", &case_variants(&io::formats::open_extensions(true)));
|
||||
for (label, exts) in io::formats::import_filters(true) {
|
||||
dialog = dialog.add_filter(label, &case_variants(&exts));
|
||||
}
|
||||
@@ -348,7 +288,6 @@ fn services(cc: &eframe::CreationContext<'_>) -> Services {
|
||||
let future = dialog.save_file();
|
||||
Box::pin(async move { future.await.map(|p| p.path().to_string_lossy().into_owned()) })
|
||||
})),
|
||||
graphics_adapters: Vec::new(),
|
||||
graphics_path: graphics_path().map(|p| p.to_string_lossy().to_string()),
|
||||
window_system: window_system_now(),
|
||||
http_get: Some(std::sync::Arc::new(http_get)),
|
||||
@@ -358,7 +297,6 @@ fn services(cc: &eframe::CreationContext<'_>) -> Services {
|
||||
can_restore: None,
|
||||
restore_file: None,
|
||||
download_design: None,
|
||||
easyprint_upload: Some(std::sync::Arc::new(easyprint_upload)),
|
||||
browser_files: None,
|
||||
agent_http: Some(std::sync::Arc::new(agent_http)),
|
||||
secrets: Some(std::sync::Arc::new(Keychain)),
|
||||
@@ -430,7 +368,7 @@ fn agent_http(r: &solidcraft_ui_egui::agent_chat::backend::HttpRequest) -> Resul
|
||||
let agent: ureq::Agent = ureq::Agent::config_builder()
|
||||
.tls_config(tls)
|
||||
.http_status_as_error(false)
|
||||
.timeout_global(Some(Duration::from_secs(r.timeout_s.clamp(1, 3600))))
|
||||
.timeout_global(Some(Duration::from_secs(r.timeout_s.clamp(1, 900))))
|
||||
.timeout_connect(Some(Duration::from_secs(10)))
|
||||
.user_agent("SolidCraft")
|
||||
.max_redirects(0)
|
||||
@@ -455,24 +393,6 @@ fn agent_http(r: &solidcraft_ui_egui::agent_chat::backend::HttpRequest) -> Resul
|
||||
Ok((status, body))
|
||||
}
|
||||
|
||||
/// Open with EasyPrint: upload the model to Printables' import endpoint on a worker and answer on
|
||||
/// `done` with the address to open. Through [`agent_http`]: no redirects, a timeout, no cookies.
|
||||
fn easyprint_upload(name: &str, bytes: Vec<u8>, done: std::sync::mpsc::Sender<Result<String, String>>) {
|
||||
use solidcraft_engine::easyprint;
|
||||
let name = name.to_string();
|
||||
std::thread::spawn(move || {
|
||||
let (content_type, body) = easyprint::multipart(&name, &bytes);
|
||||
let request = solidcraft_ui_egui::agent_chat::backend::HttpRequest {
|
||||
method: "POST",
|
||||
url: easyprint::IMPORT_URL.into(),
|
||||
headers: vec![("Content-Type".into(), content_type), ("Accept".into(), "application/json".into())],
|
||||
body: Some(body),
|
||||
timeout_s: 180,
|
||||
};
|
||||
let _ = done.send(agent_http(&request).and_then(|(status, body)| easyprint::parse_import_response(status, &body)));
|
||||
});
|
||||
}
|
||||
|
||||
/// API keys in the system keychain (service "SolidCraft agent chat", one entry per agent).
|
||||
struct Keychain;
|
||||
|
||||
@@ -539,15 +459,14 @@ fn http_call(url: &str, timeout: std::time::Duration) -> Result<ureq::http::Resp
|
||||
}
|
||||
|
||||
/// The per-user settings folder: eframe's storage (`app.ron`) and the anti-aliasing preference.
|
||||
fn settings_dir() -> Result<Option<std::path::PathBuf>, String> {
|
||||
let selected = std::env::var_os("SOLIDCRAFT_SETTINGS_DIR");
|
||||
startup::settings_directory(selected.as_deref(), eframe::storage_dir("SolidCraft"))
|
||||
fn settings_dir() -> Option<std::path::PathBuf> {
|
||||
eframe::storage_dir("SolidCraft")
|
||||
}
|
||||
|
||||
/// The anti-aliasing preference, kept next to eframe's storage (it is needed before the
|
||||
/// window opens).
|
||||
fn graphics_path() -> Option<std::path::PathBuf> {
|
||||
settings_dir().ok().flatten().map(|d| d.join("graphics.json"))
|
||||
settings_dir().map(|d| d.join("graphics.json"))
|
||||
}
|
||||
|
||||
/// Compatibility shim (docs/compatibility.md): on the first launch after the rename, copy the
|
||||
@@ -557,7 +476,7 @@ fn graphics_path() -> Option<std::path::PathBuf> {
|
||||
/// old app id's `~/.var/app` folder; that copy comes first, as it is the newest. The old folders
|
||||
/// are never changed.
|
||||
fn adopt_legacy_settings() {
|
||||
let (Some(current), Some(legacy)) = (settings_dir().ok().flatten(), eframe::storage_dir("SolveCraft")) else { return };
|
||||
let (Some(current), Some(legacy)) = (settings_dir(), eframe::storage_dir("SolveCraft")) else { return };
|
||||
let legacy_eframe = eframe::storage_dir(solidcraft_engine::io::LEGACY_APP_ID);
|
||||
let old_flatpak = solidcraft_engine::io::legacy_flatpak_path(¤t);
|
||||
if let Err(e) = adopt_settings(¤t, old_flatpak.as_deref(), &legacy, legacy_eframe.as_deref()) {
|
||||
@@ -608,22 +527,13 @@ fn scene_samples(backend: eframe::wgpu::Backend, samples: u32, supported: &[u32]
|
||||
}
|
||||
}
|
||||
|
||||
/// Keep the default until the installed renderer's startup check proves a fallback is needed.
|
||||
fn scene_msaa_request(saved: Option<u16>) -> u16 {
|
||||
saved.unwrap_or(MSAA)
|
||||
}
|
||||
|
||||
/// Query the scene's colour/depth intersection, rather than the depthless window's counts.
|
||||
fn scene_sample_choices(depth_bits: u8, stencil_bits: u8, query: impl FnOnce(Option<eframe::wgpu::TextureFormat>) -> Vec<u32>) -> Vec<u32> {
|
||||
query(eframe::egui_wgpu::depth_format_from_bits(depth_bits, stencil_bits))
|
||||
}
|
||||
|
||||
/// An explicitly saved anti-aliasing choice, absent until Graphics changes it.
|
||||
fn msaa() -> Option<u16> {
|
||||
/// Anti-aliasing samples from Preferences (4 when unset).
|
||||
fn msaa() -> u16 {
|
||||
graphics()
|
||||
.and_then(|v| v.get("msaa").and_then(serde_json::Value::as_u64))
|
||||
.and_then(|m| u16::try_from(m).ok())
|
||||
.filter(|m| [1, 2, 4, 8].contains(m))
|
||||
.unwrap_or(MSAA)
|
||||
}
|
||||
|
||||
/// Windows and Linux: no OS title bar; the application bar is the title bar
|
||||
@@ -639,8 +549,7 @@ const FLOAT_DEPTH_BITS: u8 = 32;
|
||||
const STENCIL_BITS: u8 = 8;
|
||||
const MSAA: u16 = 4;
|
||||
|
||||
/// `custom_titlebar`: no OS decorations, the application bar is the title bar.
|
||||
fn app_viewport(custom_titlebar: bool) -> Result<egui::ViewportBuilder, eframe::Error> {
|
||||
fn app_viewport() -> Result<egui::ViewportBuilder, eframe::Error> {
|
||||
// Let macOS use the bundle's layered icon; eframe's default bitmap would replace it.
|
||||
#[cfg(target_os = "macos")]
|
||||
let icon = egui::IconData::default();
|
||||
@@ -653,7 +562,7 @@ fn app_viewport(custom_titlebar: bool) -> Result<egui::ViewportBuilder, eframe::
|
||||
.with_inner_size([1600.0, 1000.0])
|
||||
.with_min_inner_size([960.0, 600.0])
|
||||
.with_drag_and_drop(true)
|
||||
.with_decorations(!custom_titlebar)
|
||||
.with_decorations(!CUSTOM_TITLEBAR)
|
||||
.with_fullsize_content_view(true)
|
||||
.with_titlebar_shown(false)
|
||||
.with_title_shown(false)
|
||||
@@ -663,29 +572,11 @@ fn app_viewport(custom_titlebar: bool) -> Result<egui::ViewportBuilder, eframe::
|
||||
/// The window's app id: it names the desktop entry, so it is the packages' reverse-DNS id.
|
||||
const APP_ID: &str = solidcraft_engine::io::APP_ID;
|
||||
|
||||
/// Disconnects from the 3D mouse driver when dropped, as `main` returns or a panic unwinds out
|
||||
/// of it (`solidcraft_3dx_client::stop`).
|
||||
#[cfg(target_os = "macos")]
|
||||
struct DriverClient;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
impl Drop for DriverClient {
|
||||
fn drop(&mut self) {
|
||||
solidcraft_3dx_client::stop();
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> eframe::Result {
|
||||
#[cfg(target_os = "macos")]
|
||||
objc2_foundation::NSProcessInfo::processInfo().setProcessName(objc2_foundation::ns_string!("SolidCraft"));
|
||||
mark("main");
|
||||
// SOLIDCRAFT_PROFILE=1: the profiler records from the start (diagnostics.profile reads it).
|
||||
solidcraft_engine::timing::profile_from_env();
|
||||
// Validate before legacy adoption, crash markers or any user preference read.
|
||||
let settings = settings_dir().map_err(|error| eframe::Error::AppCreation(error.into()))?;
|
||||
if std::env::var_os("SOLIDCRAFT_SETTINGS_DIR").is_none() {
|
||||
adopt_legacy_settings();
|
||||
}
|
||||
adopt_legacy_settings();
|
||||
let mut safe_mode = startup::env_on(std::env::var(startup::SAFE_MODE_ENV).ok().as_deref());
|
||||
let mut control_port: Option<u16> = solidcraft_engine::geom::env::var("CONTROL_PORT").and_then(|p| p.parse().ok());
|
||||
let mut files = Vec::new();
|
||||
@@ -722,6 +613,7 @@ fn main() -> eframe::Result {
|
||||
}
|
||||
// Settings that don't read are moved aside; launches that never got a window are counted,
|
||||
// and after a few in a row this one starts in safe mode (docs/book/src/guide/troubleshooting.md).
|
||||
let settings = settings_dir();
|
||||
let mut notes: Vec<String> = Vec::new();
|
||||
let mut failed_starts = 0;
|
||||
if let Some(dir) = &settings {
|
||||
@@ -741,13 +633,7 @@ fn main() -> eframe::Result {
|
||||
if safe_mode {
|
||||
notes.push("Safe mode: default settings (yours are kept for the next normal start), no recovery offer, software rendering.".into());
|
||||
}
|
||||
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}");
|
||||
notes.push(note.clone());
|
||||
}
|
||||
let saved_samples = if safe_mode { None } else { msaa() };
|
||||
let samples = if safe_mode { MSAA } else { msaa() };
|
||||
let wgpu_options = wgpu_options(cpu_renderer, if safe_mode { None } else { backend_preference() }.as_deref());
|
||||
let depth_bits = if !safe_mode && startup::float_depth_everywhere(&wgpu_options) { FLOAT_DEPTH_BITS } else { DEPTH_BITS };
|
||||
// Crashes leave a report in the crash folder (solidcraft_engine::crash, docs/crash-reports.md).
|
||||
@@ -761,22 +647,14 @@ fn main() -> eframe::Result {
|
||||
solidcraft_engine::crash::install(dir, build)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
// A crash in native code (a graphics driver) says where it happened: the exception or signal
|
||||
// and the module on Windows and macOS, and on every system the startup step the app was in.
|
||||
// A crash in native code (a graphics driver) says where it happened: the exception and the
|
||||
// module on Windows, and on every system the startup step the app was in.
|
||||
#[cfg(target_os = "windows")]
|
||||
native_crash::install();
|
||||
#[cfg(target_os = "macos")]
|
||||
native_crash_macos::install();
|
||||
// Windows with only OpenGL to start: the system title bar instead of ours (#2063).
|
||||
let started = match &wgpu_options.wgpu_setup {
|
||||
eframe::egui_wgpu::WgpuSetup::CreateNew(setup) => setup.instance_descriptor.backends,
|
||||
_ => eframe::wgpu::Backends::all(),
|
||||
};
|
||||
let custom_titlebar = CUSTOM_TITLEBAR && !startup::system_title_bar(cfg!(target_os = "windows"), started);
|
||||
eframe::egui_wgpu::set_startup_hook(startup_step);
|
||||
solidcraft_engine::crash::phase("window");
|
||||
let options = eframe::NativeOptions {
|
||||
viewport: app_viewport(custom_titlebar)?,
|
||||
viewport: app_viewport()?,
|
||||
// The window's pass (egui's) has no depth and one sample: the viewport draws its scene
|
||||
// offscreen with its own depth and MSAA, then blits it, so the UI pass pays for neither.
|
||||
depth_buffer: 0,
|
||||
@@ -827,10 +705,6 @@ fn main() -> eframe::Result {
|
||||
let created = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||
let created_in_app = created.clone();
|
||||
let starting = settings.clone();
|
||||
// A panic on the window's thread ends the app without `on_exit`; this disconnects from the
|
||||
// 3D mouse driver then too.
|
||||
#[cfg(target_os = "macos")]
|
||||
let _driver_client = DriverClient;
|
||||
mark("run_native");
|
||||
let result = eframe::run_native(
|
||||
"SolidCraft",
|
||||
@@ -844,24 +718,18 @@ fn main() -> eframe::Result {
|
||||
mark("fonts");
|
||||
let mut app = SolveApp::new(Session::default(), services(cc));
|
||||
app.apply_off_thread = Some(solidcraft_ui_egui::dialogs::APPLY_AT_ONCE);
|
||||
app.custom_titlebar = custom_titlebar;
|
||||
app.custom_titlebar = CUSTOM_TITLEBAR;
|
||||
// Windows: the OS hit test offers the resize border, at the window's DPI, from the
|
||||
// first message (#1381).
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// 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");
|
||||
}
|
||||
// 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 {
|
||||
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");
|
||||
}
|
||||
}
|
||||
app.integrated_titlebar = cfg!(target_os = "macos");
|
||||
app.cpu_meshing = safe_mode;
|
||||
app.graphics_fallback = fallback_note.clone();
|
||||
if let Some(p) = cc.storage.and_then(|s| solidcraft_ui_egui::prefs::stored(|k| s.get_string(k))).filter(|_| !safe_mode) {
|
||||
app.load_prefs(&p);
|
||||
}
|
||||
@@ -871,37 +739,19 @@ fn main() -> eframe::Result {
|
||||
restart_on_software: device_lost::restart_on_software(after_loss, cpu_renderer),
|
||||
lost_at: None,
|
||||
});
|
||||
if !safe_mode {
|
||||
app.preferences.graphics_adapter =
|
||||
graphics().and_then(|v| v.get("adapter").and_then(serde_json::Value::as_str).map(str::to_string)).unwrap_or_default();
|
||||
}
|
||||
if let Some(rs) = &cc.wgpu_render_state {
|
||||
app.services.graphics_adapters = rs
|
||||
.available_adapters
|
||||
.iter()
|
||||
.map(|adapter| {
|
||||
let info = adapter.get_info();
|
||||
(info.name.clone(), format!("{} ({:?}, {:?})", info.name, info.device_type, info.backend))
|
||||
})
|
||||
.collect();
|
||||
let info = rs.adapter.get_info();
|
||||
let samples = scene_msaa_request(saved_samples);
|
||||
let supported = scene_sample_choices(depth_bits, STENCIL_BITS, |depth| {
|
||||
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);
|
||||
let scene_samples = scene_samples(rs.adapter.get_info().backend, u32::from(samples), &rs.msaa_supported);
|
||||
eprintln!("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;
|
||||
app.preferences.msaa = u32::from(samples);
|
||||
app.set_wgpu_split(rs, depth_bits, STENCIL_BITS, scene_samples);
|
||||
solidcraft_ui_egui::gpu::install_picture_renderer(rs);
|
||||
// The graphics driver is loaded now; it may have put its own exception filter
|
||||
// in front of ours.
|
||||
#[cfg(target_os = "windows")]
|
||||
native_crash::reinstall();
|
||||
// Reflect a confirmed probe fallback or an unsupported saved count in Graphics.
|
||||
// A saved anti-aliasing the device can't do: back to the default, with a notice.
|
||||
solidcraft_ui_egui::prefs::check_msaa(&mut app, u32::from(samples));
|
||||
let info = rs.adapter.get_info();
|
||||
solidcraft_engine::crash::set_environment(&format!(
|
||||
"Renderer: {:?} {} ({}), MSAA {scene_samples}x",
|
||||
info.backend, info.name, info.driver
|
||||
@@ -926,19 +776,12 @@ fn main() -> eframe::Result {
|
||||
app.session.mark_saved();
|
||||
app.session.undo.clear();
|
||||
}
|
||||
// Lock files next to open designs: a design open on another computer says so (#5467),
|
||||
// the files given at launch included.
|
||||
solidcraft_ui_egui::open_elsewhere::start(&mut app);
|
||||
// A launch from Finder: its files arrived before the window.
|
||||
#[cfg(target_os = "macos")]
|
||||
let files = {
|
||||
open_documents::set_ui(&cc.egui_ctx);
|
||||
[files, open_documents::take()].concat()
|
||||
};
|
||||
// No design given: the designs open at the last quit, when the preference asks (#5542).
|
||||
// Not when driven over the control channel, whose clients expect a fresh design.
|
||||
let files =
|
||||
if files.is_empty() && !sample && control_port.is_none() { solidcraft_ui_egui::documents::designs_to_reopen(&app) } else { files };
|
||||
for f in &files {
|
||||
app.open_path_async(f);
|
||||
}
|
||||
@@ -967,9 +810,8 @@ fn main() -> eframe::Result {
|
||||
if !notes.is_empty() {
|
||||
app.set_status(notes.join(" "), true);
|
||||
}
|
||||
let nav_bridge = nav_bridge::NavBridge::start(&cc.egui_ctx);
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
let space_mouse = space_mouse::SpaceMouse::start(&cc.egui_ctx, nav_bridge.claim());
|
||||
let space_mouse = space_mouse::SpaceMouse::start(&cc.egui_ctx);
|
||||
#[cfg(target_os = "macos")]
|
||||
let native_menu = match native_menu::NativeMenu::new(&app, &cc.egui_ctx) {
|
||||
Ok(menu) => Some(menu),
|
||||
@@ -994,13 +836,12 @@ fn main() -> eframe::Result {
|
||||
space_mouse,
|
||||
#[cfg(target_os = "macos")]
|
||||
native_menu,
|
||||
nav_bridge,
|
||||
}))
|
||||
}),
|
||||
);
|
||||
control_server::remove_token_file();
|
||||
match result {
|
||||
Err(e) if !created.load(std::sync::atomic::Ordering::SeqCst) => no_window(e, cpu_renderer, started, settings.as_deref()),
|
||||
Err(e) if !created.load(std::sync::atomic::Ordering::SeqCst) => no_window(e, cpu_renderer, settings.as_deref()),
|
||||
r => r,
|
||||
}
|
||||
}
|
||||
@@ -1021,19 +862,13 @@ fn wgpu_options(cpu: bool, preference: Option<&str>) -> eframe::egui_wgpu::WgpuC
|
||||
let mut options = eframe::egui_wgpu::WgpuConfiguration::default();
|
||||
if let eframe::egui_wgpu::WgpuSetup::CreateNew(setup) = &mut options.wgpu_setup {
|
||||
let env_set = std::env::var(startup::WGPU_BACKEND_ENV).is_ok_and(|v| !v.trim().is_empty());
|
||||
// A retry applies only to this launch. WGPU_BACKEND always has priority.
|
||||
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:?}");
|
||||
setup.instance_descriptor.backends = backends;
|
||||
}
|
||||
let intel_mac = cfg!(all(target_os = "macos", target_arch = "x86_64"));
|
||||
let indirect = std::env::var(startup::INDIRECT_VALIDATION_ENV).ok();
|
||||
setup.instance_descriptor.flags = startup::window_instance_flags(setup.instance_descriptor.flags, intel_mac, indirect.as_deref());
|
||||
let environment = std::env::var("WGPU_ADAPTER_NAME").ok();
|
||||
let adapter = graphics().and_then(|v| v.get("adapter").and_then(serde_json::Value::as_str).map(str::to_string));
|
||||
startup::configure_adapter(setup, cpu, environment, adapter);
|
||||
if cpu {
|
||||
setup.native_adapter_selector = Some(std::sync::Arc::new(startup::pick_cpu_adapter));
|
||||
}
|
||||
// A float depth buffer with a stencil, wherever the adapter has it (see `FLOAT_DEPTH_BITS`).
|
||||
let base = setup.device_descriptor.clone();
|
||||
setup.device_descriptor = std::sync::Arc::new(move |adapter| {
|
||||
@@ -1045,47 +880,24 @@ fn wgpu_options(cpu: bool, preference: Option<&str>) -> eframe::egui_wgpu::WgpuC
|
||||
options
|
||||
}
|
||||
|
||||
/// A failed renderer returns here before app creation. A fresh process avoids reusing winit's
|
||||
/// 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 {
|
||||
/// The window never opened (no graphics adapter, a device that wouldn't start): once more on
|
||||
/// the software renderer, else a message box saying what failed instead of a silent exit.
|
||||
fn no_window(error: eframe::Error, tried_cpu: bool, settings: Option<&std::path::Path>) -> eframe::Result {
|
||||
log::error!("the window could not open: {error}");
|
||||
eprintln!("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);
|
||||
}
|
||||
solidcraft_engine::crash::end_session();
|
||||
let explicit = std::env::var(startup::WGPU_BACKEND_ENV).is_ok_and(|v| !v.trim().is_empty());
|
||||
if let Some(next) = startup::fallback_backend(cfg!(target_os = "windows"), explicit, tried_cpu, failed)
|
||||
&& let Ok(exe) = std::env::current_exe()
|
||||
{
|
||||
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}");
|
||||
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" {
|
||||
command.env(startup::RENDERER_ENV, "cpu").env_remove(startup::RETRY_BACKEND_ENV);
|
||||
} else {
|
||||
command.env(startup::RETRY_BACKEND_ENV, next);
|
||||
}
|
||||
match command.status() {
|
||||
if !tried_cpu && let Ok(exe) = std::env::current_exe() {
|
||||
eprintln!("SolidCraft: trying again with the software renderer ({}=cpu)", startup::RENDERER_ENV);
|
||||
match std::process::Command::new(exe).args(std::env::args_os().skip(1)).env(startup::RENDERER_ENV, "cpu").status() {
|
||||
Ok(status) if status.success() => return Ok(()),
|
||||
// The final try showed its own message.
|
||||
// The second try showed its own message.
|
||||
Ok(_) => return Err(error),
|
||||
Err(e) => log::error!("could not start again on {next}: {e}"),
|
||||
Err(e) => log::error!("could not start again with the software renderer: {e}"),
|
||||
}
|
||||
}
|
||||
let message = if explicit {
|
||||
format!("SolidCraft could not open its window with WGPU_BACKEND.\n\n{error}\n\nChange or remove WGPU_BACKEND to try another graphics API.")
|
||||
} else {
|
||||
startup::no_window_message(&error.to_string(), settings)
|
||||
};
|
||||
let _ = rfd::MessageDialog::new()
|
||||
.set_level(rfd::MessageLevel::Error)
|
||||
.set_title("SolidCraft could not start")
|
||||
.set_description(message)
|
||||
.set_description(startup::no_window_message(&error.to_string(), settings))
|
||||
.set_buttons(rfd::MessageButtons::Ok)
|
||||
.show();
|
||||
Err(error)
|
||||
@@ -1174,11 +986,11 @@ mod backend_tests {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod viewport_tests {
|
||||
mod icon_tests {
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn macos_preserves_the_bundle_icon_instead_of_setting_a_runtime_bitmap() {
|
||||
let viewport = super::app_viewport(super::CUSTOM_TITLEBAR).expect("viewport");
|
||||
let viewport = super::app_viewport().expect("viewport");
|
||||
assert_eq!(viewport.icon.as_deref(), Some(&eframe::egui::IconData::default()));
|
||||
assert_eq!(viewport.app_id.as_deref(), Some(super::APP_ID));
|
||||
}
|
||||
@@ -1186,89 +998,12 @@ mod viewport_tests {
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[test]
|
||||
fn desktop_viewport_supplies_the_original_mark_to_the_window() {
|
||||
let viewport = super::app_viewport(super::CUSTOM_TITLEBAR).expect("viewport");
|
||||
let viewport = super::app_viewport().expect("viewport");
|
||||
let icon = viewport.icon.expect("running window must have an icon");
|
||||
assert_eq!((icon.width, icon.height), (64, 64));
|
||||
assert_eq!(icon.rgba, super::app_icon::rgba(64).expect("original mark"));
|
||||
assert_eq!(viewport.app_id.as_deref(), Some(super::APP_ID));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fullscreen_bar_clears_native_chrome_and_undo_redo_keep_working() {
|
||||
use egui::{Event, PointerButton, Pos2, RawInput, Rect, ViewportId, vec2};
|
||||
use serde_json::json;
|
||||
use solidcraft_ui_egui::scenario::handle_at;
|
||||
|
||||
// AppKit points are independent of Retina scale and of the user's UI zoom.
|
||||
for (zoom, retina) in [(1.0, 1.0), (1.0, 2.0), (1.5, 2.0), (2.0, 2.0)] {
|
||||
let ctx = egui::Context::default();
|
||||
solidcraft_ui_egui::fonts::setup_bundled_fonts(&ctx);
|
||||
ctx.set_zoom_factor(zoom);
|
||||
let mut app = super::SolveApp::new(super::Session::default(), Default::default());
|
||||
app.integrated_titlebar = true;
|
||||
for (command, params) in [
|
||||
("sketch.create", json!({"plane":"XY"})),
|
||||
("sketch.rectangle.two_point", json!({"p0":[0,0],"p1":[40,30]})),
|
||||
("sketch.finish", json!({})),
|
||||
("solid.extrude", json!({"distance":15})),
|
||||
] {
|
||||
app.run(command, params).unwrap();
|
||||
}
|
||||
let mut time = 0.0;
|
||||
let mut frame = |app: &mut super::SolveApp, fullscreen, events| {
|
||||
time += 0.05;
|
||||
let mut raw = RawInput {
|
||||
screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(1600.0, 1000.0))),
|
||||
time: Some(time),
|
||||
events,
|
||||
..Default::default()
|
||||
};
|
||||
let viewport = raw.viewports.get_mut(&ViewportId::ROOT).unwrap();
|
||||
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));
|
||||
};
|
||||
// Enter, leave, and enter again: no stale inset when returning to a normal window.
|
||||
for fullscreen in [false, true, false, true] {
|
||||
frame(&mut app, fullscreen, vec![]);
|
||||
frame(&mut app, fullscreen, vec![]);
|
||||
let top = if fullscreen { 32.0 / zoom } else { 0.0 };
|
||||
for name in ["appbar:file", "appbar:undo", "appbar:redo", "appbar:help"] {
|
||||
let at = handle_at(name).unwrap();
|
||||
assert!((at.y - top - 17.0).abs() < 0.1, "{name}: {at:?}, fullscreen={fullscreen}, zoom={zoom}");
|
||||
}
|
||||
for (handle, bodies) in [("appbar:undo", 0), ("appbar:redo", 1)] {
|
||||
let at = handle_at(handle).unwrap();
|
||||
frame(&mut app, fullscreen, vec![Event::PointerMoved(at)]);
|
||||
for pressed in [true, false] {
|
||||
frame(
|
||||
&mut app,
|
||||
fullscreen,
|
||||
vec![Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Default::default() }],
|
||||
);
|
||||
}
|
||||
assert_eq!(app.session.model.state().bodies.len(), bodies, "{handle}, fullscreen={fullscreen}, zoom={zoom}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fullscreen_inset_preserves_existing_safe_areas_and_resets_on_exit() {
|
||||
use egui::epaint::MarginF32;
|
||||
use egui::{RawInput, SafeAreaInsets, ViewportId};
|
||||
let mut raw = RawInput::default();
|
||||
raw.viewports.get_mut(&ViewportId::ROOT).unwrap().fullscreen = Some(true);
|
||||
raw.safe_area_insets = Some(SafeAreaInsets(MarginF32 { top: 40.0, left: 5.0, right: 6.0, bottom: 7.0 }));
|
||||
super::fullscreen_content_insets(&mut raw, 32.0, 2.0);
|
||||
assert_eq!(raw.safe_area_insets.unwrap().0, MarginF32 { top: 40.0, left: 5.0, right: 6.0, bottom: 7.0 });
|
||||
|
||||
raw.viewports.get_mut(&ViewportId::ROOT).unwrap().fullscreen = Some(false);
|
||||
raw.safe_area_insets = None;
|
||||
super::fullscreen_content_insets(&mut raw, 32.0, 1.0);
|
||||
assert_eq!(raw.safe_area_insets, Some(SafeAreaInsets::default()));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -1306,34 +1041,6 @@ mod msaa_tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scene_sample_choices_include_the_scene_depth_format() {
|
||||
use eframe::wgpu::TextureFormat;
|
||||
for (bits, expected) in [(24, TextureFormat::Depth24PlusStencil8), (32, TextureFormat::Depth32FloatStencil8)] {
|
||||
// A synthetic colour target supports 8x, but the scene depth supports only 1x/4x.
|
||||
let supported = super::scene_sample_choices(bits, 8, |depth| match depth {
|
||||
Some(format) if format == expected => vec![1, 4],
|
||||
None => vec![1, 4, 8],
|
||||
other => panic!("unexpected scene depth: {other:?}"),
|
||||
});
|
||||
assert_eq!(supported, [1, 4]);
|
||||
assert_eq!(super::scene_samples(eframe::wgpu::Backend::Dx12, 8, &supported), 4);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scene_msaa_keeps_four_samples_by_default_and_explicit_choices() {
|
||||
use eframe::wgpu::Backend;
|
||||
let default = super::scene_msaa_request(None);
|
||||
assert_eq!(default, 4);
|
||||
for backend in [Backend::Dx12, Backend::Vulkan, Backend::Metal] {
|
||||
assert_eq!(super::scene_samples(backend, u32::from(default), &[1, 4]), 4);
|
||||
}
|
||||
for saved in [1, 2, 4, 8] {
|
||||
assert_eq!(super::scene_msaa_request(Some(saved)), saved);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_scene_follows_the_backend_policy_too() {
|
||||
use eframe::wgpu::Backend;
|
||||
|
||||
@@ -1,879 +0,0 @@
|
||||
//! macOS: what a crash in native code leaves behind (docs/crash-reports.md).
|
||||
//!
|
||||
//! A crash inside a graphics driver or another library (a segfault, an `abort()`, a stack
|
||||
//! overflow) is no Rust panic: the panic hook never runs and the next start only found "an
|
||||
//! unclean exit". A signal handler for SIGSEGV, SIGBUS, SIGILL, SIGFPE, SIGTRAP (how a trap
|
||||
//! instruction arrives on Apple silicon) and SIGABRT now writes, before the process goes, the
|
||||
//! same small JSON file as the Windows filter (`native_crash.rs`): the signal, the faulting
|
||||
//! module and the offset into it, and a short frame-pointer walk, next to the run's crash journal
|
||||
//! (`solidcraft_engine::crash::native_paths`). The next start puts it into the unclean exit's
|
||||
//! report. The signal then goes on to the handler that was there before (the standard library's
|
||||
//! stack overflow message) or to the default action, so the process still dies of it and macOS
|
||||
//! still writes its own crash report (`~/Library/Logs/DiagnosticReports`; the file names the
|
||||
//! `pid` the JSON records).
|
||||
//!
|
||||
//! Installing a signal handler and asking dyld which image holds an address have no safe API,
|
||||
//! hence the `unsafe_code` allowance on this module (the app crate denies it). The handler runs
|
||||
//! on a thread that may have a broken heap, or on the small alternate stack after a stack
|
||||
//! overflow: it doesn't allocate, formats into a buffer on the stack, and only reads memory on
|
||||
//! the thread's own stack. It writes what it knows from the signal first and only then calls
|
||||
//! `dladdr` (not on POSIX's async-signal-safe list; it takes dyld's recursive reader lock and
|
||||
//! doesn't allocate) to name the modules, rewriting the file, so a hang or a fault in there
|
||||
//! still leaves a record.
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
use std::ffi::{CStr, CString, c_int, c_void};
|
||||
use std::fmt::Write as _;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::sync::OnceLock;
|
||||
use std::sync::atomic::{AtomicBool, AtomicI32, AtomicUsize, Ordering};
|
||||
|
||||
use libc::{SIGABRT, SIGBUS, SIGFPE, SIGILL, SIGSEGV, SIGTRAP, siginfo_t};
|
||||
|
||||
/// The signals a crash arrives as, in the order of [`PREVIOUS`].
|
||||
const SIGNALS: [c_int; 6] = [SIGSEGV, SIGBUS, SIGILL, SIGFPE, SIGTRAP, SIGABRT];
|
||||
/// Frames the walk records (the faulting instruction first).
|
||||
const MAX_FRAMES: usize = 24;
|
||||
/// A fault this far below a thread's stack is taken for a stack overflow.
|
||||
const GUARD: usize = 64 << 10;
|
||||
|
||||
/// Where the handler writes (NUL-terminated), set once by [`install`].
|
||||
static PATH: OnceLock<CString> = OnceLock::new();
|
||||
/// The handler that was installed before ours for each of [`SIGNALS`] (called after ours), as an
|
||||
/// address (`SIG_DFL` is 0, `SIG_IGN` 1), and its `sa_flags`.
|
||||
static PREVIOUS: [AtomicUsize; 6] = [const { AtomicUsize::new(libc::SIG_DFL) }; 6];
|
||||
static PREVIOUS_FLAGS: [AtomicI32; 6] = [const { AtomicI32::new(0) }; 6];
|
||||
/// The handler has started writing (a second signal while it does is only passed on).
|
||||
static BUSY: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// Catch native crashes from now on (after `solidcraft_engine::crash::install`; nothing without it).
|
||||
pub fn install() {
|
||||
let Some((info, _)) = solidcraft_engine::crash::native_paths() else { return };
|
||||
let Ok(path) = CString::new(info.as_os_str().as_bytes()) else { return };
|
||||
if PATH.set(path).is_err() {
|
||||
return;
|
||||
}
|
||||
for (slot, &signal) in SIGNALS.iter().enumerate() {
|
||||
// SAFETY: `sigaction` is plain data; all zeros is a valid (empty) value.
|
||||
let mut ours: libc::sigaction = unsafe { std::mem::zeroed() };
|
||||
ours.sa_sigaction = handler as *const () as usize;
|
||||
// On the alternate stack the standard library gives each of its threads, so that a stack
|
||||
// overflow can still be recorded.
|
||||
ours.sa_flags = libc::SA_SIGINFO | libc::SA_ONSTACK;
|
||||
// SAFETY: as above.
|
||||
let mut old: libc::sigaction = unsafe { std::mem::zeroed() };
|
||||
// SAFETY: `handler` has the signature SA_SIGINFO calls it with and stays valid for the
|
||||
// process's life; both structs are valid for the call.
|
||||
if unsafe { libc::sigaction(signal, &ours, &mut old) } == 0
|
||||
&& old.sa_sigaction != ours.sa_sigaction
|
||||
&& let (Some(p), Some(f)) = (PREVIOUS.get(slot), PREVIOUS_FLAGS.get(slot))
|
||||
{
|
||||
f.store(old.sa_flags, Ordering::SeqCst);
|
||||
p.store(old.sa_sigaction, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The signal handler: record, then pass the signal on.
|
||||
unsafe extern "C" fn handler(signal: c_int, info: *mut siginfo_t, context: *mut c_void) {
|
||||
// SAFETY: `__error` is this thread's errno, restored for whatever runs after us.
|
||||
let errno = unsafe { *libc::__error() };
|
||||
let slot = SIGNALS.iter().position(|&s| s == signal).unwrap_or(0);
|
||||
let previous = PREVIOUS.get(slot).map_or(libc::SIG_DFL, |p| p.load(Ordering::SeqCst));
|
||||
// SAFETY: the system passes a valid siginfo and the interrupted thread's context for the
|
||||
// duration of an SA_SIGINFO handler.
|
||||
let f = unsafe { fault(signal, info, context) };
|
||||
// Someone sent a signal the app ignored before: still nothing to record.
|
||||
let ignored = previous == libc::SIG_IGN && !f.fault;
|
||||
if !ignored && !BUSY.swap(true, Ordering::SeqCst) {
|
||||
record(&f);
|
||||
}
|
||||
// SAFETY: as above.
|
||||
unsafe { pass_on(signal, slot, previous, f.fault, info, context) };
|
||||
// SAFETY: as above.
|
||||
unsafe { *libc::__error() = errno };
|
||||
}
|
||||
|
||||
/// The signal comes from the faulting instruction itself (not `kill`, `raise` or `abort`), so
|
||||
/// returning runs it again: on arm64, the exception the thread last took matches the signal.
|
||||
/// macOS gives a SIGSEGV sent with `kill` the same `si_code` as a real one (SEGV_ACCERR), but
|
||||
/// the exception is then the thread's last system call (or something long past, whose fault
|
||||
/// address doesn't match).
|
||||
#[cfg(any(target_arch = "aarch64", test))]
|
||||
fn arm64_fault(signal: c_int, si_code: c_int, addr: usize, esr: u32, far: u64) -> bool {
|
||||
const ABORTS: [u32; 4] = [0x20, 0x21, 0x24, 0x25];
|
||||
const SVC: u32 = 0x15;
|
||||
let class = esr >> 26;
|
||||
signal != SIGABRT
|
||||
&& (1..0x10000).contains(&si_code)
|
||||
&& match signal {
|
||||
SIGSEGV | SIGBUS => ABORTS.contains(&class) && far as usize == addr,
|
||||
_ => class != SVC && !ABORTS.contains(&class),
|
||||
}
|
||||
}
|
||||
|
||||
/// As [`arm64_fault`], from the x86 trap number and page-fault address.
|
||||
#[cfg(any(target_arch = "x86_64", test))]
|
||||
fn x86_fault(signal: c_int, si_code: c_int, addr: usize, trapno: u16, faultvaddr: u64) -> bool {
|
||||
signal != SIGABRT
|
||||
&& (1..0x10000).contains(&si_code)
|
||||
&& match signal {
|
||||
SIGSEGV | SIGBUS => (trapno == 14 && faultvaddr as usize == addr) || matches!(trapno, 12 | 13),
|
||||
SIGILL => trapno == 6,
|
||||
SIGFPE => matches!(trapno, 0 | 16 | 19),
|
||||
SIGTRAP => matches!(trapno, 1 | 3),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Hand the signal to the previous handler, or to the default action. A crash signal that was
|
||||
/// only sent goes to the default action: the handler before ours (the standard library's, for
|
||||
/// SIGSEGV and SIGBUS) expects a fault that happens again when it returns.
|
||||
unsafe fn pass_on(signal: c_int, slot: usize, previous: usize, fault: bool, info: *mut siginfo_t, context: *mut c_void) {
|
||||
let ours = handler as *const () as usize;
|
||||
if previous != libc::SIG_DFL && previous != libc::SIG_IGN && previous != ours && (fault || signal == SIGABRT) {
|
||||
let flags = PREVIOUS_FLAGS.get(slot).map_or(0, |f| f.load(Ordering::SeqCst));
|
||||
if flags & libc::SA_SIGINFO != 0 {
|
||||
// SAFETY: the address came from `sigaction` as an SA_SIGINFO handler.
|
||||
let previous: unsafe extern "C" fn(c_int, *mut siginfo_t, *mut c_void) = unsafe { std::mem::transmute(previous) };
|
||||
// SAFETY: as for this handler.
|
||||
unsafe { previous(signal, info, context) };
|
||||
} else {
|
||||
// SAFETY: the address came from `sigaction` as a plain handler.
|
||||
let previous: unsafe extern "C" fn(c_int) = unsafe { std::mem::transmute(previous) };
|
||||
// SAFETY: as for this handler.
|
||||
unsafe { previous(signal) };
|
||||
}
|
||||
return;
|
||||
}
|
||||
if previous == libc::SIG_IGN && !fault {
|
||||
return;
|
||||
}
|
||||
// SAFETY: as in `install`.
|
||||
let mut default: libc::sigaction = unsafe { std::mem::zeroed() };
|
||||
default.sa_sigaction = libc::SIG_DFL;
|
||||
// SAFETY: a valid struct; restoring the default action is async-signal-safe.
|
||||
unsafe { libc::sigaction(signal, &default, std::ptr::null_mut()) };
|
||||
// A fault happens again when we return, now with the default action, so macOS reports it at
|
||||
// the instruction; anything else is raised again, and arrives once this handler returns.
|
||||
if !fault {
|
||||
// SAFETY: async-signal-safe.
|
||||
unsafe { libc::raise(signal) };
|
||||
}
|
||||
}
|
||||
|
||||
/// The darwin `ucontext_t`, up to the pointer to the machine context (`<sys/_types/_ucontext.h>`).
|
||||
#[repr(C)]
|
||||
struct UContext {
|
||||
_onstack: c_int,
|
||||
_sigmask: u32,
|
||||
_stack: libc::stack_t,
|
||||
_link: *mut c_void,
|
||||
_mcsize: usize,
|
||||
mcontext: *const MContext,
|
||||
}
|
||||
|
||||
/// The darwin arm64 machine context: exception state, then the general registers
|
||||
/// (`<mach/arm/_structs.h>`; the floating-point state that follows isn't read).
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
#[repr(C)]
|
||||
struct MContext {
|
||||
far: u64,
|
||||
esr: u32,
|
||||
_exception: u32,
|
||||
_x: [u64; 29],
|
||||
fp: u64,
|
||||
_lr: u64,
|
||||
_sp: u64,
|
||||
pc: u64,
|
||||
}
|
||||
|
||||
/// The darwin x86_64 machine context: exception state, then the general registers
|
||||
/// (`<mach/i386/_structs.h>`; rax rbx rcx rdx rdi rsi rbp rsp r8–r15, then rip).
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
#[repr(C)]
|
||||
struct MContext {
|
||||
trapno: u16,
|
||||
_cpu: u16,
|
||||
err: u32,
|
||||
faultvaddr: u64,
|
||||
gpr: [u64; 16],
|
||||
rip: u64,
|
||||
}
|
||||
|
||||
/// What the faulting instruction did to its address, when the processor says.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
enum Access {
|
||||
Unknown,
|
||||
Read,
|
||||
Write,
|
||||
Execute,
|
||||
}
|
||||
|
||||
/// What a crash signal says, read out of the signal before anything is looked up.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct Fault {
|
||||
signal: c_int,
|
||||
si_code: c_int,
|
||||
/// `si_addr`: the address that faulted (for SIGILL, SIGFPE and SIGTRAP, the instruction).
|
||||
addr: usize,
|
||||
pc: usize,
|
||||
fp: usize,
|
||||
access: Access,
|
||||
/// It comes from the faulting instruction (not `kill`, `raise` or `abort`).
|
||||
fault: bool,
|
||||
/// The process that sent the signal (0 or our own for a fault or an `abort()`).
|
||||
sender: i32,
|
||||
pid: i32,
|
||||
thread: u64,
|
||||
thread_name: [u8; 64],
|
||||
/// The thread's stack, `[low, high)` (0, 0 when unknown).
|
||||
stack: (usize, usize),
|
||||
}
|
||||
|
||||
impl Fault {
|
||||
/// The fault hit just below the thread's stack: the stack ran out.
|
||||
fn overflow(&self) -> bool {
|
||||
let (low, high) = self.stack;
|
||||
matches!(self.signal, SIGSEGV | SIGBUS) && high > low && self.addr < low.saturating_add(4096) && self.addr >= low.saturating_sub(GUARD)
|
||||
}
|
||||
|
||||
fn signal_name(&self) -> &'static str {
|
||||
match self.signal {
|
||||
SIGSEGV => "SIGSEGV",
|
||||
SIGBUS => "SIGBUS",
|
||||
SIGILL => "SIGILL",
|
||||
SIGFPE => "SIGFPE",
|
||||
SIGTRAP => "SIGTRAP",
|
||||
SIGABRT => "SIGABRT",
|
||||
_ => "signal",
|
||||
}
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
if self.overflow() {
|
||||
return "stack overflow";
|
||||
}
|
||||
match self.signal {
|
||||
SIGSEGV => "segmentation fault",
|
||||
SIGBUS => "bus error",
|
||||
SIGILL => "illegal instruction",
|
||||
SIGFPE => "arithmetic exception",
|
||||
SIGTRAP => "trap",
|
||||
SIGABRT => "abort",
|
||||
_ => "signal",
|
||||
}
|
||||
}
|
||||
|
||||
/// The `si_code` names that say something about a crash.
|
||||
fn code_name(&self) -> Option<&'static str> {
|
||||
Some(match (self.signal, self.si_code) {
|
||||
(SIGSEGV, 1) => "SEGV_MAPERR",
|
||||
(SIGSEGV, 2) => "SEGV_ACCERR",
|
||||
(SIGBUS, 1) => "BUS_ADRALN",
|
||||
(SIGBUS, 2) => "BUS_ADRERR",
|
||||
(SIGBUS, 3) => "BUS_OBJERR",
|
||||
(SIGFPE, 7) => "FPE_INTDIV",
|
||||
(SIGFPE, 8) => "FPE_INTOVF",
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// One line on what happened: the address read or written, a stack overflow, who sent it.
|
||||
fn detail(&self, b: &mut Buf) {
|
||||
if self.sender != 0 && self.sender != self.pid {
|
||||
let _ = write!(b, "sent by process {}", self.sender);
|
||||
return;
|
||||
}
|
||||
if !self.fault {
|
||||
if self.signal != SIGABRT {
|
||||
let _ = b.write_str("sent with kill or raise, not a fault");
|
||||
}
|
||||
return;
|
||||
}
|
||||
let what = match self.access {
|
||||
Access::Read => "read of",
|
||||
Access::Write => "write to",
|
||||
Access::Execute => "execution at",
|
||||
Access::Unknown => "access to",
|
||||
};
|
||||
if self.overflow() {
|
||||
let _ = write!(b, "{what} 0x{:016X}, below the thread's stack", self.addr);
|
||||
} else if matches!(self.signal, SIGSEGV | SIGBUS) {
|
||||
let _ = write!(b, "{what} 0x{:016X}", self.addr);
|
||||
} else {
|
||||
let _ = write!(b, "at 0x{:016X}", self.addr);
|
||||
}
|
||||
if let Some(c) = self.code_name() {
|
||||
let _ = write!(b, " ({c})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An image that holds an address, as `dladdr` names it.
|
||||
struct Module<'a> {
|
||||
path: &'a [u8],
|
||||
base: usize,
|
||||
symbol: Option<(&'a [u8], usize)>,
|
||||
}
|
||||
|
||||
/// A fixed buffer to format into without allocating (what doesn't fit is cut off).
|
||||
struct Buf {
|
||||
bytes: [u8; 8192],
|
||||
len: usize,
|
||||
}
|
||||
|
||||
impl Buf {
|
||||
fn new() -> Buf {
|
||||
Buf { bytes: [0; 8192], len: 0 }
|
||||
}
|
||||
|
||||
fn room(&self) -> usize {
|
||||
self.bytes.len().saturating_sub(self.len)
|
||||
}
|
||||
|
||||
fn as_bytes(&self) -> &[u8] {
|
||||
self.bytes.get(..self.len).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// `s` as the inside of a JSON string, at most `max` bytes of it (bytes that aren't UTF-8
|
||||
/// and control characters become `?`).
|
||||
fn escaped(&mut self, s: &[u8], max: usize) {
|
||||
for chunk in s.get(..s.len().min(max)).unwrap_or_default().utf8_chunks() {
|
||||
for c in chunk.valid().chars() {
|
||||
let _ = match c {
|
||||
'"' => self.write_str("\\\""),
|
||||
'\\' => self.write_str("\\\\"),
|
||||
c if (c as u32) < 0x20 => self.write_str("?"),
|
||||
c => self.write_char(c),
|
||||
};
|
||||
}
|
||||
if !chunk.invalid().is_empty() {
|
||||
let _ = self.write_str("?");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Write for Buf {
|
||||
fn write_str(&mut self, s: &str) -> std::fmt::Result {
|
||||
let n = s.len().min(self.room());
|
||||
if let (Some(to), Some(from)) = (self.bytes.get_mut(self.len..self.len + n), s.as_bytes().get(..n)) {
|
||||
to.copy_from_slice(from);
|
||||
self.len += n;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// The last part of a path.
|
||||
fn file_name(path: &[u8]) -> &[u8] {
|
||||
path.rsplit(|&c| c == b'/').next().unwrap_or(path)
|
||||
}
|
||||
|
||||
/// The report: what the signal says, then (with `resolve`) the module of the faulting
|
||||
/// instruction and the `frames`. Always valid JSON: frames stop while there is room to close it.
|
||||
fn json(b: &mut Buf, f: &Fault, frames: &[usize], resolve: impl Fn(usize, bool) -> Option<Module<'static>>) {
|
||||
let signal = f.signal_name();
|
||||
let _ = write!(b, "{{\"signal\":\"{signal}\",\"number\":{},\"code\":\"{signal}\",\"name\":\"{}\"", f.signal, f.name());
|
||||
let _ = write!(b, ",\"si_code\":{},\"address\":\"0x{:016X}\",\"thread\":{},\"pid\":{}", f.si_code, f.pc, f.thread, f.pid);
|
||||
let name = f.thread_name.split(|&c| c == 0).next().unwrap_or_default();
|
||||
if !name.is_empty() {
|
||||
let _ = b.write_str(",\"thread_name\":\"");
|
||||
b.escaped(name, 64);
|
||||
let _ = b.write_str("\"");
|
||||
}
|
||||
let mark = b.len;
|
||||
let _ = b.write_str(",\"detail\":\"");
|
||||
let start = b.len;
|
||||
f.detail(b);
|
||||
if b.len == start {
|
||||
b.len = mark;
|
||||
} else {
|
||||
let _ = b.write_str("\"");
|
||||
}
|
||||
if let Some(m) = resolve(f.pc, false) {
|
||||
let _ = write!(b, ",\"offset\":\"0x{:X}\",\"module\":\"", f.pc.wrapping_sub(m.base));
|
||||
b.escaped(m.path, 300);
|
||||
let _ = b.write_str("\"");
|
||||
}
|
||||
if !frames.is_empty() {
|
||||
let _ = b.write_str(",\"frames\":[");
|
||||
for (i, &at) in frames.iter().enumerate() {
|
||||
// A frame is at most ~300 bytes; keep room for it and the closing `]}`.
|
||||
if b.room() < 400 {
|
||||
break;
|
||||
}
|
||||
if i > 0 {
|
||||
let _ = b.write_str(",");
|
||||
}
|
||||
let _ = b.write_str("\"");
|
||||
// A return address points after the call: look up the call itself.
|
||||
match resolve(at, i > 0) {
|
||||
Some(m) => {
|
||||
b.escaped(file_name(m.path), 100);
|
||||
let _ = write!(b, "+0x{:X}", at.wrapping_sub(m.base));
|
||||
if let Some((symbol, start)) = m.symbol {
|
||||
let _ = b.write_str(" ");
|
||||
b.escaped(symbol, 160);
|
||||
let _ = write!(b, "+0x{:X}", at.wrapping_sub(start));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let _ = write!(b, "0x{at:016X}");
|
||||
}
|
||||
}
|
||||
let _ = b.write_str("\"");
|
||||
}
|
||||
let _ = b.write_str("]");
|
||||
}
|
||||
let _ = b.write_str("}");
|
||||
}
|
||||
|
||||
/// The return addresses up the frame-pointer chain from `fp`, after `pc`. `read` gives the word
|
||||
/// at an address; frames outside `stack`, misaligned or not going up end the walk.
|
||||
fn walk(pc: usize, fp: usize, stack: (usize, usize), read: impl Fn(usize) -> Option<usize>, out: &mut [usize; MAX_FRAMES]) -> usize {
|
||||
let (low, high) = stack;
|
||||
let mut n = 0;
|
||||
if let Some(slot) = out.get_mut(n) {
|
||||
*slot = pc;
|
||||
n += 1;
|
||||
}
|
||||
let mut fp = fp;
|
||||
while n < MAX_FRAMES {
|
||||
let in_stack = fp >= low && fp.checked_add(16).is_some_and(|end| end <= high);
|
||||
if !fp.is_multiple_of(8) || !in_stack {
|
||||
break;
|
||||
}
|
||||
let (Some(next), Some(ret)) = (read(fp), read(fp + 8)) else { break };
|
||||
let ret = strip_pointer_auth(ret);
|
||||
if ret == 0 {
|
||||
break;
|
||||
}
|
||||
if let Some(slot) = out.get_mut(n) {
|
||||
*slot = ret;
|
||||
n += 1;
|
||||
}
|
||||
if next <= fp {
|
||||
break;
|
||||
}
|
||||
fp = next;
|
||||
}
|
||||
n
|
||||
}
|
||||
|
||||
/// Return addresses saved by arm64e code carry a signature in their top bits.
|
||||
fn strip_pointer_auth(addr: usize) -> usize {
|
||||
if cfg!(target_arch = "aarch64") { addr & 0x0000_7FFF_FFFF_FFFF } else { addr }
|
||||
}
|
||||
|
||||
/// Read what the signal and the interrupted thread's registers say.
|
||||
unsafe fn fault(signal: c_int, info: *const siginfo_t, context: *const c_void) -> Fault {
|
||||
let mut f = Fault {
|
||||
signal,
|
||||
si_code: 0,
|
||||
addr: 0,
|
||||
pc: 0,
|
||||
fp: 0,
|
||||
access: Access::Unknown,
|
||||
fault: false,
|
||||
sender: 0,
|
||||
// SAFETY: async-signal-safe.
|
||||
pid: unsafe { libc::getpid() },
|
||||
thread: 0,
|
||||
thread_name: [0; 64],
|
||||
stack: (0, 0),
|
||||
};
|
||||
if !info.is_null() {
|
||||
// SAFETY: the system passes a valid siginfo for the duration of the handler.
|
||||
let info = unsafe { &*info };
|
||||
f.si_code = info.si_code;
|
||||
f.addr = info.si_addr as usize;
|
||||
f.sender = info.si_pid;
|
||||
// Without registers (never seen), the signal's own code has to do.
|
||||
f.fault = signal != SIGABRT && (1..0x10000).contains(&f.si_code);
|
||||
}
|
||||
let context = context as *const UContext;
|
||||
// SAFETY: an SA_SIGINFO handler's third argument is the interrupted thread's ucontext, whose
|
||||
// machine context pointer the kernel fills in.
|
||||
let mcontext = if context.is_null() { std::ptr::null() } else { unsafe { (*context).mcontext } };
|
||||
if !mcontext.is_null() {
|
||||
// SAFETY: as above.
|
||||
let m = unsafe { &*mcontext };
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
{
|
||||
f.pc = strip_pointer_auth(m.pc as usize);
|
||||
f.fp = m.fp as usize;
|
||||
f.fault = arm64_fault(signal, f.si_code, f.addr, m.esr, m.far);
|
||||
// A data abort says whether it was a write (WnR); an instruction abort is execution.
|
||||
f.access = match m.esr >> 26 {
|
||||
0x24 | 0x25 if m.esr & (1 << 6) != 0 => Access::Write,
|
||||
0x24 | 0x25 => Access::Read,
|
||||
0x20 | 0x21 => Access::Execute,
|
||||
_ => Access::Unknown,
|
||||
};
|
||||
}
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
{
|
||||
f.pc = m.rip as usize;
|
||||
f.fp = m.gpr.get(6).copied().unwrap_or(0) as usize;
|
||||
f.fault = x86_fault(signal, f.si_code, f.addr, m.trapno, m.faultvaddr);
|
||||
// A page fault's error code: bit 4 instruction fetch, bit 1 write.
|
||||
f.access = match (m.trapno, m.err) {
|
||||
(14, e) if e & 0x10 != 0 => Access::Execute,
|
||||
(14, e) if e & 0x2 != 0 => Access::Write,
|
||||
(14, _) => Access::Read,
|
||||
_ => Access::Unknown,
|
||||
};
|
||||
}
|
||||
}
|
||||
if f.pc == 0 && matches!(signal, SIGILL | SIGFPE | SIGTRAP) {
|
||||
f.pc = f.addr;
|
||||
}
|
||||
// SAFETY: these read the calling thread's own pthread structure; the buffers are as long as
|
||||
// we say.
|
||||
unsafe {
|
||||
let me = libc::pthread_self();
|
||||
libc::pthread_threadid_np(me, &mut f.thread);
|
||||
libc::pthread_getname_np(me, f.thread_name.as_mut_ptr().cast(), f.thread_name.len());
|
||||
let high = libc::pthread_get_stackaddr_np(me) as usize;
|
||||
let size = libc::pthread_get_stacksize_np(me);
|
||||
if let Some(low) = high.checked_sub(size) {
|
||||
f.stack = (low, high);
|
||||
}
|
||||
}
|
||||
f
|
||||
}
|
||||
|
||||
/// The image that holds `addr` (the call before it, for a return address).
|
||||
fn resolve(addr: usize, return_address: bool) -> Option<Module<'static>> {
|
||||
let at = if return_address { addr.saturating_sub(1) } else { addr };
|
||||
// SAFETY: `Dl_info` is plain data; all zeros is a valid value.
|
||||
let mut info: libc::Dl_info = unsafe { std::mem::zeroed() };
|
||||
// SAFETY: `dladdr` only reads dyld's image list; the strings it returns live as long as the
|
||||
// image, which a crashing process never unloads.
|
||||
unsafe {
|
||||
if libc::dladdr(at as *const c_void, &mut info) == 0 || info.dli_fname.is_null() {
|
||||
return None;
|
||||
}
|
||||
let symbol = (!info.dli_sname.is_null()).then(|| (CStr::from_ptr(info.dli_sname).to_bytes(), info.dli_saddr as usize));
|
||||
Some(Module { path: CStr::from_ptr(info.dli_fname).to_bytes(), base: info.dli_fbase as usize, symbol })
|
||||
}
|
||||
}
|
||||
|
||||
/// Write what the signal says into the info file, then again with the modules and frames.
|
||||
fn record(f: &Fault) {
|
||||
let Some(path) = PATH.get() else { return };
|
||||
// SAFETY: NUL-terminated path; the descriptor is closed below.
|
||||
let fd = unsafe { libc::open(path.as_ptr(), libc::O_WRONLY | libc::O_CREAT | libc::O_TRUNC | libc::O_CLOEXEC, 0o600) };
|
||||
if fd < 0 {
|
||||
return;
|
||||
}
|
||||
let mut b = Buf::new();
|
||||
json(&mut b, f, &[], |_, _| None);
|
||||
let bytes = b.as_bytes();
|
||||
// SAFETY: the buffer is as long as we say.
|
||||
unsafe { libc::write(fd, bytes.as_ptr().cast(), bytes.len()) };
|
||||
let mut frames = [0usize; MAX_FRAMES];
|
||||
// SAFETY: `walk` only reads inside the thread's own stack, 8-byte aligned.
|
||||
let n = walk(f.pc, f.fp, f.stack, |at| Some(unsafe { (at as *const usize).read() }), &mut frames);
|
||||
b.len = 0;
|
||||
json(&mut b, f, frames.get(..n).unwrap_or_default(), resolve);
|
||||
let bytes = b.as_bytes();
|
||||
// SAFETY: as above.
|
||||
unsafe {
|
||||
if libc::pwrite(fd, bytes.as_ptr().cast(), bytes.len(), 0) == bytes.len() as isize {
|
||||
libc::ftruncate(fd, bytes.len() as libc::off_t);
|
||||
}
|
||||
libc::close(fd);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::os::unix::process::ExitStatusExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn fault(signal: c_int, si_code: c_int, addr: usize) -> Fault {
|
||||
let mut thread_name = [0u8; 64];
|
||||
thread_name[..4].copy_from_slice(b"main");
|
||||
Fault {
|
||||
signal,
|
||||
si_code,
|
||||
addr,
|
||||
pc: 0x1_0000_1234,
|
||||
fp: 0,
|
||||
access: Access::Read,
|
||||
fault: true,
|
||||
sender: 0,
|
||||
pid: 77,
|
||||
thread: 4242,
|
||||
thread_name,
|
||||
stack: (0x7000_0000, 0x7080_0000),
|
||||
}
|
||||
}
|
||||
|
||||
fn report(f: &Fault, frames: &[usize]) -> serde_json::Value {
|
||||
let mut b = Buf::new();
|
||||
json(&mut b, f, frames, |at, _| {
|
||||
(at >= 0x1_0000_0000).then_some(Module {
|
||||
path: b"/Applications/SolidCraft.app/Contents/MacOS/solidcraft",
|
||||
base: 0x1_0000_0000,
|
||||
symbol: Some((b"_ZN4main17h0123456789abcdefE", 0x1_0000_1200)),
|
||||
})
|
||||
});
|
||||
serde_json::from_slice(b.as_bytes()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_segfault_says_the_signal_the_address_read_and_the_module() {
|
||||
let v = report(&fault(SIGSEGV, 1, 0x10), &[0x1_0000_1234, 0x1_0000_2000, 0x5000]);
|
||||
assert_eq!(v["signal"], "SIGSEGV");
|
||||
assert_eq!(v["code"], "SIGSEGV");
|
||||
assert_eq!(v["name"], "segmentation fault");
|
||||
assert_eq!(v["detail"], "read of 0x0000000000000010 (SEGV_MAPERR)");
|
||||
assert_eq!(v["address"], "0x0000000100001234");
|
||||
assert_eq!(v["module"], "/Applications/SolidCraft.app/Contents/MacOS/solidcraft");
|
||||
assert_eq!(v["offset"], "0x1234");
|
||||
assert_eq!(v["thread"], 4242);
|
||||
assert_eq!(v["thread_name"], "main");
|
||||
assert_eq!(v["pid"], 77);
|
||||
let frames: Vec<&str> = v["frames"].as_array().unwrap().iter().map(|f| f.as_str().unwrap()).collect();
|
||||
assert_eq!(
|
||||
frames,
|
||||
["solidcraft+0x1234 _ZN4main17h0123456789abcdefE+0x34", "solidcraft+0x2000 _ZN4main17h0123456789abcdefE+0xE00", "0x0000000000005000"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_fault_below_the_stack_is_a_stack_overflow() {
|
||||
let mut f = fault(SIGBUS, 2, 0x7000_0000 - 0x100);
|
||||
f.access = Access::Write;
|
||||
let v = report(&f, &[]);
|
||||
assert_eq!(v["name"], "stack overflow");
|
||||
assert_eq!(v["code"], "SIGBUS");
|
||||
assert_eq!(v["detail"], "write to 0x000000006FFFFF00, below the thread's stack (BUS_ADRERR)");
|
||||
// Far below it, or inside it, it is not.
|
||||
assert_eq!(report(&fault(SIGBUS, 2, 0x10), &[])["name"], "bus error");
|
||||
assert_eq!(report(&fault(SIGSEGV, 2, 0x7040_0000), &[])["name"], "segmentation fault");
|
||||
// Nor without knowing the stack.
|
||||
let mut f = fault(SIGSEGV, 1, 0x7000_0000 - 0x100);
|
||||
f.stack = (0, 0);
|
||||
assert_eq!(report(&f, &[])["name"], "segmentation fault");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_abort_or_a_signal_sent_by_another_process_has_no_address_detail() {
|
||||
let mut f = fault(SIGABRT, 0, 0);
|
||||
f.fault = false;
|
||||
f.sender = 77;
|
||||
let v = report(&f, &[]);
|
||||
assert_eq!(v["signal"], "SIGABRT");
|
||||
assert_eq!(v["name"], "abort");
|
||||
assert!(v.get("detail").is_none(), "{v}");
|
||||
let mut f = fault(SIGSEGV, 0x10001, 0);
|
||||
f.fault = false;
|
||||
f.sender = 501;
|
||||
assert_eq!(report(&f, &[])["detail"], "sent by process 501");
|
||||
// `kill` without a sender, or `raise`.
|
||||
f.sender = 0;
|
||||
assert_eq!(report(&f, &[])["detail"], "sent with kill or raise, not a fault");
|
||||
// A trap names the instruction.
|
||||
assert_eq!(report(&fault(SIGTRAP, 1, 0x1_0000_1234), &[])["detail"], "at 0x0000000100001234");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_a_fault_runs_again_when_the_handler_returns() {
|
||||
// Measured on an M5 Pro, macOS 27: a read of address 16, then `kill -SEGV` while the
|
||||
// thread waits in a system call. Both have si_code 2.
|
||||
assert!(arm64_fault(SIGSEGV, 2, 0x10, 0x9200_0046, 0x10));
|
||||
assert!(!arm64_fault(SIGSEGV, 2, 0, 0x5600_0080, 0));
|
||||
assert!(!arm64_fault(SIGBUS, 1, 0, 0x5600_0080, 0));
|
||||
// A data abort long past, while the signal names no address.
|
||||
assert!(!arm64_fault(SIGSEGV, 2, 0, 0x9200_0046, 0x10));
|
||||
// A trap instruction (BRK) or an undefined one; but not a system call.
|
||||
assert!(arm64_fault(SIGTRAP, 1, 0x1000, 0xF200_0001, 0));
|
||||
assert!(arm64_fault(SIGILL, 1, 0x1000, 0x0200_0000, 0));
|
||||
assert!(!arm64_fault(SIGILL, 1, 0x1000, 0x5600_0080, 0));
|
||||
// Sent: SI_USER, no code, or an abort.
|
||||
assert!(!arm64_fault(SIGSEGV, 0x10001, 0x10, 0x9200_0046, 0x10));
|
||||
assert!(!arm64_fault(SIGTRAP, 0, 0, 0xF200_0001, 0));
|
||||
assert!(!arm64_fault(SIGABRT, 1, 0, 0x5600_0080, 0));
|
||||
// x86_64: a page fault at its address, a general protection fault, ud2, int3.
|
||||
assert!(x86_fault(SIGSEGV, 1, 0x10, 14, 0x10));
|
||||
assert!(!x86_fault(SIGSEGV, 1, 0, 14, 0x10));
|
||||
assert!(x86_fault(SIGSEGV, 1, 0, 13, 0));
|
||||
assert!(x86_fault(SIGILL, 1, 0x1000, 6, 0));
|
||||
assert!(x86_fault(SIGTRAP, 1, 0x1000, 3, 0));
|
||||
assert!(!x86_fault(SIGILL, 1, 0x1000, 14, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn names_with_quotes_control_characters_or_bad_utf8_stay_valid_json() {
|
||||
let mut f = fault(SIGILL, 1, 0x1_0000_1234);
|
||||
f.thread_name = [0; 64];
|
||||
f.thread_name[..7].copy_from_slice(b"a\"b\\\n\xFFc");
|
||||
let v = report(&f, &[]);
|
||||
assert_eq!(v["thread_name"], "a\"b\\??c");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_report_with_long_names_still_closes_its_json() {
|
||||
let long: &'static [u8] = Box::leak(vec![b'x'; 4000].into_boxed_slice());
|
||||
let f = fault(SIGSEGV, 1, 0x10);
|
||||
let mut b = Buf::new();
|
||||
json(&mut b, &f, &[0x1_0000_1234; MAX_FRAMES], |_, _| Some(Module { path: long, base: 0x1_0000_0000, symbol: Some((long, 0x1_0000_0000)) }));
|
||||
let v: serde_json::Value = serde_json::from_slice(b.as_bytes()).unwrap();
|
||||
assert_eq!(v["module"].as_str().unwrap().len(), 300);
|
||||
let frames = v["frames"].as_array().unwrap();
|
||||
assert!(!frames.is_empty() && frames.len() <= MAX_FRAMES, "{}", frames.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_frame_walk_stays_on_the_stack_and_goes_up() {
|
||||
let stack = (0x1000, 0x2000);
|
||||
// Three frames: 0x1100 -> 0x1200 -> 0x1300 -> 0 (end).
|
||||
let memory = |at: usize| match at {
|
||||
0x1100 => Some(0x1200),
|
||||
0x1108 => Some(0xA),
|
||||
0x1200 => Some(0x1300),
|
||||
0x1208 => Some(0xB),
|
||||
0x1300 => Some(0),
|
||||
0x1308 => Some(0xC),
|
||||
_ => None,
|
||||
};
|
||||
let mut out = [0; MAX_FRAMES];
|
||||
let n = walk(0x9, 0x1100, stack, memory, &mut out);
|
||||
assert_eq!(&out[..n], &[0x9, 0xA, 0xB, 0xC]);
|
||||
// A frame pointer off the stack, misaligned, or one that doesn't go up ends the walk.
|
||||
assert_eq!(walk(0x9, 0x3000, stack, memory, &mut out), 1);
|
||||
assert_eq!(walk(0x9, 0x1101, stack, memory, &mut out), 1);
|
||||
let looping = |at: usize| if at.is_multiple_of(16) { Some(at) } else { Some(0xA) };
|
||||
assert_eq!(walk(0x9, 0x1100, stack, looping, &mut out), 2);
|
||||
// A chain that never ends stops at MAX_FRAMES.
|
||||
let endless = |at: usize| if at.is_multiple_of(16) { Some(at + 16) } else { Some(0xA) };
|
||||
assert_eq!(walk(0x9, 0x1000, stack, endless, &mut out), MAX_FRAMES);
|
||||
}
|
||||
|
||||
/// The child half: install crash reporting and the handler, then crash the way
|
||||
/// `SOLIDCRAFT_NATIVE_CRASH_FAULT` says.
|
||||
#[test]
|
||||
fn child() {
|
||||
let Some(dir) = std::env::var_os("SOLIDCRAFT_NATIVE_CRASH_CHILD") else { return };
|
||||
let build = solidcraft_engine::crash::Build { version: "9.9.9", commit: "abc1234", built: "2026-10-25" };
|
||||
let fresh = solidcraft_engine::crash::install(PathBuf::from(dir), build);
|
||||
println!("FRESH {}", serde_json::to_string(&fresh).unwrap_or_default());
|
||||
let Some(fault) = std::env::var_os("SOLIDCRAFT_NATIVE_CRASH_FAULT") else {
|
||||
solidcraft_engine::crash::end_session();
|
||||
return;
|
||||
};
|
||||
super::install();
|
||||
solidcraft_engine::crash::phase("device creation");
|
||||
match fault.to_str() {
|
||||
Some("abort") => std::process::abort(),
|
||||
Some("overflow") => println!("unreachable {}", recurse(0)),
|
||||
Some("raise") => {
|
||||
// SAFETY: none needed; this is the crash under test.
|
||||
unsafe { libc::raise(SIGSEGV) };
|
||||
println!("survived");
|
||||
}
|
||||
_ => segfault(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs out of stack.
|
||||
#[inline(never)]
|
||||
fn recurse(n: u64) -> u64 {
|
||||
let a = std::hint::black_box([n; 64]);
|
||||
if n == u64::MAX { 0 } else { recurse(n + 1).wrapping_add(a[0]) }
|
||||
}
|
||||
|
||||
/// The crash under test: a read of address 16, as a null struct pointer's field would be.
|
||||
#[inline(never)]
|
||||
fn segfault() {
|
||||
// SAFETY: none; this is the crash under test.
|
||||
let v = unsafe { std::ptr::read_volatile(std::hint::black_box(16usize) as *const u64) };
|
||||
println!("unreachable {v}");
|
||||
}
|
||||
|
||||
fn run(dir: &Path, fault: Option<&str>) -> std::process::Output {
|
||||
let mut c = std::process::Command::new(std::env::current_exe().unwrap());
|
||||
c.args(["--exact", "native_crash_macos::tests::child", "--test-threads=1", "--nocapture"]).env("SOLIDCRAFT_NATIVE_CRASH_CHILD", dir);
|
||||
if let Some(f) = fault {
|
||||
c.env("SOLIDCRAFT_NATIVE_CRASH_FAULT", f);
|
||||
}
|
||||
c.output().unwrap()
|
||||
}
|
||||
|
||||
/// Crash a child the way `fault` says; returns what the handler wrote and the next start's report.
|
||||
fn crash(name: &str, fault: &str, signal: c_int) -> (serde_json::Value, serde_json::Value, String) {
|
||||
let dir = std::env::temp_dir().join(format!("solidcraft-native-crash-{name}-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
let out = run(&dir, Some(fault));
|
||||
assert_eq!(out.status.signal(), Some(signal), "the child died of the signal itself: {out:?}");
|
||||
let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
|
||||
let info =
|
||||
std::fs::read_dir(dir.join("sessions")).unwrap().flatten().map(|e| e.path()).find(|p| p.to_string_lossy().ends_with(".native.json"));
|
||||
let info = info.expect("the handler wrote what it caught");
|
||||
let v: serde_json::Value = serde_json::from_slice(&std::fs::read(info).unwrap()).unwrap();
|
||||
// The next start reports it.
|
||||
let out = run(&dir, None);
|
||||
assert!(out.status.success(), "{out:?}");
|
||||
let reports = solidcraft_engine::crash::list(&dir);
|
||||
assert_eq!(reports.len(), 1, "{reports:?}");
|
||||
let r = solidcraft_engine::crash::read(&dir, &reports[0].id).unwrap();
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
(v, r, stderr)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_segfault_is_reported_with_its_signal_module_frames_and_step() {
|
||||
let (v, r, _) = crash("segv", "segv", SIGSEGV);
|
||||
assert_eq!(v["signal"], "SIGSEGV", "{v}");
|
||||
// Address 16 is in __PAGEZERO, mapped without access: SEGV_ACCERR.
|
||||
assert_eq!(v["detail"], "read of 0x0000000000000010 (SEGV_ACCERR)", "{v}");
|
||||
assert!(v["module"].as_str().unwrap().contains("solidcraft"), "the fault is in the test binary: {v}");
|
||||
let frames = v["frames"].as_array().unwrap();
|
||||
assert!(frames.len() >= 3, "{v}");
|
||||
assert!(frames.iter().any(|f| f.as_str().unwrap().contains("child")), "the walk reaches the test that crashed: {v}");
|
||||
let message = r["message"].as_str().unwrap();
|
||||
assert!(message.contains("segmentation fault (SIGSEGV) in solidcraft"), "{r}");
|
||||
assert!(message.contains("during startup (device creation"), "{r}");
|
||||
assert_eq!(r["kind"], "unclean_exit");
|
||||
assert_eq!(r["native"]["signal"], "SIGSEGV");
|
||||
assert!(r["backtrace"].as_array().unwrap().iter().any(|f| f.as_str().unwrap().contains("segfault")), "{r}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_abort_is_reported_as_sigabrt_with_the_code_that_called_it() {
|
||||
let (v, r, _) = crash("abrt", "abort", SIGABRT);
|
||||
assert_eq!(v["signal"], "SIGABRT", "{v}");
|
||||
assert!(v.get("detail").is_none(), "{v}");
|
||||
let frames = v["frames"].as_array().unwrap();
|
||||
assert!(frames.iter().any(|f| f.as_str().unwrap().contains("abort")), "{v}");
|
||||
assert!(r["message"].as_str().unwrap().contains("abort (SIGABRT)"), "{r}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_stack_overflow_is_recorded_and_the_standard_library_still_says_so() {
|
||||
// The fault on the guard page is recorded on the alternate stack; the standard library's
|
||||
// handler, called after ours, prints its message and aborts.
|
||||
let (v, r, stderr) = crash("overflow", "overflow", SIGABRT);
|
||||
assert!(matches!(v["signal"].as_str(), Some("SIGSEGV" | "SIGBUS")), "{v}");
|
||||
assert_eq!(v["name"], "stack overflow", "{v}");
|
||||
assert!(v["detail"].as_str().unwrap().contains("below the thread's stack"), "{v}");
|
||||
assert!(stderr.contains("has overflowed its stack"), "{stderr}");
|
||||
assert!(r["message"].as_str().unwrap().contains("stack overflow ("), "{r}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_segfault_signal_that_was_only_sent_still_ends_the_app() {
|
||||
// The standard library's handler, which comes before ours, returns expecting the fault
|
||||
// to happen again; a sent signal never does, so it goes to the default action instead.
|
||||
let (v, r, _) = crash("raise", "raise", SIGSEGV);
|
||||
assert_eq!(v["detail"], "sent with kill or raise, not a fault", "{v}");
|
||||
assert!(r["message"].as_str().unwrap().contains("(SIGSEGV)"), "{r}");
|
||||
}
|
||||
}
|
||||
@@ -15,14 +15,10 @@
|
||||
//! window rescales once at the crossing and stays under the pointer, instead of the OS's move
|
||||
//! loop and winit disagreeing about a frame the window doesn't have.
|
||||
//!
|
||||
//! Replacing a window procedure and setting a window class's icon have no safe API, hence the
|
||||
//! `unsafe_code` allowance on this module (the app crate denies it; see `native_crash.rs` and
|
||||
//! `open_documents.rs` for the others).
|
||||
//! Replacing a window procedure has no safe API, hence the `unsafe_code` allowance on this
|
||||
//! module (the app crate denies it; see `native_crash.rs` and `open_documents.rs` for the others).
|
||||
//! The procedure is installed once, on the thread that owns the window, for the main window only,
|
||||
//! and calls the previous one (winit's) for everything it doesn't answer.
|
||||
//!
|
||||
//! [`set_class_icon`] gives the window's class the exe's icon, so the taskbar button shows the
|
||||
//! mark even when the window is too busy to answer the taskbar's request for its icon.
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
use std::sync::atomic::{AtomicIsize, Ordering};
|
||||
@@ -31,18 +27,12 @@ use raw_window_handle::{HasWindowHandle, RawWindowHandle};
|
||||
use solidcraft_ui_egui::titlebar;
|
||||
use windows_sys::Win32::Foundation::{HWND, LPARAM, LRESULT, RECT, SIZE, WPARAM};
|
||||
use windows_sys::Win32::Graphics::Gdi::{GetMonitorInfoW, MONITOR_DEFAULTTONEAREST, MONITORINFO, MonitorFromWindow};
|
||||
use windows_sys::Win32::System::LibraryLoader::GetModuleHandleW;
|
||||
use windows_sys::Win32::UI::HiDpi::{GetDpiForWindow, GetSystemMetricsForDpi};
|
||||
use windows_sys::Win32::UI::WindowsAndMessaging::{
|
||||
CallWindowProcW, GCLP_HICON, GCLP_HICONSM, GWL_STYLE, GWLP_WNDPROC, GetClientRect, GetWindowLongPtrW, GetWindowRect, HTBOTTOM, HTBOTTOMLEFT,
|
||||
HTBOTTOMRIGHT, HTLEFT, HTRIGHT, HTTOP, HTTOPLEFT, HTTOPRIGHT, IMAGE_ICON, IsIconic, IsZoomed, LR_DEFAULTCOLOR, LoadImageW, SM_CXICON,
|
||||
SM_CXPADDEDBORDER, SM_CXSIZEFRAME, SM_CXSMICON, SetWindowLongPtrW, WM_GETDPISCALEDSIZE, WM_NCDESTROY, WM_NCHITTEST, WNDPROC, WS_THICKFRAME,
|
||||
CallWindowProcW, GWL_STYLE, GWLP_WNDPROC, GetClientRect, GetWindowLongPtrW, GetWindowRect, HTBOTTOM, HTBOTTOMLEFT, HTBOTTOMRIGHT, HTLEFT,
|
||||
HTRIGHT, HTTOP, HTTOPLEFT, HTTOPRIGHT, IsIconic, IsZoomed, SM_CXPADDEDBORDER, SM_CXSIZEFRAME, SetWindowLongPtrW, WM_GETDPISCALEDSIZE,
|
||||
WM_NCDESTROY, WM_NCHITTEST, WNDPROC, WS_THICKFRAME,
|
||||
};
|
||||
// windows-sys has SetClassLongPtrW only on 64-bit targets; on 32-bit Windows it is SetClassLongW.
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
use windows_sys::Win32::UI::WindowsAndMessaging::SetClassLongPtrW;
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
use windows_sys::Win32::UI::WindowsAndMessaging::SetClassLongW as SetClassLongPtrW;
|
||||
|
||||
/// The work area (the monitor without the taskbar) of the monitor nearest the window, in physical
|
||||
/// pixels: left, top, width, height.
|
||||
@@ -64,48 +54,6 @@ pub fn work_area(window: &impl HasWindowHandle) -> Option<(i32, i32, i32, i32)>
|
||||
Some((rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top))
|
||||
}
|
||||
|
||||
/// The exe's icon resource (`build.rs` embeds it as `1 ICON`).
|
||||
const ICON_RESOURCE: usize = 1;
|
||||
|
||||
/// Give the main window's class the exe's icon, big and small. Call it before the window is
|
||||
/// shown (eframe's app creator).
|
||||
///
|
||||
/// The taskbar asks a window for its icon when the window appears. eframe sets the window's
|
||||
/// icon before that, while the window is still hidden, and then shows the window at the end of
|
||||
/// its first frame; if that frame is slow, or startup kept the thread busy long enough for
|
||||
/// Windows to mark the window as not responding, the request isn't answered in time. The taskbar then
|
||||
/// takes the window class's icon and doesn't ask again. winit registers its class without one,
|
||||
/// so the button showed Windows' generic icon for as long as the window was open.
|
||||
/// `false` if it isn't a Win32 window or the exe's icon doesn't load; SetClassLongPtrW's own
|
||||
/// failure can't be told from a class that had no icon (both return 0), so it isn't reported.
|
||||
pub fn set_class_icon(window: &impl HasWindowHandle) -> bool {
|
||||
let Ok(handle) = window.window_handle() else { return false };
|
||||
let RawWindowHandle::Win32(w) = handle.as_raw() else { return false };
|
||||
let hwnd = w.hwnd.get() as HWND;
|
||||
// SAFETY: a plain query on the live main window.
|
||||
let dpi = unsafe { GetDpiForWindow(hwnd) };
|
||||
let (Some(big), Some(small)) = (exe_icon(SM_CXICON, dpi), exe_icon(SM_CXSMICON, dpi)) else { return false };
|
||||
// SAFETY: `hwnd` is the live main window and both icons are valid handles. The class keeps
|
||||
// them for the rest of the process, so they are never destroyed. `as _`: on 32-bit Windows
|
||||
// this is SetClassLongW (see the imports), which takes an i32.
|
||||
unsafe {
|
||||
SetClassLongPtrW(hwnd, GCLP_HICON, big as _);
|
||||
SetClassLongPtrW(hwnd, GCLP_HICONSM, small as _);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// The exe's icon at the size of system metric `metric` (`SM_CXICON`, `SM_CXSMICON`) at `dpi`.
|
||||
fn exe_icon(metric: i32, dpi: u32) -> Option<windows_sys::Win32::Foundation::HANDLE> {
|
||||
// SAFETY: GetModuleHandleW(null) is this exe, loaded for the life of the process; the name
|
||||
// is an integer resource id (MAKEINTRESOURCE), which LoadImageW accepts in place of a string.
|
||||
let icon = unsafe {
|
||||
let size = GetSystemMetricsForDpi(metric, dpi);
|
||||
LoadImageW(GetModuleHandleW(std::ptr::null()), std::ptr::without_provenance(ICON_RESOURCE), IMAGE_ICON, size, size, LR_DEFAULTCOLOR)
|
||||
};
|
||||
(!icon.is_null()).then_some(icon)
|
||||
}
|
||||
|
||||
/// The window procedure ours replaced (winit's), as an address; 0 before [`install`].
|
||||
static PREVIOUS: AtomicIsize = AtomicIsize::new(0);
|
||||
|
||||
|
||||
@@ -105,7 +105,6 @@ impl NativeMenu {
|
||||
("messages", "Messages\u{2026}"),
|
||||
("report", "Report an Issue\u{2026}"),
|
||||
("crashes", "Crash Reports\u{2026}"),
|
||||
("install_cli", "Install solidcraft-cli command"),
|
||||
] {
|
||||
let item = MenuItem::new(app.tr_label(label), true, None);
|
||||
help.append(&item)?;
|
||||
|
||||
@@ -1,463 +0,0 @@
|
||||
//! The navigation bridge: a 3D input device's own program (a homemade device's driver app) moves
|
||||
//! the view over a loopback connection, the way a 3D mouse driver hands its axes to the programs
|
||||
//! it supports, so the device emulates no mouse and the pointer stays the user's.
|
||||
//!
|
||||
//! The device app listens on `127.0.0.1:<port>`; with Preferences › Navigation › "Navigate with a
|
||||
//! device app" on (off by default), SolidCraft connects to it, says hello and reads one JSON
|
||||
//! object per line (docs/3d-mouse.md). The reader thread only sums the motion; the egui thread
|
||||
//! hands it to the view once per frame (`space_mouse::bridge_frame`). A device the app says it
|
||||
//! reads is left to it by the 3D mouse reader ([`Claim`]), so the view doesn't move twice.
|
||||
|
||||
use std::io::{ErrorKind, Read, Write};
|
||||
use std::net::{Ipv4Addr, SocketAddr, TcpStream};
|
||||
use std::sync::{Arc, Mutex, MutexGuard};
|
||||
use std::time::Duration;
|
||||
|
||||
use egui::Context;
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_ui_egui::SolveApp;
|
||||
use solidcraft_ui_egui::space_mouse::{BRIDGE_VIEWS, BridgeMotion, bridge_frame, bridge_step};
|
||||
|
||||
/// The longest line read; a longer one is skipped to its end.
|
||||
const MAX_LINE: usize = 4096;
|
||||
/// Standard views waiting for the next frame; past this many, more are a flood and dropped.
|
||||
const MAX_VIEWS: usize = 8;
|
||||
/// The longest name a device app may give for itself or its device, in characters.
|
||||
const MAX_NAME: usize = 64;
|
||||
/// How long a connection attempt waits.
|
||||
const CONNECT_TIMEOUT: Duration = Duration::from_millis(500);
|
||||
/// How long a read waits for data before the reader looks at the preferences again.
|
||||
const READ_WAKE: Duration = Duration::from_millis(250);
|
||||
/// The wait before connecting again when no device app answered or one went away.
|
||||
const RETRY_AFTER: Duration = Duration::from_secs(1);
|
||||
/// In zoom mode, a motion of this many points along Y is one zoom step: with 8 % a step, the 1 %
|
||||
/// a point that a zoom drag moves.
|
||||
const POINTS_PER_ZOOM_STEP: f64 = 8.0;
|
||||
|
||||
/// What the device's motion drives, as the device app last said (`mode`).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
enum Mode {
|
||||
#[default]
|
||||
Orbit,
|
||||
Pan,
|
||||
Zoom,
|
||||
}
|
||||
|
||||
/// What the reader thread and the egui thread share.
|
||||
#[derive(Debug, Default)]
|
||||
struct Shared {
|
||||
/// The port the preferences ask to connect to; none while the bridge is off.
|
||||
wanted: Option<u16>,
|
||||
/// The device app connected, by the name it gave ("?" until it gives one).
|
||||
connected: Option<String>,
|
||||
/// The device the connected app reads, as it named it: the 3D mouse reader leaves that one alone.
|
||||
device: Option<String>,
|
||||
/// Motion since the egui thread last took it.
|
||||
motion: BridgeMotion,
|
||||
/// Standard views asked for since then.
|
||||
views: Vec<String>,
|
||||
}
|
||||
|
||||
fn lock(shared: &Mutex<Shared>) -> Option<MutexGuard<'_, Shared>> {
|
||||
shared.lock().ok()
|
||||
}
|
||||
|
||||
/// A name a device app gave, without control characters and cut to [`MAX_NAME`]; none when empty.
|
||||
fn name(value: Option<&Value>) -> Option<String> {
|
||||
let name: String = value?.as_str()?.chars().filter(|c| !c.is_control()).take(MAX_NAME).collect();
|
||||
let name = name.trim();
|
||||
(!name.is_empty()).then(|| name.to_string())
|
||||
}
|
||||
|
||||
/// A number of a message, finite and in range; none when it isn't one.
|
||||
fn number(msg: &Value, key: &str) -> f64 {
|
||||
bridge_step(msg.get(key).and_then(Value::as_f64).unwrap_or(0.0))
|
||||
}
|
||||
|
||||
/// Takes one line from the device app into `shared`. Lines that aren't a message this version
|
||||
/// knows are ignored. Returns whether the view has something new to show.
|
||||
fn consume(line: &[u8], mode: &mut Mode, shared: &mut Shared) -> bool {
|
||||
let Ok(msg) = serde_json::from_slice::<Value>(line) else { return false };
|
||||
let add = |shared: &mut Shared, motion: BridgeMotion| {
|
||||
shared.motion = shared.motion.plus(motion);
|
||||
!motion.is_still()
|
||||
};
|
||||
match msg.get("t").and_then(Value::as_str) {
|
||||
Some("axis") => {
|
||||
let (x, y) = (number(&msg, "x"), number(&msg, "y"));
|
||||
let motion = match mode {
|
||||
Mode::Orbit => BridgeMotion { orbit: [x, y], ..Default::default() },
|
||||
Mode::Pan => BridgeMotion { pan: [x, y], ..Default::default() },
|
||||
// Up zooms in, as a zoom drag does.
|
||||
Mode::Zoom => BridgeMotion { zoom: -y / POINTS_PER_ZOOM_STEP, ..Default::default() },
|
||||
};
|
||||
add(shared, motion)
|
||||
}
|
||||
Some("zoom") => add(shared, BridgeMotion { zoom: number(&msg, "d"), ..Default::default() }),
|
||||
Some("mode") => {
|
||||
let on = msg.get("on").and_then(Value::as_bool).unwrap_or(false);
|
||||
*mode = match msg.get("mode").and_then(Value::as_str) {
|
||||
Some("pan") if on => Mode::Pan,
|
||||
Some("zoom") if on => Mode::Zoom,
|
||||
_ => Mode::Orbit,
|
||||
};
|
||||
false
|
||||
}
|
||||
Some("view") => {
|
||||
let view = msg.get("name").and_then(Value::as_str).and_then(|n| BRIDGE_VIEWS.iter().find(|v| **v == n));
|
||||
match view {
|
||||
Some(view) if shared.views.len() < MAX_VIEWS => {
|
||||
shared.views.push((*view).to_string());
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
Some("hello") => {
|
||||
shared.connected = name(msg.get("server")).or_else(|| shared.connected.clone());
|
||||
shared.device = name(msg.get("device"));
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Splits what arrives into lines, across reads. A line longer than [`MAX_LINE`] is dropped up to
|
||||
/// its newline, so a peer that never sends one can't make the reader hold more than that.
|
||||
#[derive(Default)]
|
||||
struct Lines {
|
||||
line: Vec<u8>,
|
||||
skipping: bool,
|
||||
}
|
||||
|
||||
impl Lines {
|
||||
fn feed(&mut self, mut data: &[u8], mut each: impl FnMut(&[u8])) {
|
||||
while !data.is_empty() {
|
||||
let end = data.iter().position(|b| *b == b'\n');
|
||||
let (part, rest) = match end {
|
||||
Some(i) => (data.get(..i).unwrap_or_default(), data.get(i + 1..).unwrap_or_default()),
|
||||
None => (data, &[][..]),
|
||||
};
|
||||
if !self.skipping {
|
||||
let room = (MAX_LINE + 1).saturating_sub(self.line.len());
|
||||
self.line.extend_from_slice(part.get(..room.min(part.len())).unwrap_or_default());
|
||||
if self.line.len() > MAX_LINE {
|
||||
self.line.clear();
|
||||
self.skipping = true;
|
||||
}
|
||||
}
|
||||
if end.is_some() {
|
||||
if !self.skipping {
|
||||
each(self.line.strip_suffix(b"\r").unwrap_or(&self.line));
|
||||
}
|
||||
self.line.clear();
|
||||
self.skipping = false;
|
||||
}
|
||||
data = rest;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What SolidCraft says first, so the device app knows which program it is driving.
|
||||
fn hello() -> Value {
|
||||
json!({"t": "hello", "app": "solidcraft", "version": env!("CARGO_PKG_VERSION")})
|
||||
}
|
||||
|
||||
/// Reads one connection until the device app closes it, it fails, or the preferences no longer
|
||||
/// ask for this port; then forgets what it had.
|
||||
fn serve(mut stream: TcpStream, port: u16, ctx: &Context, shared: &Mutex<Shared>) {
|
||||
// Without a read timeout the reader couldn't notice the bridge being switched off.
|
||||
if stream.set_read_timeout(Some(READ_WAKE)).is_err() || writeln!(stream, "{}", hello()).is_err() {
|
||||
return;
|
||||
}
|
||||
let _ = stream.set_nodelay(true);
|
||||
log::info!("navigation bridge: connected to 127.0.0.1:{port}");
|
||||
if let Some(mut shared) = lock(shared) {
|
||||
shared.connected = Some("?".into());
|
||||
}
|
||||
ctx.request_repaint();
|
||||
let (mut lines, mut mode) = (Lines::default(), Mode::default());
|
||||
let mut chunk = [0_u8; 4096];
|
||||
while lock(shared).is_some_and(|shared| shared.wanted == Some(port)) {
|
||||
match stream.read(&mut chunk) {
|
||||
Ok(0) => break,
|
||||
Ok(size) => {
|
||||
let mut news = false;
|
||||
if let Some(mut shared) = lock(shared) {
|
||||
lines.feed(chunk.get(..size).unwrap_or_default(), |line| news |= consume(line, &mut mode, &mut shared));
|
||||
}
|
||||
if news {
|
||||
ctx.request_repaint();
|
||||
}
|
||||
}
|
||||
Err(error) if matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut | ErrorKind::Interrupted) => {}
|
||||
Err(error) => {
|
||||
log::info!("navigation bridge: connection lost: {error}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(mut shared) = lock(shared) {
|
||||
*shared = Shared { wanted: shared.wanted, ..Shared::default() };
|
||||
}
|
||||
ctx.request_repaint();
|
||||
}
|
||||
|
||||
/// Connects to the device app while the preferences ask for it, again whenever it goes away.
|
||||
fn reader(ctx: Context, shared: Arc<Mutex<Shared>>) {
|
||||
loop {
|
||||
let Some(port) = lock(&shared).and_then(|shared| shared.wanted) else {
|
||||
std::thread::sleep(READ_WAKE);
|
||||
continue;
|
||||
};
|
||||
if let Ok(stream) = TcpStream::connect_timeout(&SocketAddr::from((Ipv4Addr::LOCALHOST, port)), CONNECT_TIMEOUT) {
|
||||
serve(stream, port, &ctx, &shared);
|
||||
}
|
||||
std::thread::sleep(RETRY_AFTER);
|
||||
}
|
||||
}
|
||||
|
||||
/// Which 3D mouse the connected device app reads, for the 3D mouse reader to leave alone. Only
|
||||
/// the Windows and macOS readers know a device's strings to compare.
|
||||
#[derive(Clone)]
|
||||
#[cfg_attr(not(any(target_os = "windows", target_os = "macos")), allow(dead_code))]
|
||||
pub struct Claim(Arc<Mutex<Shared>>);
|
||||
|
||||
#[cfg_attr(not(any(target_os = "windows", target_os = "macos")), allow(dead_code))]
|
||||
impl Claim {
|
||||
/// Whether a device with these manufacturer and product strings is the one the connected
|
||||
/// device app reads (its name appears in one, ignoring case).
|
||||
pub fn claims(&self, strings: &[String]) -> bool {
|
||||
let Some(device) = lock(&self.0).and_then(|shared| shared.device.clone()) else { return false };
|
||||
let device = device.to_lowercase();
|
||||
strings.iter().any(|s| s.to_lowercase().contains(&device))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NavBridge {
|
||||
shared: Arc<Mutex<Shared>>,
|
||||
}
|
||||
|
||||
impl NavBridge {
|
||||
pub fn start(ctx: &Context) -> Self {
|
||||
let shared = Arc::new(Mutex::new(Shared::default()));
|
||||
let (worker_ctx, worker_state) = (ctx.clone(), Arc::clone(&shared));
|
||||
if let Err(error) = std::thread::Builder::new().name("solidcraft-nav-bridge".into()).spawn(move || reader(worker_ctx, worker_state)) {
|
||||
log::warn!("navigation bridge: reader could not start: {error}");
|
||||
}
|
||||
Self { shared }
|
||||
}
|
||||
|
||||
#[cfg_attr(not(any(target_os = "windows", target_os = "macos")), allow(dead_code))]
|
||||
pub fn claim(&self) -> Claim {
|
||||
Claim(Arc::clone(&self.shared))
|
||||
}
|
||||
|
||||
/// Each frame: passes the preferences to the reader, and the motion and views since the last
|
||||
/// frame to the view. Motion that arrived while the window had no focus is dropped with it.
|
||||
pub fn tick(&mut self, app: &mut SolveApp, ctx: &Context) {
|
||||
let wanted = Some(app.preferences.nav_bridge_port).filter(|port| app.preferences.nav_bridge_enabled && *port != 0);
|
||||
let Some((motion, views, connected)) = lock(&self.shared).map(|mut shared| {
|
||||
shared.wanted = wanted;
|
||||
(std::mem::take(&mut shared.motion), std::mem::take(&mut shared.views), shared.connected.clone())
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
app.space_mouse.bridge = connected.filter(|_| wanted.is_some());
|
||||
if !motion.is_still() || !views.is_empty() {
|
||||
bridge_frame(app, ctx, motion, &views);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::net::TcpListener;
|
||||
use std::time::Instant;
|
||||
|
||||
fn feed(shared: &mut Shared, mode: &mut Mode, text: &str) -> bool {
|
||||
let mut news = false;
|
||||
Lines::default().feed(text.as_bytes(), |line| news |= consume(line, mode, shared));
|
||||
news
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn axis_motion_orbits_pans_or_zooms_as_the_mode_says() {
|
||||
let (mut shared, mut mode) = (Shared::default(), Mode::default());
|
||||
assert!(feed(&mut shared, &mut mode, "{\"t\":\"axis\",\"x\":3,\"y\":-2}\n{\"t\":\"axis\",\"x\":1.5,\"y\":0}\n"));
|
||||
assert_eq!(shared.motion, BridgeMotion { orbit: [4.5, -2.0], ..Default::default() });
|
||||
feed(&mut shared, &mut mode, "{\"t\":\"mode\",\"mode\":\"pan\",\"on\":true}\n{\"t\":\"axis\",\"x\":5,\"y\":6}\n");
|
||||
assert_eq!(mode, Mode::Pan);
|
||||
assert_eq!(shared.motion.pan, [5.0, 6.0]);
|
||||
feed(&mut shared, &mut mode, "{\"t\":\"mode\",\"mode\":\"zoom\",\"on\":true}\n{\"t\":\"axis\",\"x\":0,\"y\":-16}\n");
|
||||
assert_eq!(shared.motion.zoom, 2.0, "up zooms in, 8 points a step");
|
||||
// Off, whichever mode it names, is back to orbit.
|
||||
feed(&mut shared, &mut mode, "{\"t\":\"mode\",\"mode\":\"pan\",\"on\":false}\n");
|
||||
assert_eq!(mode, Mode::Orbit);
|
||||
// The ring's steps add up with the zoom mode's.
|
||||
feed(&mut shared, &mut mode, "{\"t\":\"zoom\",\"d\":-0.5}\n");
|
||||
assert_eq!(shared.motion.zoom, 1.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn views_are_queued_by_their_known_names_only() {
|
||||
let (mut shared, mut mode) = (Shared::default(), Mode::default());
|
||||
assert!(feed(&mut shared, &mut mode, "{\"t\":\"view\",\"name\":\"iso\"}\n{\"t\":\"view\",\"name\":\"fit\"}\n"));
|
||||
assert!(!feed(&mut shared, &mut mode, "{\"t\":\"view\",\"name\":\"ISO\"}\n{\"t\":\"view\",\"name\":\"../etc\"}\n{\"t\":\"view\"}\n"));
|
||||
assert_eq!(shared.views, ["iso", "fit"]);
|
||||
let flood = "{\"t\":\"view\",\"name\":\"top\"}\n".repeat(100);
|
||||
feed(&mut shared, &mut mode, &flood);
|
||||
assert_eq!(shared.views.len(), MAX_VIEWS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_lines_never_move_the_view_out_of_range() {
|
||||
let (mut shared, mut mode) = (Shared::default(), Mode::default());
|
||||
let lines = [
|
||||
"not json",
|
||||
"{\"t\":\"axis\",\"x\":\"5\",\"y\":null}",
|
||||
"{\"t\":\"axis\",\"x\":1e308,\"y\":-1e308}",
|
||||
"{\"t\":\"axis\",\"x\":1e999}",
|
||||
"{\"t\":\"zoom\",\"d\":[1]}",
|
||||
"{\"t\":42}",
|
||||
"[1,2,3]",
|
||||
"{\"t\":\"mode\",\"mode\":{\"x\":1},\"on\":\"yes\"}",
|
||||
"\u{0}\u{1}\u{2}",
|
||||
];
|
||||
for line in lines {
|
||||
Lines::default().feed(format!("{line}\n").as_bytes(), |l| {
|
||||
consume(l, &mut mode, &mut shared);
|
||||
});
|
||||
}
|
||||
let limit = solidcraft_ui_egui::space_mouse::BRIDGE_MAX_STEP;
|
||||
let values = shared.motion.orbit.into_iter().chain(shared.motion.pan).chain([shared.motion.zoom]);
|
||||
for value in values {
|
||||
assert!(value.is_finite() && value.abs() <= limit, "{value}");
|
||||
}
|
||||
assert_eq!(mode, Mode::Orbit);
|
||||
// Invalid UTF-8 is no message either.
|
||||
assert!(!consume(&[0xff, 0xfe, b'{', b'}'], &mut mode, &mut shared));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lines_split_across_reads_and_long_ones_are_skipped_to_their_end() {
|
||||
let mut got = Vec::new();
|
||||
let mut lines = Lines::default();
|
||||
for part in ["{\"t\":", "\"zoom\"}\r\n{\"a\"", ":1}\n"] {
|
||||
lines.feed(part.as_bytes(), |line| got.push(String::from_utf8_lossy(line).to_string()));
|
||||
}
|
||||
assert_eq!(got, ["{\"t\":\"zoom\"}", "{\"a\":1}"]);
|
||||
got.clear();
|
||||
let long = vec![b'x'; MAX_LINE * 3];
|
||||
lines.feed(&long, |line| got.push(String::from_utf8_lossy(line).to_string()));
|
||||
lines.feed(&long, |line| got.push(String::from_utf8_lossy(line).to_string()));
|
||||
assert!(lines.line.len() <= MAX_LINE + 1, "never holds more than one line");
|
||||
lines.feed(b"tail\nnext\n", |line| got.push(String::from_utf8_lossy(line).to_string()));
|
||||
assert_eq!(got, ["next"], "the long line is dropped to its newline, the next one read");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_device_app_names_itself_and_the_device_it_leaves_alone() {
|
||||
let shared = Arc::new(Mutex::new(Shared::default()));
|
||||
let claim = Claim(Arc::clone(&shared));
|
||||
let strings = ["STMicroelectronics".to_string(), "MAKMOUSE 3D".to_string()];
|
||||
assert!(!claim.claims(&strings), "nothing is claimed before an app says so");
|
||||
let mut mode = Mode::default();
|
||||
if let Ok(mut shared) = shared.lock() {
|
||||
let hello = format!("{{\"t\":\"hello\",\"server\":\"Dev\\u0007App {}\",\"device\":\"makmouse\"}}\n", "x".repeat(200));
|
||||
feed(&mut shared, &mut mode, &hello);
|
||||
assert_eq!(shared.connected.as_deref().map(|n| n.chars().count()), Some(MAX_NAME));
|
||||
assert!(shared.connected.as_deref().is_some_and(|n| n.starts_with("DevApp")));
|
||||
}
|
||||
assert!(claim.claims(&strings), "the name matches ignoring case");
|
||||
assert!(!claim.claims(&["3Dconnexion".to_string(), "SpaceMouse Compact".to_string()]), "another 3D mouse stays the reader's");
|
||||
}
|
||||
|
||||
/// Waits until `check` holds, or fails after a few seconds.
|
||||
fn wait_for(what: &str, mut check: impl FnMut() -> bool) {
|
||||
let deadline = Instant::now() + Duration::from_secs(5);
|
||||
while !check() {
|
||||
assert!(Instant::now() < deadline, "timed out waiting for {what}");
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_connection_says_hello_reads_the_stream_and_ends_when_the_bridge_is_switched_off() {
|
||||
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).unwrap();
|
||||
let port = listener.local_addr().unwrap().port();
|
||||
let shared = Arc::new(Mutex::new(Shared { wanted: Some(port), ..Default::default() }));
|
||||
let client = TcpStream::connect((Ipv4Addr::LOCALHOST, port)).unwrap();
|
||||
let (mut server, _) = listener.accept().unwrap();
|
||||
let worker = {
|
||||
let shared = Arc::clone(&shared);
|
||||
std::thread::spawn(move || serve(client, port, &Context::default(), &shared))
|
||||
};
|
||||
let mut hello_line = String::new();
|
||||
BufReader::new(server.try_clone().unwrap()).read_line(&mut hello_line).unwrap();
|
||||
let hello: Value = serde_json::from_str(&hello_line).unwrap();
|
||||
assert_eq!((hello["t"].as_str(), hello["app"].as_str()), (Some("hello"), Some("solidcraft")));
|
||||
server
|
||||
.write_all(b"{\"t\":\"hello\",\"server\":\"MAKMOUSE\",\"device\":\"MAKMOUSE\"}\n{\"t\":\"axis\",\"x\":2,\"y\":1}\n{\"t\":\"view\",\"name\":\"top\"}\n")
|
||||
.unwrap();
|
||||
wait_for("the stream", || shared.lock().is_ok_and(|s| s.motion.orbit == [2.0, 1.0] && s.views == ["top"]));
|
||||
assert_eq!(shared.lock().unwrap().connected.as_deref(), Some("MAKMOUSE"));
|
||||
// Switched off: the connection ends and what it had is forgotten.
|
||||
shared.lock().unwrap().wanted = None;
|
||||
worker.join().unwrap();
|
||||
let after = shared.lock().unwrap();
|
||||
assert!(after.connected.is_none() && after.device.is_none() && after.motion.is_still() && after.views.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_closed_connection_forgets_the_app() {
|
||||
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).unwrap();
|
||||
let port = listener.local_addr().unwrap().port();
|
||||
let shared = Arc::new(Mutex::new(Shared { wanted: Some(port), ..Default::default() }));
|
||||
let client = TcpStream::connect((Ipv4Addr::LOCALHOST, port)).unwrap();
|
||||
let (server, _) = listener.accept().unwrap();
|
||||
let worker = {
|
||||
let shared = Arc::clone(&shared);
|
||||
std::thread::spawn(move || serve(client, port, &Context::default(), &shared))
|
||||
};
|
||||
wait_for("the connection", || shared.lock().is_ok_and(|s| s.connected.is_some()));
|
||||
drop(server);
|
||||
worker.join().unwrap();
|
||||
let after = shared.lock().unwrap();
|
||||
assert_eq!((after.connected.as_deref(), after.wanted), (None, Some(port)), "still wanted, so the reader connects again");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_frame_hands_the_view_the_motion_and_passes_the_preferences_on() {
|
||||
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() }, |_| {});
|
||||
let mut bridge = NavBridge { shared: Arc::new(Mutex::new(Shared::default())) };
|
||||
let queue = |bridge: &NavBridge| {
|
||||
let mut shared = bridge.shared.lock().unwrap();
|
||||
shared.connected = Some("MAKMOUSE".into());
|
||||
shared.motion = BridgeMotion { orbit: [10.0, 0.0], ..Default::default() };
|
||||
};
|
||||
// Off (the default): nothing is wanted, nothing reaches the view.
|
||||
queue(&bridge);
|
||||
bridge.tick(&mut app, &ctx);
|
||||
assert_eq!(bridge.shared.lock().unwrap().wanted, None);
|
||||
assert!(app.viewport.bridge_motion.is_none() && app.space_mouse.bridge.is_none());
|
||||
// On: the port is passed to the reader and the motion to the view, once.
|
||||
app.preferences.nav_bridge_enabled = true;
|
||||
app.preferences.nav_bridge_port = 9123;
|
||||
queue(&bridge);
|
||||
bridge.tick(&mut app, &ctx);
|
||||
assert_eq!(bridge.shared.lock().unwrap().wanted, Some(9123));
|
||||
assert_eq!(app.space_mouse.bridge.as_deref(), Some("MAKMOUSE"));
|
||||
assert_eq!(app.viewport.bridge_motion.map(|m| m.orbit), Some([10.0, 0.0]));
|
||||
assert!(bridge.shared.lock().unwrap().motion.is_still(), "taken, not handed over twice");
|
||||
// Port 0 is no port.
|
||||
app.preferences.nav_bridge_port = 0;
|
||||
bridge.tick(&mut app, &ctx);
|
||||
assert_eq!(bridge.shared.lock().unwrap().wanted, None);
|
||||
}
|
||||
}
|
||||
@@ -241,7 +241,6 @@ struct Reading {
|
||||
device: Option<Source>,
|
||||
/// A source that is connected but has not yet shown a device behind it (spacenavd's socket answers
|
||||
/// with no mouse plugged in; its protocol has no enumeration, so the first record is the evidence).
|
||||
#[cfg(target_os = "linux")]
|
||||
awaiting: Option<Source>,
|
||||
axes: Axes,
|
||||
/// When each of X, Y, Z, Rx, Ry and Rz last arrived. Each ages on its own: a device may send
|
||||
@@ -288,7 +287,6 @@ impl Reading {
|
||||
}
|
||||
|
||||
/// A record arrived from the source being waited on: there is a device.
|
||||
#[cfg(target_os = "linux")]
|
||||
fn heard(&mut self) {
|
||||
if self.device.is_none() {
|
||||
self.device = self.awaiting.take();
|
||||
@@ -298,7 +296,6 @@ impl Reading {
|
||||
fn new(now: Instant) -> Self {
|
||||
Self {
|
||||
device: None,
|
||||
#[cfg(target_os = "linux")]
|
||||
awaiting: None,
|
||||
axes: Axes::default(),
|
||||
at: [now; 6],
|
||||
@@ -463,20 +460,12 @@ fn connected(ctx: &Context, shared: &Mutex<Reading>, source: Source) {
|
||||
let now = Instant::now();
|
||||
let stale = now.checked_sub(STALE_AFTER).unwrap_or(now);
|
||||
// A daemon is not yet a device: it counts once a record comes from it.
|
||||
let waiting = needs_evidence(source);
|
||||
let (device, awaiting) = if needs_evidence(source) { (None, Some(source)) } else { (Some(source), None) };
|
||||
// A HID device reports the state of its buttons, so the first state seen is a baseline; the daemon
|
||||
// and the driver send presses and releases, which start from nothing down.
|
||||
let buttons_known = !matches!(source, Source::Hid(_));
|
||||
let generation = CONNECTIONS.fetch_add(1, Ordering::Relaxed).wrapping_add(1);
|
||||
*reading = Reading {
|
||||
device: (!waiting).then_some(source),
|
||||
#[cfg(target_os = "linux")]
|
||||
awaiting: waiting.then_some(source),
|
||||
at: [stale; 6],
|
||||
buttons_known,
|
||||
generation,
|
||||
..Reading::new(now)
|
||||
};
|
||||
*reading = Reading { device, awaiting, at: [stale; 6], buttons_known, generation, ..Reading::new(now) };
|
||||
}
|
||||
ctx.request_repaint();
|
||||
}
|
||||
@@ -521,175 +510,43 @@ fn hid_api() -> hidapi::HidResult<HidApi> {
|
||||
HidApi::new()
|
||||
}
|
||||
|
||||
/// An open 3D-mouse interface: the device, how its reports are laid out, and its vendor and
|
||||
/// product ids.
|
||||
#[cfg(any(target_os = "windows", target_os = "macos", test))]
|
||||
struct Candidate<D> {
|
||||
device: D,
|
||||
layout: Layout,
|
||||
ids: (u16, u16),
|
||||
/// The device's manufacturer and product strings ([`device_strings`]).
|
||||
#[cfg_attr(not(any(target_os = "windows", target_os = "macos")), allow(dead_code))]
|
||||
strings: Vec<String>,
|
||||
}
|
||||
|
||||
/// Input reports read with a timeout in milliseconds: hidapi's devices, and the tests' simulated
|
||||
/// ones. `Ok(0)` is a timeout; an error means the device is gone.
|
||||
#[cfg(any(target_os = "windows", target_os = "macos", test))]
|
||||
trait ReportSource {
|
||||
fn read_report(&self, report: &mut [u8], timeout_ms: i32) -> Result<usize, String>;
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
impl ReportSource for hidapi::HidDevice {
|
||||
fn read_report(&self, report: &mut [u8], timeout_ms: i32) -> Result<usize, String> {
|
||||
self.read_timeout(report, timeout_ms).map_err(|error| error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// What following the candidates tells the reader.
|
||||
#[cfg(any(target_os = "windows", target_os = "macos", test))]
|
||||
enum Followed<'a> {
|
||||
/// This device is the one read now, so its calibration applies. With the buttons it last
|
||||
/// reported down, when it reported them while another was read: they are its baseline, so a
|
||||
/// press that makes it the device in use counts as a press, not as a button already held.
|
||||
Device((u16, u16), Option<u32>),
|
||||
/// An input report from it.
|
||||
Report(&'a Layout, &'a [u8]),
|
||||
/// It is gone.
|
||||
Gone,
|
||||
}
|
||||
|
||||
/// Whether a report shows its device in use: an axis off zero or a button down. A cap at rest
|
||||
/// sends zeros at most, and an idle 3Dconnexion Universal Receiver sends nothing at all.
|
||||
#[cfg(any(target_os = "windows", target_os = "macos", test))]
|
||||
fn shows_use(layout: Layout, report: &[u8]) -> bool {
|
||||
layout.decode(report).is_some_and(|decoded| decoded.axes.iter().flatten().any(|axis| *axis != 0) || decoded.buttons.is_some_and(|down| down != 0))
|
||||
}
|
||||
|
||||
/// Reads every candidate and follows the one in use (#3988). The first is followed from the start,
|
||||
/// as when it was the only one read. Another takes over once it shows use while the followed one
|
||||
/// has shown none for `STALE_AFTER`, so two devices moved together don't hand the reading back
|
||||
/// and forth. An idle Universal Receiver opens fine and never fails; read on its own it kept a
|
||||
/// SpaceMouse next to it from ever being read. A candidate that fails is dropped. If it was the
|
||||
/// followed one, the reading is reset and the next takes over. Returns when none is left.
|
||||
#[cfg(any(target_os = "windows", target_os = "macos", test))]
|
||||
fn follow<D: ReportSource>(mut candidates: Vec<Candidate<D>>, now: impl Fn() -> Instant, mut emit: impl FnMut(Followed<'_>)) {
|
||||
let Some(first) = candidates.first() else { return };
|
||||
emit(Followed::Device(first.ids, None));
|
||||
let mut followed = 0;
|
||||
// When the followed device last showed use.
|
||||
let mut used_at: Option<Instant> = None;
|
||||
// The buttons each candidate last reported down, by index, as long as it has reported them.
|
||||
let mut buttons: Vec<Option<u32>> = candidates.iter().map(|_| None).collect();
|
||||
let mut report = [0_u8; REPORT_BUFFER_BYTES];
|
||||
while !candidates.is_empty() {
|
||||
// One device is waited on as before; several take turns, briefly, so none waits behind
|
||||
// an idle one. Reports queue in the system meanwhile, so none is lost.
|
||||
let timeout_ms = if candidates.len() == 1 { 100 } else { 10 };
|
||||
let mut index = 0;
|
||||
while let Some(candidate) = candidates.get(index) {
|
||||
match candidate.device.read_report(&mut report, timeout_ms) {
|
||||
Ok(0) => {}
|
||||
Ok(size) => {
|
||||
let bytes = report.get(..size).unwrap_or_default();
|
||||
let before = buttons.get(index).copied().flatten();
|
||||
if let (Some(seen), Some(down)) = (buttons.get_mut(index), candidate.layout.decode(bytes).and_then(|decoded| decoded.buttons)) {
|
||||
*seen = Some(down);
|
||||
}
|
||||
if shows_use(candidate.layout, bytes) {
|
||||
let at = now();
|
||||
if index != followed && used_at.is_none_or(|used| at.duration_since(used) >= STALE_AFTER) {
|
||||
let (vendor, product) = candidate.ids;
|
||||
log::info!("3D mouse: reading {vendor:04x}:{product:04x}, the device in use");
|
||||
followed = index;
|
||||
emit(Followed::Device(candidate.ids, before));
|
||||
}
|
||||
if index == followed {
|
||||
used_at = Some(at);
|
||||
}
|
||||
}
|
||||
if index == followed {
|
||||
emit(Followed::Report(&candidate.layout, bytes));
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
log::info!("3D mouse disconnected: {error}");
|
||||
candidates.remove(index);
|
||||
if index < buttons.len() {
|
||||
buttons.remove(index);
|
||||
}
|
||||
if index == followed {
|
||||
emit(Followed::Gone);
|
||||
followed = 0;
|
||||
used_at = None;
|
||||
if let Some(next) = candidates.first() {
|
||||
emit(Followed::Device(next.ids, buttons.first().copied().flatten()));
|
||||
}
|
||||
} else if index < followed {
|
||||
followed -= 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The device's manufacturer and product strings, which say whether a device app has it
|
||||
/// ([`crate::nav_bridge::Claim`]). Asked of the opened 3D mouse only.
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
fn device_strings(device: &hidapi::HidDevice) -> Vec<String> {
|
||||
[device.get_manufacturer_string(), device.get_product_string()].into_iter().filter_map(|s| s.ok().flatten()).collect()
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
fn reader(ctx: Context, shared: Arc<Mutex<Reading>>, bridge: crate::nav_bridge::Claim) {
|
||||
fn reader(ctx: Context, shared: Arc<Mutex<Reading>>) {
|
||||
let Ok(mut api) = hid_api() else {
|
||||
log::warn!("3D mouse: HID initialization failed");
|
||||
return;
|
||||
};
|
||||
loop {
|
||||
let open: Vec<_> = candidates(&mut api)
|
||||
.into_iter()
|
||||
.filter_map(|path| {
|
||||
let device = api
|
||||
.open_path(&path)
|
||||
.inspect_err(|error| {
|
||||
// On macOS: missing Input Monitoring permission, or the 3DxWare helper holding the device.
|
||||
// The driver having the device is the reason it cannot be opened, not a problem.
|
||||
if !DRIVER_CONNECTED.load(Ordering::Relaxed) && !DRIVER_REGISTERED.load(Ordering::Relaxed) {
|
||||
log::warn!("3D mouse: cannot open the device: {error}");
|
||||
}
|
||||
})
|
||||
.ok()?;
|
||||
// Read when the device was opened, from this device alone.
|
||||
let info = device.get_device_info().ok()?;
|
||||
let layout = device_layout(&device, info.vendor_id())?;
|
||||
let strings = device_strings(&device);
|
||||
Some(Candidate { device, layout, ids: (info.vendor_id(), info.product_id()), strings })
|
||||
})
|
||||
.collect();
|
||||
let names: Vec<_> = open.iter().map(|candidate| (candidate.ids, candidate.strings.clone())).collect();
|
||||
// Whether the device read now is one a device app has: it moves the view over the navigation
|
||||
// bridge, so read here as well the device would move it twice. What was read before ages out.
|
||||
let mut strings: &[String] = &[];
|
||||
follow(open, Instant::now, |event| match event {
|
||||
Followed::Device(ids, buttons) => {
|
||||
strings = names.iter().find(|(known, _)| *known == ids).map_or(&[][..], |(_, strings)| strings.as_slice());
|
||||
connected(&ctx, &shared, Source::Hid(Some(ids)));
|
||||
// Taken over from another device: what it reported meanwhile is its baseline.
|
||||
if let Some(buttons) = buttons
|
||||
&& let Ok(mut reading) = shared.lock()
|
||||
{
|
||||
reading.set_buttons(buttons);
|
||||
if let Some((device, layout, ids)) = candidates(&mut api).into_iter().find_map(|path| {
|
||||
let device = api
|
||||
.open_path(&path)
|
||||
.inspect_err(|error| {
|
||||
// On macOS: missing Input Monitoring permission, or the 3DxWare helper holding the device.
|
||||
// The driver having the device is the reason it cannot be opened, not a problem.
|
||||
if !DRIVER_CONNECTED.load(Ordering::Relaxed) && !DRIVER_REGISTERED.load(Ordering::Relaxed) {
|
||||
log::warn!("3D mouse: cannot open the device: {error}");
|
||||
}
|
||||
})
|
||||
.ok()?;
|
||||
// Read when the device was opened, from this device alone.
|
||||
let info = device.get_device_info().ok()?;
|
||||
let layout = device_layout(&device, info.vendor_id())?;
|
||||
Some((device, layout, (info.vendor_id(), info.product_id())))
|
||||
}) {
|
||||
connected(&ctx, &shared, Source::Hid(Some(ids)));
|
||||
let mut report = [0_u8; REPORT_BUFFER_BYTES];
|
||||
loop {
|
||||
match device.read_timeout(&mut report, 100) {
|
||||
Ok(0) => {}
|
||||
Ok(size) => publish(&ctx, &shared, layout, report.get(..size).unwrap_or_default()),
|
||||
Err(error) => {
|
||||
log::info!("3D mouse disconnected: {error}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Followed::Report(..) if bridge.claims(strings) => {}
|
||||
Followed::Report(layout, report) => publish(&ctx, &shared, *layout, report),
|
||||
Followed::Gone => reset(&ctx, &shared),
|
||||
});
|
||||
reset(&ctx, &shared);
|
||||
}
|
||||
std::thread::sleep(RETRY_AFTER);
|
||||
}
|
||||
}
|
||||
@@ -1071,8 +928,7 @@ pub struct SpaceMouse {
|
||||
}
|
||||
|
||||
impl SpaceMouse {
|
||||
/// Starts the reader. A device that `bridge` says its own app reads is left to that app.
|
||||
pub fn start(ctx: &Context, bridge: crate::nav_bridge::Claim) -> Self {
|
||||
pub fn start(ctx: &Context) -> Self {
|
||||
let now = Instant::now();
|
||||
let shared = Arc::new(Mutex::new(Reading::new(now)));
|
||||
let worker_state = Arc::clone(&shared);
|
||||
@@ -1081,14 +937,11 @@ impl SpaceMouse {
|
||||
let drivers = Arc::new(Drivers::default());
|
||||
#[cfg(target_os = "linux")]
|
||||
let worker_drivers = Arc::clone(&drivers);
|
||||
// hidraw names no device's strings to compare (`Claim`).
|
||||
#[cfg(target_os = "linux")]
|
||||
let _ = bridge;
|
||||
let spawned = std::thread::Builder::new().name("solidcraft-3d-mouse".into()).spawn(move || {
|
||||
#[cfg(target_os = "linux")]
|
||||
reader(worker_ctx, worker_state, worker_drivers, Machine);
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
reader(worker_ctx, worker_state, bridge);
|
||||
reader(worker_ctx, worker_state);
|
||||
});
|
||||
if let Err(error) = spawned {
|
||||
log::warn!("3D mouse: reader could not start: {error}");
|
||||
@@ -1430,175 +1283,6 @@ mod tests {
|
||||
assert!(shared.lock().unwrap().arrived, "but the report did arrive");
|
||||
}
|
||||
|
||||
/// A simulated 3D-mouse interface. Each read takes the next scripted step, a report or a
|
||||
/// timeout (empty), and sets the shared clock to the step's time. Once the script runs out,
|
||||
/// the device is unplugged.
|
||||
struct Simulated<'a> {
|
||||
clock: &'a std::cell::Cell<Instant>,
|
||||
start: Instant,
|
||||
steps: std::cell::RefCell<std::collections::VecDeque<(u64, Vec<u8>)>>,
|
||||
}
|
||||
|
||||
impl ReportSource for Simulated<'_> {
|
||||
fn read_report(&self, report: &mut [u8], _timeout_ms: i32) -> Result<usize, String> {
|
||||
let (ms, bytes) = self.steps.borrow_mut().pop_front().ok_or_else(|| "unplugged".to_string())?;
|
||||
self.clock.set(self.start + Duration::from_millis(ms));
|
||||
report.get_mut(..bytes.len()).ok_or_else(|| "report too long".to_string())?.copy_from_slice(&bytes);
|
||||
Ok(bytes.len())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum Seen {
|
||||
/// The product id now read.
|
||||
Device(u16),
|
||||
/// The buttons it had reported down before it was read, its baseline.
|
||||
Baseline(u32),
|
||||
/// A report published, the product id read when it was, and whether it showed use.
|
||||
Report(u16, Vec<u8>, bool),
|
||||
Gone,
|
||||
}
|
||||
|
||||
/// Follows simulated devices, given as (product id, scripted steps in ms), and returns what
|
||||
/// the reader was told.
|
||||
fn follow_simulated(devices: &[(u16, &[(u64, &[u8])])]) -> Vec<Seen> {
|
||||
follow_simulated_with(Layout::default(), devices)
|
||||
}
|
||||
|
||||
/// As `follow_simulated`, with every device's reports laid out as `layout`.
|
||||
fn follow_simulated_with(layout: Layout, devices: &[(u16, &[(u64, &[u8])])]) -> Vec<Seen> {
|
||||
let start = Instant::now();
|
||||
let clock = std::cell::Cell::new(start);
|
||||
let candidates = devices
|
||||
.iter()
|
||||
.map(|(product, steps)| Candidate {
|
||||
device: Simulated {
|
||||
clock: &clock,
|
||||
start,
|
||||
steps: std::cell::RefCell::new(steps.iter().map(|(ms, bytes)| (*ms, bytes.to_vec())).collect()),
|
||||
},
|
||||
layout,
|
||||
ids: (0x256f, *product),
|
||||
strings: Vec::new(),
|
||||
})
|
||||
.collect();
|
||||
let (mut seen, mut read) = (Vec::new(), 0);
|
||||
follow(
|
||||
candidates,
|
||||
|| clock.get(),
|
||||
|event| match event {
|
||||
Followed::Device((_, product), buttons) => {
|
||||
read = product;
|
||||
seen.push(Seen::Device(product));
|
||||
seen.extend(buttons.map(Seen::Baseline));
|
||||
}
|
||||
Followed::Report(layout, bytes) => seen.push(Seen::Report(read, bytes.to_vec(), shows_use(*layout, bytes))),
|
||||
Followed::Gone => seen.push(Seen::Gone),
|
||||
},
|
||||
);
|
||||
seen
|
||||
}
|
||||
|
||||
const RECEIVER: u16 = 0xc652;
|
||||
const SPACEMOUSE: u16 = 0xc63a;
|
||||
const PUSH_X: &[u8] = &[1, 100, 0, 0, 0, 0, 0];
|
||||
const AT_REST: &[u8] = &[1, 0, 0, 0, 0, 0, 0];
|
||||
const TILT: &[u8] = &[2, 0, 0, 50, 0, 0, 0];
|
||||
|
||||
/// #3988: a Universal Receiver with nothing paired is found first and never fails, and a wired
|
||||
/// SpaceMouse next to it was never read. The receiver is followed only until the SpaceMouse
|
||||
/// moves; the SpaceMouse's reports, its zeros included, are read from then on.
|
||||
#[test]
|
||||
fn an_idle_receiver_does_not_keep_a_spacemouse_from_being_read() {
|
||||
let receiver: &[(u64, &[u8])] = &[(0, &[]), (10, &[]), (20, &[]), (30, &[])];
|
||||
let spacemouse: &[(u64, &[u8])] = &[(5, &[]), (15, PUSH_X), (25, AT_REST), (35, TILT)];
|
||||
assert_eq!(
|
||||
follow_simulated(&[(RECEIVER, receiver), (SPACEMOUSE, spacemouse)]),
|
||||
[
|
||||
Seen::Device(RECEIVER),
|
||||
Seen::Device(SPACEMOUSE),
|
||||
Seen::Report(SPACEMOUSE, PUSH_X.to_vec(), true),
|
||||
Seen::Report(SPACEMOUSE, AT_REST.to_vec(), false),
|
||||
Seen::Report(SPACEMOUSE, TILT.to_vec(), true),
|
||||
// The receiver is unplugged first, then the SpaceMouse, the one being read.
|
||||
Seen::Gone,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// Two devices in use at once don't trade the reading: the other takes over only after the
|
||||
/// followed one has shown no use for `STALE_AFTER`. A report at rest or with an id the layout
|
||||
/// doesn't know shows no use, and only the followed device's reports are published.
|
||||
#[test]
|
||||
fn another_device_takes_over_only_once_the_followed_one_is_still() {
|
||||
let unknown_id: &[u8] = &[0x17, 80];
|
||||
let first: &[(u64, &[u8])] = &[(0, PUSH_X), (100, AT_REST), (200, &[])];
|
||||
let second: &[(u64, &[u8])] = &[(50, PUSH_X), (150, unknown_id), (250, TILT)];
|
||||
assert_eq!(
|
||||
follow_simulated(&[(0xc635, first), (SPACEMOUSE, second)]),
|
||||
[
|
||||
Seen::Device(0xc635),
|
||||
Seen::Report(0xc635, PUSH_X.to_vec(), true),
|
||||
// The second moves 50 ms after the first did: not yet. The first's own rest
|
||||
// report is read, and the second's unknown report is not.
|
||||
Seen::Report(0xc635, AT_REST.to_vec(), false),
|
||||
// 250 ms after the first last moved, the second takes over.
|
||||
Seen::Device(SPACEMOUSE),
|
||||
Seen::Report(SPACEMOUSE, TILT.to_vec(), true),
|
||||
Seen::Gone,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// One device is read as before: every report, until it fails. When the followed one of
|
||||
/// several fails, the reading is reset and the next one is read.
|
||||
#[test]
|
||||
fn a_device_that_fails_hands_the_reading_to_the_next() {
|
||||
let only: &[(u64, &[u8])] = &[(0, &[]), (1, AT_REST), (2, &[0x17, 80])];
|
||||
assert_eq!(
|
||||
follow_simulated(&[(SPACEMOUSE, only)]),
|
||||
[
|
||||
Seen::Device(SPACEMOUSE),
|
||||
Seen::Report(SPACEMOUSE, AT_REST.to_vec(), false),
|
||||
Seen::Report(SPACEMOUSE, vec![0x17, 80], false),
|
||||
Seen::Gone
|
||||
]
|
||||
);
|
||||
let unplugged: &[(u64, &[u8])] = &[];
|
||||
let next: &[(u64, &[u8])] = &[(1, PUSH_X)];
|
||||
assert_eq!(
|
||||
follow_simulated(&[(SPACEMOUSE, unplugged), (RECEIVER, next)]),
|
||||
[Seen::Device(SPACEMOUSE), Seen::Gone, Seen::Device(RECEIVER), Seen::Report(RECEIVER, PUSH_X.to_vec(), true), Seen::Gone]
|
||||
);
|
||||
assert_eq!(follow_simulated(&[]), []);
|
||||
}
|
||||
|
||||
/// A button press that makes a device the one in use is a press. The buttons it reported while
|
||||
/// another was read are its baseline; the reader keeps them, so the press isn't taken for a
|
||||
/// button already held, as a device's first button report is.
|
||||
#[test]
|
||||
fn a_press_that_takes_over_counts_as_a_press() {
|
||||
let mut layout = Layout::default();
|
||||
layout.buttons[0] =
|
||||
Some(solidcraft_space_mouse::Field { report_id: 3, bit_offset: 0, bits: 1, logical_min: 0, logical_max: 1, relative: false });
|
||||
let (none_down, first_down): (&[u8], &[u8]) = (&[3, 0], &[3, 1]);
|
||||
let receiver: &[(u64, &[u8])] = &[(0, &[]), (10, &[]), (20, &[])];
|
||||
let spacemouse: &[(u64, &[u8])] = &[(5, none_down), (15, first_down), (25, &[])];
|
||||
assert_eq!(
|
||||
follow_simulated_with(layout, &[(RECEIVER, receiver), (SPACEMOUSE, spacemouse)]),
|
||||
[Seen::Device(RECEIVER), Seen::Device(SPACEMOUSE), Seen::Baseline(0), Seen::Report(SPACEMOUSE, first_down.to_vec(), true), Seen::Gone,]
|
||||
);
|
||||
// What the reader does with that: the press after the baseline is a tap.
|
||||
let mut reading = Reading { buttons_known: false, ..Reading::new(Instant::now()) };
|
||||
reading.set_buttons(0);
|
||||
assert!(decode_report(first_down, layout, &mut reading, Instant::now()));
|
||||
assert_eq!((reading.buttons, reading.taps), (1, 1));
|
||||
// Without one, the same report is the baseline: held, not pressed.
|
||||
let mut unseen = Reading { buttons_known: false, ..Reading::new(Instant::now()) };
|
||||
assert!(decode_report(first_down, layout, &mut unseen, Instant::now()));
|
||||
assert_eq!((unseen.buttons, unseen.taps), (1, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_held_reading_stays_until_the_next_one_and_a_hid_reading_goes_stale() {
|
||||
let now = Instant::now();
|
||||
|
||||
@@ -8,9 +8,8 @@
|
||||
//! row the next launch starts in safe mode by itself.
|
||||
//! - **Safe mode** (`--safe-mode` or `SOLIDCRAFT_SAFE_MODE=1`): default settings (the saved ones
|
||||
//! are left untouched), no recovery offer, the software renderer and CPU meshing.
|
||||
//! - **A renderer that cannot start**: on Windows, try the remaining graphics APIs in order,
|
||||
//! then the software renderer (`SOLIDCRAFT_RENDERER=cpu`). An explicit `WGPU_BACKEND` fails
|
||||
//! cleanly without switching. Exhausted retries show a message box with the error.
|
||||
//! - **No usable graphics device**: one retry on the software renderer
|
||||
//! (`SOLIDCRAFT_RENDERER=cpu`), then a message box saying what failed and what to try.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
@@ -22,25 +21,6 @@ pub const RENDERER_ENV: &str = "SOLIDCRAFT_RENDERER";
|
||||
pub const FAILED_STARTS_FOR_SAFE_MODE: u32 = 2;
|
||||
const MARKER: &str = "starting";
|
||||
|
||||
/// Explicit application-settings isolation for side-by-side builds and native UI
|
||||
/// acceptance. A supplied invalid path refuses startup rather than falling back
|
||||
/// to the user's ordinary settings. No directory is read or created here.
|
||||
pub fn settings_directory(override_path: Option<&std::ffi::OsStr>, default: Option<PathBuf>) -> Result<Option<PathBuf>, String> {
|
||||
match override_path {
|
||||
None => Ok(default),
|
||||
Some(raw) => {
|
||||
let path = PathBuf::from(raw);
|
||||
if raw.is_empty() || !path.is_absolute() {
|
||||
return Err("SOLIDCRAFT_SETTINGS_DIR must be a nonempty absolute directory path".into());
|
||||
}
|
||||
if path.exists() && !path.is_dir() {
|
||||
return Err("SOLIDCRAFT_SETTINGS_DIR names a file, not a directory".into());
|
||||
}
|
||||
Ok(Some(path))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether an environment value switches something on.
|
||||
pub fn env_on(v: Option<&str>) -> bool {
|
||||
v.map(str::trim).is_some_and(|v| !v.is_empty() && v != "0" && !v.eq_ignore_ascii_case("false"))
|
||||
@@ -130,48 +110,6 @@ pub fn pick_cpu_adapter(adapters: &[eframe::wgpu::Adapter], surface: Option<&efr
|
||||
})
|
||||
}
|
||||
|
||||
/// Adapter properties used to choose a card without opening a device.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AdapterChoice {
|
||||
pub name: String,
|
||||
pub compatible: bool,
|
||||
}
|
||||
|
||||
/// Choose only surface-compatible adapters. Names saved in preferences are exact; the
|
||||
/// environment override follows wgpu's case-insensitive substring convention.
|
||||
/// No match leaves selection to the normal wgpu request.
|
||||
pub fn choose_adapter(adapters: &[AdapterChoice], environment: Option<&str>, preference: Option<&str>) -> Option<usize> {
|
||||
// wgpu's environment helper panics on a missing name; an override must remain recoverable.
|
||||
let compatible = || adapters.iter().enumerate().filter(|(_, a)| a.compatible);
|
||||
if let Some(name) = environment.filter(|s| !s.is_empty()) {
|
||||
let name = name.to_lowercase();
|
||||
if let Some((index, _)) = compatible().find(|(_, a)| a.name.to_lowercase().contains(&name)) {
|
||||
return Some(index);
|
||||
}
|
||||
}
|
||||
if let Some(name) = preference.filter(|s| !s.is_empty())
|
||||
&& let Some((index, _)) = compatible().find(|(_, a)| a.name == name)
|
||||
{
|
||||
return Some(index);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Leave Automatic on wgpu's normal request path; named choices may defer to it too.
|
||||
pub fn configure_adapter(setup: &mut eframe::egui_wgpu::WgpuSetupCreateNew, cpu: bool, environment: Option<String>, preference: Option<String>) {
|
||||
if cpu {
|
||||
setup.native_adapter_selector = Some(std::sync::Arc::new(|adapters, surface| pick_cpu_adapter(adapters, surface).map(Some)));
|
||||
} else if environment.as_deref().is_some_and(|s| !s.is_empty()) || preference.as_deref().is_some_and(|s| !s.is_empty()) {
|
||||
setup.native_adapter_selector = Some(std::sync::Arc::new(move |adapters, surface| {
|
||||
let choices: Vec<_> = adapters
|
||||
.iter()
|
||||
.map(|a| AdapterChoice { name: a.get_info().name, compatible: surface.is_none_or(|s| a.is_surface_supported(s)) })
|
||||
.collect();
|
||||
Ok(choose_adapter(&choices, environment.as_deref(), preference.as_deref()).and_then(|i| adapters.get(i)).cloned())
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/// `WGPU_BACKEND`: when set, wgpu picks the backends from it and the order below is not used.
|
||||
pub const WGPU_BACKEND_ENV: &str = "WGPU_BACKEND";
|
||||
|
||||
@@ -219,34 +157,6 @@ pub fn choose_backends(
|
||||
Some(WINDOWS_ORDER.into_iter().find(|b| has_hardware(*b)).unwrap_or(Backends::DX12))
|
||||
}
|
||||
|
||||
/// Internal, process-local retry choice; never written to the graphics preference.
|
||||
pub const RETRY_BACKEND_ENV: &str = "SOLIDCRAFT_STARTUP_BACKEND";
|
||||
/// Why this launch is using a fallback, carried to the new process for status and Preferences.
|
||||
pub const FALLBACK_NOTE_ENV: &str = "SOLIDCRAFT_GRAPHICS_FALLBACK";
|
||||
|
||||
/// A handled startup failure tries each remaining Windows API once, then software rendering.
|
||||
/// An explicit environment choice is final, including when software rendering was requested.
|
||||
pub fn fallback_backend(windows: bool, explicit: bool, cpu: bool, failed: eframe::wgpu::Backends) -> Option<&'static str> {
|
||||
if explicit || cpu {
|
||||
return None;
|
||||
}
|
||||
if windows {
|
||||
let last = WINDOWS_ORDER.iter().rposition(|backend| failed.intersects(*backend));
|
||||
if let Some(next) = last.and_then(|i| WINDOWS_ORDER.get(i + 1)) {
|
||||
return Some(if *next == eframe::wgpu::Backends::VULKAN { "vulkan" } else { "gl" });
|
||||
}
|
||||
}
|
||||
Some("cpu")
|
||||
}
|
||||
|
||||
/// Keep driver diagnostics on one bounded line, including across the finite retry chain.
|
||||
pub fn fallback_note(previous: &str, failed: eframe::wgpu::Backends, next: &str, error: &str) -> String {
|
||||
let previous: String = previous.chars().take(2048).collect();
|
||||
let error: String = error.chars().take(1024).collect();
|
||||
let line = format!("{previous} Graphics fallback: {failed:?} failed ({error}); trying {next}.");
|
||||
line.split_whitespace().collect::<Vec<_>>().join(" ").chars().take(4096).collect()
|
||||
}
|
||||
|
||||
/// The device feature for a 32-bit float depth buffer with a stencil (reversed depth, #1434).
|
||||
pub const FLOAT_DEPTH_STENCIL: eframe::wgpu::Features = eframe::wgpu::Features::DEPTH32FLOAT_STENCIL8;
|
||||
|
||||
@@ -279,29 +189,6 @@ pub fn has_hardware_adapter(backends: eframe::wgpu::Backends) -> bool {
|
||||
adapters.iter().any(|a| a.get_info().device_type != eframe::wgpu::DeviceType::Cpu)
|
||||
}
|
||||
|
||||
/// The environment variable that switches wgpu's indirect-call validation on or off.
|
||||
pub const INDIRECT_VALIDATION_ENV: &str = "WGPU_VALIDATION_INDIRECT_CALL";
|
||||
|
||||
/// The instance flags for the window. wgpu's indirect-call validation compiles a compute shader
|
||||
/// when the device is created, and on Intel Macs (macOS 12 with Intel Iris graphics, #3024,
|
||||
/// #3080, #4605) the Metal compiler refuses it: the device is lost and the window never opens.
|
||||
/// So on an Intel Mac the validation is left off, unless the user turned it on or off with
|
||||
/// `WGPU_VALIDATION_INDIRECT_CALL` themselves.
|
||||
pub fn window_instance_flags(flags: eframe::wgpu::InstanceFlags, intel_mac: bool, env: Option<&str>) -> eframe::wgpu::InstanceFlags {
|
||||
if intel_mac && env.map(str::trim).is_none_or(str::is_empty) { flags - eframe::wgpu::InstanceFlags::VALIDATION_INDIRECT_CALL } else { flags }
|
||||
}
|
||||
|
||||
/// Windows with OpenGL: the window keeps the system title bar (#2063). The window without OS
|
||||
/// decorations keeps its caption style while having no frame, and some OpenGL drivers (older
|
||||
/// Intel HD/UHD) then show the picture a caption's height lower than the window, so it no longer
|
||||
/// lines up with the pointer. Direct3D 12 and Vulkan draw it in place and keep the application bar
|
||||
/// as the title bar. `backends` are the ones wgpu starts: OpenGL alone is decided before the
|
||||
/// window opens.
|
||||
pub fn system_title_bar(windows: bool, backends: eframe::wgpu::Backends) -> bool {
|
||||
use eframe::wgpu::Backends;
|
||||
windows && backends.contains(Backends::GL) && !backends.intersects(Backends::DX12 | Backends::VULKAN)
|
||||
}
|
||||
|
||||
/// The message shown when no window could be opened at all.
|
||||
pub fn no_window_message(error: &str, settings: Option<&Path>) -> String {
|
||||
let folder = settings.map(|d| d.display().to_string()).unwrap_or_else(|| "the SolidCraft settings folder".into());
|
||||
@@ -320,48 +207,6 @@ pub fn no_window_message(error: &str, settings: Option<&Path>) -> String {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn cards() -> Vec<AdapterChoice> {
|
||||
["Integrated", "Discrete small", "Discrete large"].into_iter().map(|name| AdapterChoice { name: name.into(), compatible: true }).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adapter_names_environment_override_and_missing_card_fallback() {
|
||||
let cards = cards();
|
||||
assert_eq!(choose_adapter(&cards, None, Some("Discrete small")), Some(1));
|
||||
assert_eq!(choose_adapter(&cards, Some("LARGE"), Some("Discrete small")), Some(2));
|
||||
assert_eq!(choose_adapter(&cards, None, Some("removed")), None);
|
||||
assert_eq!(choose_adapter(&cards, Some("missing"), Some("Discrete small")), Some(1));
|
||||
assert_eq!(choose_adapter(&cards, Some(""), Some("Discrete small")), Some(1));
|
||||
assert_eq!(choose_adapter(&cards, None, Some("Discrete")), None, "saved names are exact");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn automatic_adapter_leaves_selection_to_wgpu() {
|
||||
let mut cards = cards();
|
||||
assert_eq!(choose_adapter(&cards, None, None), None);
|
||||
assert_eq!(choose_adapter(&cards, Some(""), Some("")), None);
|
||||
cards[2].compatible = false;
|
||||
assert_eq!(choose_adapter(&cards, Some("large"), Some("Discrete large")), None);
|
||||
assert_eq!(choose_adapter(&cards, Some("small"), None), Some(1));
|
||||
assert_eq!(choose_adapter(&[], None, None), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn automatic_adapter_preserves_the_previous_request_configuration() {
|
||||
use eframe::egui_wgpu::WgpuSetupCreateNew;
|
||||
let previous = WgpuSetupCreateNew::without_display_handle();
|
||||
for preference in [None, Some(String::new())] {
|
||||
let mut setup = WgpuSetupCreateNew::without_display_handle();
|
||||
configure_adapter(&mut setup, false, None, preference);
|
||||
assert!(setup.native_adapter_selector.is_none(), "Automatic must use the normal wgpu request");
|
||||
assert_eq!(setup.power_preference, previous.power_preference);
|
||||
assert_eq!(setup.instance_descriptor.backends, previous.instance_descriptor.backends);
|
||||
}
|
||||
let mut setup = WgpuSetupCreateNew::without_display_handle();
|
||||
configure_adapter(&mut setup, false, Some("missing".into()), Some("removed".into()));
|
||||
assert!(setup.native_adapter_selector.unwrap()(&[], None).unwrap().is_none(), "missing names must use the normal request too");
|
||||
}
|
||||
|
||||
fn scratch(name: &str) -> PathBuf {
|
||||
let d = std::env::temp_dir().join(format!("solidcraft-startup-{name}-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&d);
|
||||
@@ -369,32 +214,6 @@ mod tests {
|
||||
d
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn isolated_settings_are_explicit_absolute_and_never_fall_back() {
|
||||
let default = std::env::temp_dir().join("ordinary-settings");
|
||||
assert_eq!(settings_directory(None, Some(default.clone())).unwrap(), Some(default.clone()));
|
||||
let selected = std::env::temp_dir().join("solidcraft-isolated-settings-fixture");
|
||||
assert_eq!(settings_directory(Some(selected.as_os_str()), Some(default.clone())).unwrap(), Some(selected));
|
||||
for raw in ["", "relative/settings", "../ordinary"] {
|
||||
assert!(settings_directory(Some(std::ffi::OsStr::new(raw)), Some(default.clone())).is_err());
|
||||
}
|
||||
let file = std::env::current_exe().unwrap();
|
||||
assert!(settings_directory(Some(file.as_os_str()), Some(default)).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intel_macs_start_without_indirect_call_validation() {
|
||||
use eframe::wgpu::InstanceFlags as F;
|
||||
let on = F::VALIDATION | F::VALIDATION_INDIRECT_CALL;
|
||||
assert_eq!(window_instance_flags(on, true, None), F::VALIDATION);
|
||||
assert_eq!(window_instance_flags(on, true, Some(" ")), F::VALIDATION);
|
||||
// The user's own choice stands, on or off.
|
||||
assert_eq!(window_instance_flags(on, true, Some("1")), on);
|
||||
assert_eq!(window_instance_flags(F::VALIDATION, true, Some("0")), F::VALIDATION);
|
||||
// Every other machine keeps what it had.
|
||||
assert_eq!(window_instance_flags(on, false, None), on);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn switches_read_from_the_environment() {
|
||||
assert!(env_on(Some("1")) && env_on(Some("yes")) && !env_on(Some("0")) && !env_on(Some(" ")) && !env_on(None) && !env_on(Some("false")));
|
||||
@@ -471,54 +290,6 @@ mod tests {
|
||||
assert_eq!(choose_backends(true, false, false, Some(""), probe(&[Backends::GL])), Some(Backends::GL));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_windows_backends_fall_back_in_order_and_stop_after_software() {
|
||||
use eframe::wgpu::Backends as B;
|
||||
assert_eq!(fallback_backend(true, false, false, B::DX12), Some("vulkan"));
|
||||
assert_eq!(fallback_backend(true, false, false, B::VULKAN), Some("gl"));
|
||||
assert_eq!(fallback_backend(true, false, false, B::GL), Some("cpu"));
|
||||
assert_eq!(fallback_backend(true, false, true, B::DX12), None);
|
||||
assert_eq!(fallback_backend(true, false, false, B::DX12 | B::VULKAN), Some("gl"));
|
||||
assert_eq!(fallback_backend(false, false, false, B::VULKAN), Some("cpu"));
|
||||
let note = fallback_note("", B::DX12, "vulkan", "egui_pipeline\n0x80070057");
|
||||
assert!(note.contains("egui_pipeline 0x80070057") && note.contains("vulkan") && !note.contains('\n'));
|
||||
let long = fallback_note(&"x".repeat(5000), B::VULKAN, "gl", &"y".repeat(5000));
|
||||
assert!(long.chars().count() <= 4096 && long.ends_with("trying gl."));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_backend_failure_never_switches_api_or_retries_software() {
|
||||
use eframe::wgpu::Backends as B;
|
||||
for windows in [false, true] {
|
||||
for cpu in [false, true] {
|
||||
for backend in [B::DX12, B::VULKAN, B::GL, B::all()] {
|
||||
assert_eq!(fallback_backend(windows, true, cpu, backend), None);
|
||||
let mut probed = false;
|
||||
assert_eq!(
|
||||
choose_backends(windows, true, cpu, Some("vulkan"), |_| {
|
||||
probed = true;
|
||||
false
|
||||
}),
|
||||
None
|
||||
);
|
||||
assert!(!probed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_opengl_on_windows_gets_the_system_title_bar() {
|
||||
use eframe::wgpu::Backends;
|
||||
assert!(system_title_bar(true, Backends::GL));
|
||||
for b in [Backends::DX12, Backends::VULKAN, Backends::DX12 | Backends::GL, Backends::VULKAN | Backends::GL, Backends::all()] {
|
||||
assert!(!system_title_bar(true, b), "{b:?}");
|
||||
}
|
||||
for b in [Backends::GL, Backends::VULKAN, Backends::METAL] {
|
||||
assert!(!system_title_bar(false, b), "{b:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wgpu_backend_the_preference_and_the_software_renderer_override_the_order() {
|
||||
use eframe::wgpu::Backends;
|
||||
|
||||
@@ -25,7 +25,6 @@ assets/
|
||||
fonts/{liberation-sans,biz-udp-gothic}/ # unmodified fonts and OFL licences
|
||||
locales/{en,de,ja,ko}.json # shared offline catalogs
|
||||
platforms/{macos,linux,windows}/ # bundle, freedesktop and WiX templates
|
||||
platforms/macos/dmg/ # disk image window: layout, dmgbuild settings, background art
|
||||
archive/icons/macos-v1/
|
||||
source/icon.vectorcraft # earlier editable work, retained
|
||||
# redundant historical exports are retired
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
//! Original illustrative opaque finishes; not measured optical or engineering data.
|
||||
//! (name, sRGB color, metal fraction, perceptual roughness). IOR is an illustrative 1.5.
|
||||
//! Separate names preserve the scalar library's appearance and saved assignments.
|
||||
pub const OPAQUE_FINISHES: [(&str, [u8; 3], f64, f64); 6] = [
|
||||
("Steel - Satin PBR", [158, 164, 172], 1.0, 0.4),
|
||||
("Aluminum - Satin PBR", [204, 208, 214], 1.0, 0.3),
|
||||
("Copper - Clean PBR", [204, 122, 80], 1.0, 0.25),
|
||||
("ABS - Molded PBR", [222, 222, 216], 0.0, 0.4),
|
||||
("PLA - Smooth PBR", [110, 160, 222], 0.0, 0.35),
|
||||
("Wood - Oak Uniform PBR Preview", [190, 146, 92], 0.0, 0.65),
|
||||
];
|
||||
@@ -48,38 +48,6 @@ source = "#236ead"
|
||||
light = "#236ead"
|
||||
dark = "#2976b8"
|
||||
|
||||
# Simultaneous component colours for inspection icons; existing workspace ramps stay unchanged.
|
||||
|
||||
[roles.component-orange-1]
|
||||
source = "#f1d2af"
|
||||
light = "#f1d2af"
|
||||
dark = "#ba8f6a"
|
||||
|
||||
[roles.component-orange-2]
|
||||
source = "#c06f00"
|
||||
light = "#c06f00"
|
||||
dark = "#b86f04"
|
||||
|
||||
[roles.component-orange-3]
|
||||
source = "#a45000"
|
||||
light = "#a45000"
|
||||
dark = "#95682d"
|
||||
|
||||
[roles.component-yellow-1]
|
||||
source = "#e5d99b"
|
||||
light = "#e5d99b"
|
||||
dark = "#a9985f"
|
||||
|
||||
[roles.component-yellow-2]
|
||||
source = "#a28200"
|
||||
light = "#a28200"
|
||||
dark = "#9a8100"
|
||||
|
||||
[roles.component-yellow-3]
|
||||
source = "#826700"
|
||||
light = "#826700"
|
||||
dark = "#857100"
|
||||
|
||||
[strokes] # on the 64 grid
|
||||
geometry = 2.55
|
||||
action = 4.25
|
||||
|
||||
@@ -57,13 +57,10 @@ circle_tt false
|
||||
circle_ttt false
|
||||
close false
|
||||
coil false
|
||||
colored_components false
|
||||
colored_components_off false
|
||||
combine false
|
||||
compare false
|
||||
component false
|
||||
compute false
|
||||
cone false
|
||||
configuration_table false
|
||||
conic false
|
||||
constrainer false
|
||||
@@ -151,7 +148,6 @@ origin false
|
||||
pan false
|
||||
params false
|
||||
paste false
|
||||
path_flange false
|
||||
pattern_circ false
|
||||
pattern_path false
|
||||
pattern_rect false
|
||||
@@ -248,7 +244,6 @@ status_error false
|
||||
status_recovery false
|
||||
style false
|
||||
suppress false
|
||||
surface false
|
||||
surface_blend false
|
||||
surface_boundary false
|
||||
surface_extend false
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Colored components</title>
|
||||
<path d="M9.235 17.706L15.059 20.882L15.059 48.941L9.235 44.926Z" fill="#c06f00"/>
|
||||
<path d="M35.176 9.235L15.059 20.882L15.059 48.941L36.765 35.139Z" fill="#a45000"/>
|
||||
<path d="M29.046 5L35.176 8.176L15.196 19.718L9.066 16.541Z" fill="#f1d2af"/>
|
||||
<path d="M29.046 5.529L35.176 8.706L35.176 36.235L15.059 49.471L9.235 45.765L9.066 17.071Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.066 17.6L15.059 20.882L34.647 9.235" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M15.059 20.882L15.059 47.067" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M32.222 18.595L44.949 25.944L44.949 51.122L32.222 43.774Z" fill="#328bd3"/>
|
||||
<path d="M54.934 20.173L44.949 25.944L44.949 51.122L54.934 45.352Z" fill="#236ead"/>
|
||||
<path d="M42.059 12.412L54.934 20.173L44.949 25.944L32 17.706Z" fill="#b9ddf8"/>
|
||||
<path d="M41.159 11.766L54.934 20.173L54.934 45.352L44.949 51.122L31.164 42.715L31.164 17.536Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M32.222 18.595L44.949 25.944L54.934 20.173" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M44.949 25.944L44.949 51.122" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M18.299 39.062L31.015 46.411L31.015 59L18.299 51.652Z" fill="#a28200"/>
|
||||
<path d="M41.921 40.121L31.015 46.411L31.015 59L41.921 52.711Z" fill="#826700"/>
|
||||
<path d="M29.194 32.773L41.921 40.121L31.015 46.411L18.299 39.062Z" fill="#e5d99b"/>
|
||||
<path d="M29.194 32.773L41.921 40.121L41.921 52.711L31.015 59L18.299 51.652L18.299 39.062Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M18.299 39.062L31.015 46.411L41.921 40.121" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M31.015 46.411L31.015 59" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.3 KiB |
@@ -1,21 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Disable component colors</title>
|
||||
<path d="M9.235 17.706L15.059 20.882L15.059 48.941L9.235 44.926Z" fill="#328bd3"/>
|
||||
<path d="M35.176 9.235L15.059 20.882L15.059 48.941L36.765 35.139Z" fill="#236ead"/>
|
||||
<path d="M29.046 5L35.176 8.176L15.196 19.718L9.066 16.541Z" fill="#b9ddf8"/>
|
||||
<path d="M29.046 5.529L35.176 8.706L35.176 36.235L15.059 49.471L9.235 45.765L9.066 17.071Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.066 17.6L15.059 20.882L34.647 9.235" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M15.059 20.882L15.059 47.067" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M32.222 18.595L44.949 25.944L44.949 51.122L32.222 43.774Z" fill="#328bd3"/>
|
||||
<path d="M54.934 20.173L44.949 25.944L44.949 51.122L54.934 45.352Z" fill="#236ead"/>
|
||||
<path d="M42.059 12.412L54.934 20.173L44.949 25.944L32 17.706Z" fill="#b9ddf8"/>
|
||||
<path d="M41.159 11.766L54.934 20.173L54.934 45.352L44.949 51.122L31.164 42.715L31.164 17.536Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M32.222 18.595L44.949 25.944L54.934 20.173" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M44.949 25.944L44.949 51.122" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M18.299 39.062L31.015 46.411L31.015 59L18.299 51.652Z" fill="#328bd3"/>
|
||||
<path d="M41.921 40.121L31.015 46.411L31.015 59L41.921 52.711Z" fill="#236ead"/>
|
||||
<path d="M29.194 32.773L41.921 40.121L31.015 46.411L18.299 39.062Z" fill="#b9ddf8"/>
|
||||
<path d="M29.194 32.773L41.921 40.121L41.921 52.711L31.015 59L18.299 51.652L18.299 39.062Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M18.299 39.062L31.015 46.411L41.921 40.121" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M31.015 46.411L31.015 59" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.3 KiB |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Cone</title>
|
||||
<path d="M32 5L12.7 45.97A20 11.55 0 1 0 51.3 45.97Z" fill="#328bd3"/>
|
||||
<path d="M32 5L39 59.82A20 11.55 0 0 0 51.3 45.97Z" fill="#236ead"/>
|
||||
<path d="M32 5L12.7 45.97A20 11.55 0 1 0 51.3 45.97Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 388 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Path Flange</title>
|
||||
<path d="M9.5 44L23.5 44A6 6 0 0 0 29.5 38L29.5 26A6 6 0 0 1 35.5 20L43.5 20A6 6 0 0 1 47.74 21.76L54.81 28.83" fill="none" stroke="#34465b" stroke-width="11.4" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M9.5 44L23.5 44" fill="none" stroke="#b8c3cf" stroke-width="6.3" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M23.5 44A6 6 0 0 0 29.5 38L29.5 26A6 6 0 0 1 35.5 20L43.5 20A6 6 0 0 1 47.74 21.76L54.81 28.83" fill="none" stroke="#328bd3" stroke-width="6.3" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 638 B |
@@ -108,8 +108,8 @@ pub(crate) fn glyph_icon(name: &str) -> Option<&'static str> {
|
||||
}
|
||||
pub(crate) fn glyph_letter(name: &str) -> &'static str {
|
||||
match name {
|
||||
"Horizontal" => "-",
|
||||
"Vertical" => "|",
|
||||
"Horizontal" => "H",
|
||||
"Vertical" => "V",
|
||||
_ => "?",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,11 +152,11 @@ pub(crate) fn curved_arrow(painter: &egui::Painter, at: Pos2, clockwise: bool, h
|
||||
pub(crate) const AXIS_RADIUS: f32 = 15.0;
|
||||
pub(crate) fn axis_tip(painter: &egui::Painter, c: Pos2, name: &str, d: &Vec3, at: Pos2, depth: f64, hovered: bool, t: Tokens) {
|
||||
let col = if d.x != 0.0 {
|
||||
t.axis_x
|
||||
t.triad[0]
|
||||
} else if d.y != 0.0 {
|
||||
t.axis_y
|
||||
t.triad[1]
|
||||
} else {
|
||||
t.axis_z
|
||||
t.triad[2]
|
||||
};
|
||||
let positive = !name.starts_with('-');
|
||||
let light = t.panel.r() > 128;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+7
-194
@@ -72,7 +72,6 @@
|
||||
"Choose…": "Vybrat…",
|
||||
"Circular": "Kruhově",
|
||||
"Circumference": "Obvod",
|
||||
"Clockwise": "Ve směru hodinových ručiček",
|
||||
"Close draft": "Zavřít koncept",
|
||||
"Collisions": "Kolize",
|
||||
"Colour": "Barva",
|
||||
@@ -270,7 +269,7 @@
|
||||
"Preview Row": "Náhled řádku",
|
||||
"Preview leaves the current design unchanged.": "Náhled aktuální návrh nemění.",
|
||||
"Profiles/Curves": "Profily/křivky",
|
||||
"Quantity": "Počet",
|
||||
"Quantity": "Množství",
|
||||
"RECENT": "NEDÁVNÉ",
|
||||
"REFERENCE": "REFERENCE",
|
||||
"RULED SURFACE": "PŘÍMKOVÁ PLOCHA",
|
||||
@@ -323,7 +322,6 @@
|
||||
"Search commands": "Hledat příkazy",
|
||||
"Search this library": "Prohledat tuto knihovnu",
|
||||
"Section": "Řez",
|
||||
"Section position": "Poloha průřezu",
|
||||
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Vybrat jiné: podržte levé tlačítko na modelu (nebo pravé tlačítko › Vybrat jiné…) a zobrazí se vše pod kurzorem; dalším kliknutím na stejné místo vyberete další prvek. Dvojklikem prostředním tlačítkem přejdete na výchozí pohled (Předvolby › Navigace).",
|
||||
"Select bodies (a face picks its body)": "Vyberte tělesa (plocha vybere své těleso)",
|
||||
"Select bodies or faces instead": "Vyberte místo toho tělesa nebo plochy",
|
||||
@@ -335,7 +333,6 @@
|
||||
"Show Agent Cursor": "Zobrazit kurzor agenta",
|
||||
"Show in folder": "Zobrazit ve složce",
|
||||
"Show the pointer an AI agent moves (#35)": "Zobrazit ukazatel, kterým pohybuje AI agent (#35)",
|
||||
"Show tool at cursor": "Zobrazit nástroj u kurzoru",
|
||||
"Signed axes": "Osy se znaménkem",
|
||||
"Size": "Velikost",
|
||||
"Sketch constraints": "Vazby náčrtu",
|
||||
@@ -348,11 +345,9 @@
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft se zavřel dříve, než se tyto návrhy uložily. Lze otevřít jejich poslední automatické uložení.",
|
||||
"Specialise": "Specializovat",
|
||||
"Speed": "Rychlost",
|
||||
"Square": "Čtverec",
|
||||
"Stand it up": "Postavit",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Postavit: otočení o čtvrt otáčky kolem osy X jako jeden prvek Posunutí, který lze vrátit.",
|
||||
"Stand this part up?": "Postavit tento díl?",
|
||||
"Start angle": "Počáteční úhel",
|
||||
"Start bulge": "Vypouklost začátku",
|
||||
"Start continuity": "Návaznost začátku",
|
||||
"Step": "Krok",
|
||||
@@ -417,7 +412,6 @@
|
||||
"angle, height": "úhel, výška",
|
||||
"appearance.dielectric_ior": "Index lomu dielektrika",
|
||||
"appearance.metallic": "Kovovost",
|
||||
"appearance.only_available": "Pouze dostupné vzhledy",
|
||||
"appearance.pbr_help": "Tento povrch zobrazíte v renderu nebo v produktovém PNG. Bez lomu a průchodu světla.",
|
||||
"appearance.pbr_opaque": "Neprůhledné PBR",
|
||||
"appearance.roughness": "Drsnost",
|
||||
@@ -457,7 +451,6 @@
|
||||
"cmd.canvas_delete": "Smazat plátno",
|
||||
"cmd.canvas_edit": "Upravit plátno",
|
||||
"cmd.canvas_insert": "Plátno",
|
||||
"cmd.canvas_to_sketch": "Náčrt z plátna",
|
||||
"cmd.component_activate": "Aktivovat komponentu",
|
||||
"cmd.component_break_link": "Zrušit odkaz",
|
||||
"cmd.component_capture_position": "Zachytit polohu",
|
||||
@@ -535,7 +528,6 @@
|
||||
"cmd.file_export": "Exportovat",
|
||||
"cmd.file_insert_mesh": "Vložit síť",
|
||||
"cmd.file_insert_step": "Vložit STEP",
|
||||
"cmd.file_open_in_easyprint": "Otevřít v EasyPrint",
|
||||
"cmd.file_save_mesh": "Uložit jako síť",
|
||||
"cmd.file_slicers": "Nainstalované slicery",
|
||||
"cmd.frame_member": "Prvek rámu",
|
||||
@@ -581,7 +573,6 @@
|
||||
"cmd.material_create": "Nový fyzický materiál",
|
||||
"cmd.material_delete": "Smazat fyzický materiál",
|
||||
"cmd.material_edit": "Upravit fyzický materiál",
|
||||
"cmd.material_inspect": "Zkontrolovat fyzický materiál",
|
||||
"cmd.material_list": "Seznam materiálů",
|
||||
"cmd.material_properties_directional": "Směrový modul pružnosti",
|
||||
"cmd.material_properties_validate": "Ověřit data fyzického materiálu",
|
||||
@@ -628,8 +619,6 @@
|
||||
"cmd.plastic_rules": "Seznam pravidel pro plasty",
|
||||
"cmd.plastic_snap_fit": "Zacvakávací spoj",
|
||||
"cmd.render_backends": "Renderovací jádra",
|
||||
"cmd.render_camera_fit": "Zarámovat objekt renderu",
|
||||
"cmd.render_camera_get": "Prohlédnout kameru renderu",
|
||||
"cmd.render_cancel": "Zrušit render",
|
||||
"cmd.render_export": "Exportovat obrázek renderu",
|
||||
"cmd.render_preflight": "Zkontrolovat požadavek na render",
|
||||
@@ -637,7 +626,6 @@
|
||||
"cmd.render_settings_apply": "Použít kameru renderu",
|
||||
"cmd.render_settings_get": "Nastavení renderu",
|
||||
"cmd.render_settings_set": "Uložit nastavení renderu",
|
||||
"cmd.render_stage_backdrop": "Uložit fyzické pozadí renderu",
|
||||
"cmd.render_start": "Renderovat obrázek",
|
||||
"cmd.render_status": "Průběh renderu",
|
||||
"cmd.select_clear": "Zrušit výběr",
|
||||
@@ -976,7 +964,6 @@
|
||||
"motion.key_here": "Vložit klíčový snímek sem",
|
||||
"motion.key_here_hint": "Přidat v tomto kroku klíčový snímek s aktuální hodnotou kloubu",
|
||||
"motion.no_collisions": "Během studie nedochází ke kolizím",
|
||||
"motion.plot_quantity": "Množství",
|
||||
"motion.step_back": "Krok zpět",
|
||||
"motion.step_forward": "Krok vpřed",
|
||||
"motion.to_end": "Na konec",
|
||||
@@ -1002,16 +989,6 @@
|
||||
"pattern.direction_1": "Směr 1",
|
||||
"pattern.direction_2": "Směr 2",
|
||||
"pattern.flip_direction": "Převrátit směr",
|
||||
"perf.click_for_details": "Klikněte pro podrobnosti: graf časů snímků, histogram a co vykreslila GPU",
|
||||
"perf.draw_calls": "Volání vykreslení",
|
||||
"perf.line_segments": "Úsečky",
|
||||
"perf.low_1": "1% minimum",
|
||||
"perf.renderer": "Vykreslování",
|
||||
"perf.scene_build": "Sestavení scény",
|
||||
"perf.triangles": "Trojúhelníky",
|
||||
"perf.ui_time": "Čas UI",
|
||||
"perf.viewport": "Pohled",
|
||||
"perf.worst": "nejhorší",
|
||||
"pick a hole to seat it, or OK places it at the origin": "vyberte otvor pro usazení, nebo OK umístí do počátku",
|
||||
"pick one or two vertices, edges or faces": "vyberte jeden nebo dva vrcholy, hrany či plochy",
|
||||
"prefs.cpu": "CPU (softwarově)",
|
||||
@@ -1024,43 +1001,15 @@
|
||||
"recovery.min_ago": "před {count} min",
|
||||
"recovery.never_saved": "nikdy neuloženo",
|
||||
"render.background": "Pozadí scény",
|
||||
"render.bloom_enable": "Záře ve výstupu",
|
||||
"render.bloom_help": "Kreativní záře z lineárních jasů se třemi normalizovanými průchody. Poloměr v pixelech výstupu, přičtení před sRGB. Nejde o simulaci čočky, objemu ani zlepšení konvergence. Surové EXR se nemění.",
|
||||
"render.bloom_opaque": "Záře vyžaduje neprůhledný výstup. Průhledné PNG se odmítne, neprovede se skryté sloučení.",
|
||||
"render.bloom_radius": "Poloměr záře",
|
||||
"render.bloom_strength": "Síla záře",
|
||||
"render.bloom_threshold": "Lineární práh jasů",
|
||||
"render.cpu_aa_help": "Prostorové vzorky: více vzorků zlepší hrany a prodlouží vykreslení.",
|
||||
"render.cpu_key_rim": "Hlavní a obrysové",
|
||||
"render.cpu_lighting": "Předvolba osvětlení",
|
||||
"render.cpu_neutral": "Neutrální",
|
||||
"render.cpu_opaque_required": "CPU trasování vyžaduje neprůhledné pozadí. Vypněte průhledné pozadí.",
|
||||
"render.cpu_options_pending": "Panel podporuje vzorky hran, předvolby světel, stíny a expozici. Další volby jsou v API.",
|
||||
"render.cpu_primary_samples": "Vzorky hran na pixel",
|
||||
"render.cpu_shadows": "Vrhat stíny",
|
||||
"render.cpu_summary": "Neprůhledný CPU PNG · světla · stíny · bez skla a textur",
|
||||
"render.cpu_warm_cool": "Teplé a studené",
|
||||
"render.exr_pending": "GPU může uchovat surové lineární EXR. Komprese, vícevrstvé obrazy a pomocné průchody zatím nejsou dostupné.",
|
||||
"render.fit": "Přizpůsobit produktu",
|
||||
"render.gpu_depth": "Maximální hloubka cesty",
|
||||
"render.gpu_options_help": "Tyto volby platí pro tuto úlohu. Expozice vychází z uloženého nastavení.",
|
||||
"render.gpu_path_failed": "Trasování cest GPU není dostupné",
|
||||
"render.gpu_path_first_setup": "První příprava na této GPU může trvat několik minut; SolidCraft lze mezitím používat. Při dalších spuštěních se použijí zkompilované shadery, pokud je ovladač uchová.",
|
||||
"render.gpu_path_preparing": "Trasování cest GPU: kompilace shaderů…",
|
||||
"render.gpu_seed": "Náhodné semeno",
|
||||
"render.gpu_unavailable": "Trasování cest GPU není na tomto zařízení připraveno.",
|
||||
"render.image_advanced": "Pokročilé nastavení GPU",
|
||||
"render.image_cancelled": "Render zrušen",
|
||||
"render.image_cancelling": "Rušení",
|
||||
"render.image_completed": "Obrázek připraven k exportu",
|
||||
"render.image_encoding": "Kódování PNG",
|
||||
"render.image_export": "Exportovat hotový obrázek",
|
||||
"render.image_exposure": "Expozice",
|
||||
"render.image_export": "Exportovat PNG",
|
||||
"render.image_failed": "Render selhal",
|
||||
"render.image_height": "Výška",
|
||||
"render.image_help": "Náhledový obrázek z CPU · PNG · přibližná průhlednost",
|
||||
"render.image_host_missing": "Není dostupný vykreslovací hostitel.",
|
||||
"render.image_method": "Metoda obrazu",
|
||||
"render.image_overwrite": "Nahradit existující soubor",
|
||||
"render.image_preparing": "Příprava obrázku",
|
||||
"render.image_progressive": "Progresivní render",
|
||||
@@ -1068,7 +1017,6 @@
|
||||
"render.image_reduce": "Zkuste menší obrázek nebo jednodušší scénu.",
|
||||
"render.image_running": "Renderování obrázku",
|
||||
"render.image_samples": "Vzorků na pixel",
|
||||
"render.image_scene_stale": "Před kontrolou nebo spuštěním dokončete modelování nebo obnovte scénu.",
|
||||
"render.image_stale": "Tento obrázek používá scénu zachycenou před pozdějšími změnami.",
|
||||
"render.image_start": "Spustit render",
|
||||
"render.image_supported": "Požadavek na render je podporován; fronta a paměť se znovu ověří při spuštění.",
|
||||
@@ -1078,135 +1026,18 @@
|
||||
"render.image_unknown": "Stav renderu není k dispozici",
|
||||
"render.image_use_largest": "Použít největší velikost v tomto poměru stran:",
|
||||
"render.image_width": "Šířka",
|
||||
"render.material_absorption": "Absorpce R · G · B (na mm)",
|
||||
"render.material_add": "Přidat nebo upravit těleso",
|
||||
"render.material_assignments": "Přiřazení těles",
|
||||
"render.material_binding_enable": "Zahrnout úpravy tohoto tělesa",
|
||||
"render.material_box": "Osově zarovnaný kvádr",
|
||||
"render.material_browse": "Vybrat obrázek…",
|
||||
"render.material_certification": "Uzavřené sklo používá geometrickou vzdálenost. GPU příprava ověřuje topologii, orientaci, překryvy a optická rozhraní; kontrola jen přijímá požadavek.",
|
||||
"render.material_choose_body": "Vybrat těleso scény",
|
||||
"render.material_closed": "Uzavřená objemová síť",
|
||||
"render.material_coat": "Čirý povlak na základním materiálu",
|
||||
"render.material_coat_enable": "Zapnout čirý povlak",
|
||||
"render.material_coat_help": "Ilustrativní dielektrický povlak na stávajícím neprůhledném vzhledu. Nulová váha ponechá základ beze změny.",
|
||||
"render.material_color_map": "Obraz základní barvy (sRGB)",
|
||||
"render.material_custom": "Vlastní ilustrativní optika",
|
||||
"render.material_enable": "Použít tyto úpravy materiálů GPU",
|
||||
"render.material_filter": "Filtrovat tělesa scény…",
|
||||
"render.material_filter_help": "Zobrazuje se nejvýše 128 shod. U velkých scén zpřesněte filtr.",
|
||||
"render.material_glass": "Sklo a optická absorpce",
|
||||
"render.material_glass_enable": "Použít fyzikální sklo na těleso",
|
||||
"render.material_glass_exclusive": "Tento renderer nepodporuje sklo s povlakem nebo mapami na témže tělese.",
|
||||
"render.material_glass_help": "Původní ilustrativní recepty. Vzorky ukazují propustnost, ne render. Absorpce je −ln(lineární propustnosti) / referenční vzdálenost; alfa se nemění.",
|
||||
"render.material_gpu_required": "Aktivní úpravy vyžadují GPU tracing. Před kontrolou nebo spuštěním vyberte GPU nebo úpravy vypněte.",
|
||||
"render.material_ior": "Index lomu (IOR)",
|
||||
"render.material_limits": "Nejvýše 128 nastavení těles, 32 oddělených skel, 64 povlaků a 128 vazeb textur. Shodné mapy sdílejí paměť; limity zařízení a celkové paměti zůstávají aktivní.",
|
||||
"render.material_map_invalid": "Cesty map musí být omezené místní cesty; síla normál musí být konečná mezi 0 a 1.",
|
||||
"render.material_map_required": "Před použitím map vyberte obraz barvy nebo normál.",
|
||||
"render.material_maps": "Rovinné mapy barvy a normál",
|
||||
"render.material_maps_enable": "Zapnout mapy neprůhledného tělesa",
|
||||
"render.material_maps_help": "PNG/JPEG do 1024×1024; celkem 16 MiB kódovaných dat. U a V jsou kolmé vektory opakování ve světových mm (nejvýše 128 opakování). Barva je sRGB, normály OpenGL +Y. Soubory se zachytí při kontrole/spuštění.",
|
||||
"render.material_normal_map": "Obraz normál (OpenGL, +Y)",
|
||||
"render.material_normal_strength": "Síla normál (0–1)",
|
||||
"render.material_origin": "Počátek mapování X · Y · Z (mm)",
|
||||
"render.material_refresh": "Obnovit tělesa scény",
|
||||
"render.material_refresh_needed": "Scéna se změnila. Před přidáním cíle obnovte seznam.",
|
||||
"render.material_remove": "Odstranit přiřazení",
|
||||
"render.material_repeat_u": "Jedno opakování U X · Y · Z (mm)",
|
||||
"render.material_repeat_v": "Jedno opakování V X · Y · Z (mm)",
|
||||
"render.material_revalidate": "Znovu ověřit těleso",
|
||||
"render.material_revalidate_needed": "Přiřazení patří ke starší revizi scény. Před renderem znovu ověřte přesnou identitu tělesa.",
|
||||
"render.material_roughness": "Drsnost povlaku (0,045–1)",
|
||||
"render.material_shadow": "Propouštět přímé stínové paprsky",
|
||||
"render.material_target_missing": "Pořadí a název tělesa již nesouhlasí. Odstraňte přiřazení a znovu výslovně vyberte těleso.",
|
||||
"render.material_thickness": "Tloušťka vrstvy (mm)",
|
||||
"render.material_thin": "Tenká vrstva",
|
||||
"render.material_title": "Materiály GPU obrazu",
|
||||
"render.material_transient": "Dočasné optické úpravy pro tuto kartu. Neukládají se do návrhu, nastavení renderu ani knihovny materiálů.",
|
||||
"render.material_weight": "Váha povlaku (0–1)",
|
||||
"render.method_cpu": "Trasování paprsků CPU",
|
||||
"render.method_gpu": "Trasování cest GPU",
|
||||
"render.method_raster": "CPU rastr",
|
||||
"render.navigation_help": "Kamera se ovládá stejně jako v návrhu (Předvolby › Navigace). Uložit nastavení zaznamená aktuální kameru.",
|
||||
"render.opaque_image_summary": "Náhledové PNG · neprůhledné PBR · smíšená průhlednost",
|
||||
"render.opaque_preview_help": "Neprůhledné PBR a jednoduché barvy; smíšená průhlednost. Lom světla, HDRI a finální render zatím nejsou k dispozici.",
|
||||
"render.output_format_status": "PNG je přesný dokončený obraz. Volitelné GPU EXR uchovává lineární FLOAT32 radianci před filtry. Export nikdy nenahrazuje výsledek náhledem nebo jinou úlohou.",
|
||||
"render.perspective": "Perspektivní kamera",
|
||||
"render.preview": "Integrovaný náhled",
|
||||
"render.preview_help": "Rastrové stínování a smíšená průhlednost. Lom světla, HDRI a finální render zatím nejsou k dispozici.",
|
||||
"render.progressive_image_summary": "Náhledové PNG · neprůhledné PBR · postupné vzorkování",
|
||||
"render.reload_settings": "Znovu načíst uložené nastavení",
|
||||
"render.restore_camera": "Obnovit uloženou kameru",
|
||||
"render.result_actual_help": "Jeden pixel obrázku na jeden pixel obrazovky.",
|
||||
"render.result_capture": "Zachycená revize",
|
||||
"render.result_current": "Zachycená revize modelu je aktuální.",
|
||||
"render.result_empty": "Zatím není hotový obrázek. Zkontrolujte nastavení a spusťte vykreslování.",
|
||||
"render.result_fit": "Přizpůsobit obrázek",
|
||||
"render.result_job": "Úloha",
|
||||
"render.result_navigation": "Posun a přiblížení používají nastavení navigace. Mezerník + tažení posouvá.",
|
||||
"render.result_open": "Otevřít hotový obrázek na hlavním plátně.",
|
||||
"render.result_preview": "Živý náhled",
|
||||
"render.result_retained": "Zobrazuje se poslední hotový obrázek; novější vykreslení jej zatím nenahradilo.",
|
||||
"render.result_title": "Výsledek vykreslení",
|
||||
"render.return_design": "Zpět k návrhu",
|
||||
"render.ribbon_backgrounds": "Pozadí",
|
||||
"render.ribbon_check": "Kontrola",
|
||||
"render.ribbon_check_help": "Ověřit přesný aktuální návrh renderu. Bez skrytého přepnutí vykreslovače, zmenšení nebo nahrazení zdrojů.",
|
||||
"render.ribbon_edit_lights": "Upravit světla",
|
||||
"render.ribbon_environment_help": "HDR osvětlení, pozadí kamery a fyzické plochy jsou samostatné ovládací prvky s potvrzením nahrazení.",
|
||||
"render.ribbon_finish_dialog": "Nejprve dokončete dialog souboru nebo potvrzení.",
|
||||
"render.ribbon_frame": "Zarámovat",
|
||||
"render.ribbon_hdr_library": "Knihovna HDR",
|
||||
"render.ribbon_job_active": "Render již běží; před spuštěním dalšího jej výslovně zrušte.",
|
||||
"render.ribbon_light_rigs": "Sestavy světel",
|
||||
"render.ribbon_lights_help": "Zkontrolovat světelné sestavy nebo upravit podporovaná světla; aktuální nastavení nahradí až výslovné použití.",
|
||||
"render.ribbon_materials": "Materiály",
|
||||
"render.ribbon_materials_help": "Otevřít inspektor vzhledu a původní povrchy. Optické úpravy nemění skrytě technické vlastnosti.",
|
||||
"render.ribbon_no_active_job": "Není aktivní render ke zrušení.",
|
||||
"render.ribbon_no_result": "Před exportem dokončete render.",
|
||||
"render.ribbon_no_saved_camera": "Před obnovením kamery uložte záběr.",
|
||||
"render.ribbon_saved_camera": "Uložená kamera",
|
||||
"render.ribbon_shots": "Záběry",
|
||||
"render.ribbon_studios": "Studia",
|
||||
"render.ribbon_studios_help": "Procházejte dostupná studia a před použitím zkontrolujte nahrazované prvky. Koncepty zůstávají označené.",
|
||||
"render.ribbon_surface_maps": "Mapy povrchu",
|
||||
"render.save_setup": "Uložit nastavení",
|
||||
"render.setup_name": "Název nastavení",
|
||||
"render.studio_add_area": "Přidat plošné světlo",
|
||||
"render.studio_ambient": "Konstantní zář prostředí",
|
||||
"render.studio_aperture": "Poloměr clony",
|
||||
"render.studio_area": "Plošné světlo",
|
||||
"render.studio_area_help": "Nejvýše čtyři jednostranné obdélníky. Kolmé poloviční hrany U a V určují velikost; U × V míří na osvětlenou stranu. Souřadnice jsou světové milimetry.",
|
||||
"render.studio_center": "Střed X, Y, Z (mm)",
|
||||
"render.studio_denoise": "Potlačit šum v hotovém obrazu",
|
||||
"render.studio_denoise_help": "Závěrečný filtr respektuje hrany; může změkčit detaily. Sklo a ostré zrcadlové povrchy se nefiltrují.",
|
||||
"render.studio_direction": "Směr ke slunci (X, Y, Z)",
|
||||
"render.studio_environment": "Prostředí",
|
||||
"render.studio_environment_bounds": "Intenzita prostředí musí být 0–100 a otočení −360° až 360°.",
|
||||
"render.studio_environment_none": "Bez mapy prostředí",
|
||||
"render.studio_focus": "Osová zaostřovací vzdálenost",
|
||||
"render.studio_half_u": "Poloviční hrana U (mm)",
|
||||
"render.studio_half_v": "Poloviční hrana V (mm)",
|
||||
"render.studio_hdr": "Soubor Radiance HDR",
|
||||
"render.studio_hdr_browse": "Vybrat HDR…",
|
||||
"render.studio_hdr_help": "Radiance .hdr, panorama 2:1, nejvýše 2048 × 1024. EXR není dostupné. Předpoklad barev platí jen při neurčených primárních barvách.",
|
||||
"render.studio_hdr_required": "Vyberte místní soubor .hdr.",
|
||||
"render.studio_hdr_srgb": "Neurčené primární barvy chápat jako lineární sRGB",
|
||||
"render.studio_intensity": "Intenzita prostředí",
|
||||
"render.studio_lens": "Hloubka ostrosti",
|
||||
"render.studio_lens_perspective": "Hloubka ostrosti vyžaduje perspektivní kameru.",
|
||||
"render.studio_lighting": "Studiové osvětlení",
|
||||
"render.studio_lighting_enable": "Použít vlastní studiové osvětlení",
|
||||
"render.studio_radiance": "Lineární RGB zář",
|
||||
"render.studio_remove_area": "Odebrat plošné světlo",
|
||||
"render.studio_rotation": "Otočení prostředí",
|
||||
"render.studio_runtime_help": "Optické úpravy těles jsou v Materiálech GPU obrazu. Ovládání studia a materiálů je dočasné a nemění uložené vzhledy.",
|
||||
"render.studio_sky": "Umělecká obloha",
|
||||
"render.studio_sky_help": "Původní přechod oblohy pro osvětlení; nejde o kalibrovaný model atmosféry.",
|
||||
"render.studio_sun": "Slunce",
|
||||
"render.studio_sun_radius": "Úhlový poloměr",
|
||||
"render.studio_tone_map": "Mapování tónů",
|
||||
"render.unavailable_preview": "Toto nastavení vyžaduje nedostupný profil náhledu.",
|
||||
"render.unsaved": "Neuložené změny náhledu",
|
||||
"render.use_preview": "Použít integrovaný náhled",
|
||||
@@ -1236,7 +1067,6 @@
|
||||
"search.try": "Zadejte název příkazu, prvek v tomto návrhu nebo parametr. Chcete-li spustit příkaz s hodnotami, zadejte jeho id a objekt JSON: {example}",
|
||||
"search.valid": "Platné",
|
||||
"settings.display_quality_note": "Jemnější úrovně kreslí zakřivené plochy a hrany hladčeji, ale síťování trvá déle. Mění se jen zobrazení, nikdy návrh ani exporty.",
|
||||
"settings.frame_rate_details_note": "Přidá graf časů posledních snímků s vykreslenými trojúhelníky, histogram časů snímků a to, co vykreslila GPU. Kliknutím na snímkovou frekvenci v pohledu je zobrazíte nebo skryjete.",
|
||||
"settings.frame_rate_note": "Zobrazuje snímky za sekundu v pravém dolním rohu pohledu.",
|
||||
"settings.measure_axis_distances": "Zobrazit vzdálenosti X/Y/Z",
|
||||
"settings.measure_axis_distances_help": "Při otevřeném měření zobrazit kromě celkové vzdálenosti i složky ve směru os.",
|
||||
@@ -1293,10 +1123,6 @@
|
||||
"toolbar.customize_note": "Karta {tab}. Zaškrtnutím příkazu přidáte jeho tlačítko na panel nástrojů. Můžete také přetáhnout řádek z rozbalovací nabídky panelu na panel nástrojů, odstranit tlačítko přetažením mimo panel nástrojů nebo použít připínáček na řádku rozbalovací nabídky.",
|
||||
"toolbar.image": "OBRÁZEK",
|
||||
"toolbar.render": "RENDER",
|
||||
"toolbar.render_appearance": "VZHLED",
|
||||
"toolbar.render_camera": "KAMERA",
|
||||
"toolbar.render_environment": "PROSTŘEDÍ",
|
||||
"toolbar.render_lighting": "OSVĚTLENÍ",
|
||||
"toolbar.render_scene": "SCÉNA",
|
||||
"trace.points": "Bodů podél cesty: {count}",
|
||||
"trace.stopped_by_contact": "Končí ve vzorku {sample}: {a} by prošlo skrz {b}",
|
||||
@@ -1495,7 +1321,7 @@
|
||||
"ui.agent_chat_pop_out": "Otevřít ve vlastním okně, které lze přesunout na jinou obrazovku",
|
||||
"ui.agent_chat_previous_picture": "Předchozí obrázek",
|
||||
"ui.agent_chat_privacy": "Nic se nikam neodesílá, jen agentovi, kterého spouštíte pod vlastním účtem.",
|
||||
"ui.agent_chat_privacy_note": "Chat sám nic nikam neodesílá: agent, kterého spouštíte, komunikuje se svou službou pod vaším účtem. SolidCraft nikdy nevidí ani neukládá klíč či heslo.",
|
||||
"ui.agent_chat_privacy_note": "SolidCraft nic nikam neodesílá: agent, kterého spouštíte, komunikuje se svou službou pod vaším účtem. SolidCraft nikdy nevidí ani neukládá klíč či heslo.",
|
||||
"ui.agent_chat_queue": "Zařadit",
|
||||
"ui.agent_chat_queue_full": "Čeká příliš mnoho zpráv: některou odeberte nebo počkejte na agenta.",
|
||||
"ui.agent_chat_queued": "Ve frontě",
|
||||
@@ -1559,7 +1385,6 @@
|
||||
"ui.ai_http_busy": "endpoint je zaneprázdněný nebo překročil limit; zkuste to později",
|
||||
"ui.ai_http_key": "endpoint odmítl klíč: zkontrolujte jej v Předvolby > AI",
|
||||
"ui.ai_http_not_found": "takový endpoint nebo model neexistuje: zkontrolujte základní URL a název modelu",
|
||||
"ui.ai_http_too_large": "konverzace je pro tento endpoint příliš velká; začněte nový chat",
|
||||
"ui.ai_key": "API klíč",
|
||||
"ui.ai_key_hint": "volitelné; uloženo v systémové klíčence",
|
||||
"ui.ai_key_needs_https": "Klíč se odesílá pouze přes HTTPS nebo na tento počítač.",
|
||||
@@ -1587,6 +1412,7 @@
|
||||
"ui.ai_permissions": "Oprávnění",
|
||||
"ui.ai_preset": "Předvolba",
|
||||
"ui.ai_remove": "Odebrat",
|
||||
"ui.ai_request_limit": "zastaveno po 24 požadavcích na jednu zprávu; pro pokračování pošlete další zprávu",
|
||||
"ui.ai_reset": "Obnovit",
|
||||
"ui.ai_settings": "Nastavení AI…",
|
||||
"ui.ai_show_features": "Zobrazit funkce AI",
|
||||
@@ -1680,7 +1506,6 @@
|
||||
"ui.command_done": "{command} hotovo",
|
||||
"ui.command_palette_hint": "Zadejte příkaz, např. {example} — nebo stiskněte S pro hledání",
|
||||
"ui.concentric": "Soustřednost",
|
||||
"ui.cone": "Kužel",
|
||||
"ui.conic_curve": "Kuželosečka",
|
||||
"ui.constant": "Konstantní",
|
||||
"ui.constraints_conflict": "Konflikt vazeb",
|
||||
@@ -1739,11 +1564,6 @@
|
||||
"ui.draft": "Úkos",
|
||||
"ui.drawing": "Výkres",
|
||||
"ui.drill_point": "Špička vrtáku",
|
||||
"ui.easyprint_consent": "EasyPrint je slicer od Printables v prohlížeči. Aby se v něm model otevřel, SolidCraft jej nahraje na servery Printables. Odkaz vyprší po 30 minutách a musíte být přihlášeni k Printables. Pokračovat?",
|
||||
"ui.easyprint_failed": "Nahrání do EasyPrint se nezdařilo: {error}",
|
||||
"ui.easyprint_opened": "Otevřeno v EasyPrint",
|
||||
"ui.easyprint_row": "EasyPrint (Printables, v prohlížeči)",
|
||||
"ui.easyprint_uploading": "Nahrávám na Printables…",
|
||||
"ui.edit": "Upravit",
|
||||
"ui.editJointLimits": "Upravit limity kloubu",
|
||||
"ui.edit_section_analysis": "Upravit analýzu řezu",
|
||||
@@ -1769,8 +1589,6 @@
|
||||
"ui.filter_commands_or_keys": "Filtrovat příkazy nebo klávesy",
|
||||
"ui.findInTimeline": "Najít na časové ose",
|
||||
"ui.findInWindow": "Najít v okně",
|
||||
"ui.fine": "Jemná",
|
||||
"ui.finest": "Nejjemnější",
|
||||
"ui.finish_sketch": "Dokončit náčrt",
|
||||
"ui.fit": "Přizpůsobit",
|
||||
"ui.fix_unfix": "Zafixovat/uvolnit",
|
||||
@@ -1778,7 +1596,6 @@
|
||||
"ui.flip": "Převrátit",
|
||||
"ui.format": "Formát",
|
||||
"ui.frame_rate": "Snímková frekvence",
|
||||
"ui.frame_rate_details": "Podrobnosti snímkové frekvence",
|
||||
"ui.front": "Zepředu",
|
||||
"ui.fully_constrained": "Plně zavazbeno",
|
||||
"ui.function": "Funkce",
|
||||
@@ -1801,7 +1618,6 @@
|
||||
"ui.heading_circular_pattern": "KRUHOVÉ POLE",
|
||||
"ui.heading_coil": "ŠROUBOVICE",
|
||||
"ui.heading_combine": "KOMBINOVAT",
|
||||
"ui.heading_cone": "KUŽEL",
|
||||
"ui.heading_configure": "KONFIGURACE",
|
||||
"ui.heading_constraints": "VAZBY",
|
||||
"ui.heading_construct": "KONSTRUKCE",
|
||||
@@ -1861,7 +1677,6 @@
|
||||
"ui.heading_torus": "ANULOID",
|
||||
"ui.heading_utilities": "NÁSTROJE",
|
||||
"ui.heading_utility": "NÁSTROJE",
|
||||
"ui.heading_weldments": "SVAŘENCE",
|
||||
"ui.help": "Nápověda",
|
||||
"ui.hide": "Skrýt",
|
||||
"ui.hideAxes": "Skrýt osy",
|
||||
@@ -1869,8 +1684,6 @@
|
||||
"ui.hideOrigin": "Skrýt počátek",
|
||||
"ui.hidePlanes": "Skrýt roviny",
|
||||
"ui.hideProfile": "Skrýt profil",
|
||||
"ui.hide_origin_by_default": "Ve výchozím stavu skrýt počátek",
|
||||
"ui.hide_origin_note": "Nové návrhy začínají se skrytou složkou Počátek. Návrh si ponechá vlastní nastavení počátku, jakmile ho zobrazíte nebo skryjete.",
|
||||
"ui.history_back": "O krok zpět",
|
||||
"ui.history_below_browser": "Pod prohlížečem",
|
||||
"ui.history_first": "Přejít na začátek",
|
||||
@@ -2000,7 +1813,6 @@
|
||||
"ui.project_face_edges_sketch": "Promítat hrany plochy do nových náčrtů",
|
||||
"ui.properties": "Vlastnosti",
|
||||
"ui.pull_direction": "Směr vyjímání",
|
||||
"ui.quality": "Kvalita",
|
||||
"ui.quantity": "Počet",
|
||||
"ui.quantity_2": "Počet 2",
|
||||
"ui.quit": "Ukončit",
|
||||
@@ -2039,7 +1851,9 @@
|
||||
"ui.right": "Zprava",
|
||||
"ui.save": "Uložit",
|
||||
"ui.save_as": "Uložit jako",
|
||||
"ui.save_mesh_no_slicer": "Nebyl nalezen žádný slicer: zvolte Vlastní… a vyberte svůj slicer, nebo vypněte Odeslat do sliceru",
|
||||
"ui.save_mesh_pick_bodies": "Vyberte tělesa k uložení",
|
||||
"ui.save_mesh_temp_failed": "Nelze vytvořit {folder}: {error}",
|
||||
"ui.saved_to": "Uloženo {path}",
|
||||
"ui.sbox_pin": "Připnout nahoru ve vyhledávání S",
|
||||
"ui.sbox_unpin": "Odepnout",
|
||||
@@ -2054,6 +1868,7 @@
|
||||
"ui.selection_shift_add_only": "Shift+klik pouze přidává",
|
||||
"ui.selection_shift_add_only_tip": "Zapnuto: Shift přidává položky, Ctrl/Cmd přepíná. Vypnuto (výchozí): Shift i Ctrl/Cmd přepínají.",
|
||||
"ui.send": "Odeslat",
|
||||
"ui.send_to_slicer": "Odeslat do sliceru",
|
||||
"ui.sent_to_slicer": "Odesláno do {slicer}",
|
||||
"ui.sheet_gap": "Mezera",
|
||||
"ui.shell": "Skořepina",
|
||||
@@ -2092,7 +1907,6 @@
|
||||
"ui.spline": "Splajn",
|
||||
"ui.split_body": "Rozdělit těleso",
|
||||
"ui.stand_up_tip": "Když se soubor STEP nebo IGES otevře s osou Z nahoru, nabídnout otočení o čtvrt otáčky kolem X",
|
||||
"ui.standard": "Standardní",
|
||||
"ui.start": "Start",
|
||||
"ui.start_over": "Začít znovu",
|
||||
"ui.start_page": "Úvodní stránka",
|
||||
@@ -2145,7 +1959,6 @@
|
||||
"ui.untitled": "Bez názvu",
|
||||
"ui.up_axis_note": "Která osa v pohledech míří nahoru (pohled podél osy, výchozí pohled, pohled kolmo). Návrhy se nemění.",
|
||||
"ui.updateToCurrentView": "Aktualizovat na aktuální pohled",
|
||||
"ui.upload": "Nahrát",
|
||||
"ui.view_centre": "Střed pohledu",
|
||||
"ui.visual_shaded_hidden_edges": "Stínovaný se skrytými hranami",
|
||||
"ui.visual_shaded_with_edges": "Stínovaný s hranami",
|
||||
|
||||
+11
-410
File diff suppressed because it is too large
Load Diff
+7
-163
@@ -85,7 +85,6 @@
|
||||
"Choose…": "Auswählen…",
|
||||
"Circular": "Kreisförmig",
|
||||
"Circumference": "Umfang",
|
||||
"Clockwise": "Im Uhrzeigersinn",
|
||||
"Close draft": "Entwurf schließen",
|
||||
"Collapse": "Einklappen",
|
||||
"Collisions": "Kollisionen",
|
||||
@@ -338,7 +337,7 @@
|
||||
"Profile / Edges": "Profil / Kanten",
|
||||
"Profiles or curves": "Profile oder Kurven",
|
||||
"Profiles/Curves": "Profile/Kurven",
|
||||
"Quantity": "Anzahl",
|
||||
"Quantity": "Größe",
|
||||
"RECENT": "ZULETZT",
|
||||
"REFERENCE": "REFERENZ",
|
||||
"RULED SURFACE": "REGELFLÄCHE",
|
||||
@@ -405,7 +404,6 @@
|
||||
"Search this library": "Diese Bibliothek durchsuchen",
|
||||
"Second plane": "Zweite Ebene",
|
||||
"Section": "Schnitt",
|
||||
"Section position": "Lage des Querschnitts",
|
||||
"Sections (one sketch each)": "Querschnitte (je eine Skizze)",
|
||||
"Select Other": "Andere auswählen",
|
||||
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Anderes auswählen: Halten Sie die linke Taste ruhig auf dem Modell gedrückt (oder Rechtsklick › Anderes auswählen…), um alles unter dem Cursor aufzulisten; klicken Sie erneut an dieselbe Stelle, um das nächste Element dort auszuwählen. Doppelklick mit der mittleren Taste wechselt zur Startansicht (Einstellungen › Navigation).",
|
||||
@@ -421,7 +419,6 @@
|
||||
"Show Agent Cursor": "Agenten-Cursor anzeigen",
|
||||
"Show in folder": "Im Ordner anzeigen",
|
||||
"Show the pointer an AI agent moves (#35)": "Den Zeiger anzeigen, den ein KI-Agent bewegt (#35)",
|
||||
"Show tool at cursor": "Werkzeug am Cursor anzeigen",
|
||||
"Signed axes": "Vorzeichenbehaftete Achsen",
|
||||
"Size": "Größe",
|
||||
"Sketch constraints": "Skizzenbedingungen",
|
||||
@@ -435,11 +432,9 @@
|
||||
"Specialise": "Spezialisieren",
|
||||
"Speed": "Geschwindigkeit",
|
||||
"Splitting tool": "Trennwerkzeug",
|
||||
"Square": "Quadrat",
|
||||
"Stand it up": "Aufrichten",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Aufrichten: eine Vierteldrehung um X, als ein Verschieben-Merkmal, das sich rückgängig machen lässt.",
|
||||
"Stand this part up?": "Dieses Teil aufrichten?",
|
||||
"Start angle": "Startwinkel",
|
||||
"Start bulge": "Startausbauchung",
|
||||
"Start continuity": "Startstetigkeit",
|
||||
"Step": "Schritt",
|
||||
@@ -525,7 +520,6 @@
|
||||
"angle, height": "Winkel, Höhe",
|
||||
"appearance.dielectric_ior": "Brechungsindex des Dielektrikums",
|
||||
"appearance.metallic": "Metallanteil",
|
||||
"appearance.only_available": "Nur verfügbare Erscheinungsbilder",
|
||||
"appearance.pbr_help": "Diese Oberfläche in Render oder einem Produkt-PNG ansehen. Keine Brechung oder Transmission.",
|
||||
"appearance.pbr_opaque": "Opakes PBR",
|
||||
"appearance.roughness": "Rauheit",
|
||||
@@ -565,7 +559,6 @@
|
||||
"cmd.canvas_delete": "Zeichenfläche löschen",
|
||||
"cmd.canvas_edit": "Zeichenfläche bearbeiten",
|
||||
"cmd.canvas_insert": "Zeichenfläche",
|
||||
"cmd.canvas_to_sketch": "Skizze aus Zeichenfläche",
|
||||
"cmd.component_activate": "Komponente aktivieren",
|
||||
"cmd.component_break_link": "Verknüpfung trennen",
|
||||
"cmd.component_capture_position": "Position erfassen",
|
||||
@@ -632,7 +625,6 @@
|
||||
"cmd.document_metadata": "Konstruktionseigenschaften",
|
||||
"cmd.document_metadata_remove": "Konstruktionseigenschaft entfernen",
|
||||
"cmd.document_metadata_set": "Konstruktionseigenschaften ändern",
|
||||
"cmd.document_revision": "Konstruktionsrevision",
|
||||
"cmd.document_units": "Einheiten der Konstruktion ändern",
|
||||
"cmd.edit_checkpoint": "Prüfpunkt",
|
||||
"cmd.edit_restore_checkpoint": "Prüfpunkt wiederherstellen",
|
||||
@@ -647,7 +639,6 @@
|
||||
"cmd.file_export": "Exportieren",
|
||||
"cmd.file_insert_mesh": "Netz einfügen",
|
||||
"cmd.file_insert_step": "STEP einfügen",
|
||||
"cmd.file_open_in_easyprint": "Mit EasyPrint öffnen",
|
||||
"cmd.file_save_mesh": "Als Netz speichern",
|
||||
"cmd.file_slicers": "Installierte Slicer",
|
||||
"cmd.frame_member": "Rahmenelement",
|
||||
@@ -693,7 +684,6 @@
|
||||
"cmd.material_create": "Neues physikalisches Material",
|
||||
"cmd.material_delete": "Physisches Material löschen",
|
||||
"cmd.material_edit": "Physisches Material bearbeiten",
|
||||
"cmd.material_inspect": "Physikalisches Material prüfen",
|
||||
"cmd.material_list": "Materialien auflisten",
|
||||
"cmd.material_properties_directional": "Richtungsabhängiger Elastizitätsmodul",
|
||||
"cmd.material_properties_validate": "Physikalische Materialdaten prüfen",
|
||||
@@ -740,32 +730,13 @@
|
||||
"cmd.plastic_rules": "Kunststoffregeln auflisten",
|
||||
"cmd.plastic_snap_fit": "Rastverbindung",
|
||||
"cmd.render_backends": "Render-Backends",
|
||||
"cmd.render_camera_fit": "Renderobjekt einpassen",
|
||||
"cmd.render_camera_get": "Renderkamera prüfen",
|
||||
"cmd.render_cancel": "Rendering abbrechen",
|
||||
"cmd.render_export": "PNG exportieren",
|
||||
"cmd.render_hdr_inspect": "HDR-Quelle untersuchen",
|
||||
"cmd.render_library_appearance_apply": "Opakes Bibliotheksaussehen anwenden",
|
||||
"cmd.render_library_describe": "Render-Ressource untersuchen",
|
||||
"cmd.render_library_environment_apply": "Integrierte HDR-Umgebung anwenden",
|
||||
"cmd.render_library_list": "Render-Bibliothek durchsuchen",
|
||||
"cmd.render_library_physical_prepare": "Prepare Physical Reference",
|
||||
"cmd.render_library_studio_apply": "Erstellte Studiolichter anwenden",
|
||||
"cmd.render_library_studio_prepare": "Erstellte Studiolichter vorbereiten",
|
||||
"cmd.render_library_surface_bind": "Save Surface Binding",
|
||||
"cmd.render_library_surface_prepare": "Erstellte Oberflächenmaps vorbereiten",
|
||||
"cmd.render_library_surface_unbind": "Remove Surface Binding",
|
||||
"cmd.render_library_targets": "Ziele für Render-Ressourcen suchen",
|
||||
"cmd.render_preflight": "Renderanfrage prüfen",
|
||||
"cmd.render_result_pin": "Angezeigtes Render-Ergebnis behalten",
|
||||
"cmd.render_scene": "Renderszene prüfen",
|
||||
"cmd.render_settings_apply": "Renderkamera anwenden",
|
||||
"cmd.render_settings_capture_hdr": "Render-Einstellungen mit HDR speichern",
|
||||
"cmd.render_settings_get": "Rendereinstellungen",
|
||||
"cmd.render_settings_set": "Rendereinstellungen speichern",
|
||||
"cmd.render_stage_backdrop": "Physischen Renderhintergrund speichern",
|
||||
"cmd.render_stage_clear": "Remove Render Stage",
|
||||
"cmd.render_stage_ground": "Save Render Ground",
|
||||
"cmd.render_start": "Bild rendern",
|
||||
"cmd.render_status": "Renderfortschritt",
|
||||
"cmd.select_clear": "Auswahl aufheben",
|
||||
@@ -854,7 +825,6 @@
|
||||
"cmd.sketch_spline_fit_point": "Spline mit Fitpunkten",
|
||||
"cmd.sketch_spun_profile": "Gedrehtes Profil",
|
||||
"cmd.sketch_toggle_driven": "Abhängiges Maß umschalten",
|
||||
"cmd.sketch_transform": "Transformieren",
|
||||
"cmd.sketch_trim_path": "Entlang Pfad trimmen",
|
||||
"cmd.solid_align": "Ausrichten",
|
||||
"cmd.solid_boundary_fill": "Begrenzungsfüllung",
|
||||
@@ -1088,8 +1058,6 @@
|
||||
"manage.material_hint": "Wählen Sie Körper und deren physikalisches Material aus.",
|
||||
"manage.parameters_hint": "Bearbeiten Sie Ausdrücke und Einheiten. Änderungen werden mit der Eingabetaste oder beim Verlassen eines Feldes übernommen.",
|
||||
"manage.rules_hint": "Wählen Sie eine Regel zum Bearbeiten oder erstellen Sie eine neue. Bestätigen Sie, um die Regel zu speichern.",
|
||||
"material.editor.axes_numeric": "1 / 2 / 3",
|
||||
"material.editor.axes_wood": "L / R / T",
|
||||
"measure.at": "Bei",
|
||||
"measure.item": "Element {number}",
|
||||
"messages.ago_h": "vor {n} Std.",
|
||||
@@ -1108,7 +1076,6 @@
|
||||
"motion.key_here": "Schlüsselbild hier setzen",
|
||||
"motion.key_here_hint": "Ein Schlüsselbild bei diesem Schritt mit dem aktuellen Gelenkwert hinzufügen",
|
||||
"motion.no_collisions": "Keine Kollisionen in der Untersuchung",
|
||||
"motion.plot_quantity": "Größe",
|
||||
"motion.step_back": "Schritt zurück",
|
||||
"motion.step_forward": "Schritt vor",
|
||||
"motion.to_end": "Zum Ende",
|
||||
@@ -1118,11 +1085,9 @@
|
||||
"nav.blender.help": "Mittlere Maustaste dreht, Umschalt+Mitte verschiebt, Strg+Mitte zoomt; rechtes Ziehen lässt die Kamera unverändert",
|
||||
"nav.free_orbit.help": "Maus und 3D-Maus drehen über beide Pole hinweg und behalten die Rollneigung bei. Das Einrasten mit Umschalt ist deaktiviert.",
|
||||
"nav.free_orbit.label": "Freie Drehung",
|
||||
"nav.free_orbit.snap_help": "Maus und 3D-Maus drehen über beide Pole hinweg und behalten die Rollausrichtung bei. Halten Sie beim Ziehen zum Drehen die Umschalttaste gedrückt, um Blickrichtung und Rollausrichtung einrasten zu lassen.",
|
||||
"nav.fusion.help": "Mittlere Maustaste verschiebt, Umschalt+Mitte dreht, rechte Maustaste dreht",
|
||||
"nav.inventor.help": "Mittlere Maustaste verschiebt, Umschalt+Mitte dreht, Strg+Mitte zoomt, rechte Maustaste dreht",
|
||||
"nav.openscad.help": "Linksziehen auf leerem Hintergrund dreht, Umschalt+Linksziehen zieht ein Auswahlrechteck auf, Skizzengeometrie wird durch Ziehen bearbeitet; rechtes oder mittleres Ziehen verschiebt, Umschalt+rechtes oder Umschalt+mittleres Ziehen zoomt",
|
||||
"nav.perspective.sketch": "Beim Bearbeiten einer Skizze wird die orthografische Ansicht verwendet. Beenden Sie die Skizze, um die Perspektive umzuschalten.",
|
||||
"nav.solidworks.help": "Mittlere Maustaste dreht, Strg+Mitte verschiebt, Umschalt+Mitte zoomt, rechte Maustaste dreht",
|
||||
"nav.tinkercad.help": "Rechte Maustaste dreht, mittlere oder Umschalt+rechte Maustaste verschiebt",
|
||||
"nav.touchpad.help": "Umschalt gedrückt halten und den Zeiger zum Verschieben bewegen; mit Alt/Option drehen. Keine Maustaste nötig. Vertikales Scrollen zoomt.",
|
||||
@@ -1156,35 +1121,13 @@
|
||||
"recovery.just_now": "gerade eben",
|
||||
"recovery.min_ago": "vor {count} Min.",
|
||||
"recovery.never_saved": "nie gespeichert",
|
||||
"render.add_ground": "Add fitted ground to setup",
|
||||
"render.background": "Szenenhintergrund",
|
||||
"render.bloom_enable": "Darstellungs-Bloom",
|
||||
"render.bloom_help": "Kreativer Ausgabeglanz aus linearen Glanzlichtern mit drei normierten Durchläufen. Radius in Ausgabepixeln; Addition vor sRGB. Keine Linsen-, Volumen- oder Konvergenzsimulation. Das rohe EXR bleibt unverändert.",
|
||||
"render.bloom_opaque": "Bloom benötigt eine deckende Ausgabe. Transparentes PNG wird abgelehnt, nicht still abgeflacht.",
|
||||
"render.bloom_radius": "Bloom-Radius",
|
||||
"render.bloom_strength": "Bloom-Stärke",
|
||||
"render.bloom_threshold": "Linearer Glanzlichtschwellwert",
|
||||
"render.camera_pitch": "Kameraneigung",
|
||||
"render.camera_set": "Render-Kamera einstellen",
|
||||
"render.clear_stage": "Remove saved stage",
|
||||
"render.cpu_budget": "CPU-Ressourcenprofil",
|
||||
"render.cpu_budget_bodies": "Körperobergrenze",
|
||||
"render.cpu_budget_compact": "Kompakt (bisherige Grenzen)",
|
||||
"render.cpu_budget_details": "Ressourcengrenzen",
|
||||
"render.cpu_budget_help": "Größere Grenzen ausdrücklich wählen. Auflösung und Samples werden nie automatisch reduziert.",
|
||||
"render.cpu_budget_memory": "Speicherobergrenze pro Auftrag",
|
||||
"render.cpu_budget_not_guarantee": "Dies sind Zulassungsgrenzen, keine RAM-Reservierung oder Geschwindigkeitsgarantie. Geometrie, Film und Vorschauen teilen das Speicherbudget. Das Rendern bleibt abbrechbar.",
|
||||
"render.cpu_budget_resize": "Der Entwurf überschreitet das CPU-Profil oder sein Kamera-Sample-Budget. Kleinere Abmessungen ausdrücklich bestätigen oder ein größeres Profil wählen.",
|
||||
"render.cpu_budget_studio": "Studio (große Bilder)",
|
||||
"render.cpu_budget_triangles": "Dreiecksobergrenze",
|
||||
"render.cpu_budget_workstation": "Workstation (komplexe Szenen)",
|
||||
"render.exr_pending": "Die GPU kann rohe lineare EXR-Dateien behalten. Kompression, mehrschichtige Bilder und zusätzliche Renderpässe bleiben nicht verfügbar.",
|
||||
"render.fit": "Produkt einpassen",
|
||||
"render.image_cancelled": "Rendering abgebrochen",
|
||||
"render.image_cancelling": "Wird abgebrochen",
|
||||
"render.image_completed": "Bild bereit zum Export",
|
||||
"render.image_encoding": "PNG wird kodiert",
|
||||
"render.image_export": "Fertiges Bild exportieren",
|
||||
"render.image_export": "PNG exportieren",
|
||||
"render.image_failed": "Rendering fehlgeschlagen",
|
||||
"render.image_height": "Höhe",
|
||||
"render.image_help": "CPU-Vorschaubild · PNG · angenäherte Transparenz",
|
||||
@@ -1202,98 +1145,17 @@
|
||||
"render.image_unknown": "Renderstatus nicht verfügbar",
|
||||
"render.image_use_largest": "Größte Größe mit diesem Seitenverhältnis verwenden:",
|
||||
"render.image_width": "Breite",
|
||||
"render.include_rig_cards": "Include passive cards and flags",
|
||||
"render.library_applied": "Ressourcenvariante angewendet. Vollständigen Renderauftrag vor dem Start prüfen.",
|
||||
"render.library_apply_hdr": "Mit dieser HDR-Umgebung speichern",
|
||||
"render.library_apply_maps": "Erstellte GPU-Oberflächenmaps verwenden",
|
||||
"render.library_apply_studio": "Erstellte Studiovariante speichern",
|
||||
"render.library_apply_uniform": "Einheitliches opakes Aussehen anwenden",
|
||||
"render.library_authored_help": "Original erstellte Ressourcen dienen der Darstellung, nicht als gemessene technische Daten. Jede Aktion wendet nur ihre angegebene Variante an.",
|
||||
"render.library_blocked": "Diese Ressource benötigt noch Kalibrierung oder eine externe Sonne und kann noch nicht angewendet werden.",
|
||||
"render.library_center": "Mittelpunkt des Lichtaufbaus",
|
||||
"render.library_emitters_only_ack": "Nur Strahler verwenden; dokumentierte passive Karten auslassen",
|
||||
"render.library_environment_concepts": "Umgebungskonzepte",
|
||||
"render.library_extent": "Objektausdehnung",
|
||||
"render.library_hdr": "Integrierte HDR-Umgebungen",
|
||||
"render.library_hdr_help": "Bettet das originale HDR in die gespeicherte Einstellung ein. Der Längengradursprung wird ausdrücklich umgerechnet; nach dem Speichern ist keine externe Datei nötig.",
|
||||
"render.library_light_concepts": "Beleuchtungskonzepte",
|
||||
"render.library_maps_help": "Built-in maps can be saved with pinned sources and local occurrence mapping. Temporary optical overrides remain a separate mode.",
|
||||
"render.library_material_concepts": "Materialkonzepte",
|
||||
"render.library_next": "Nächste Seite",
|
||||
"render.library_physical_profiles": "Physical reference profiles",
|
||||
"render.library_previous": "Vorherige Seite",
|
||||
"render.library_quality_concepts": "Qualitätsspezifikationen",
|
||||
"render.library_replace_environment": "Vorhandene Umgebung ausdrücklich ersetzen",
|
||||
"render.library_replace_studio": "Replace saved lighting, environment and stage",
|
||||
"render.library_results": "Passende Ressourcen",
|
||||
"render.library_rig_help": "Fit once at an explicit center and physical extent. Complete rigs include passive cards; calibration blockers still refuse. Conservative emitter/card clearance is checked before saving.",
|
||||
"render.library_rigs": "Erstellte Lichtaufbauten",
|
||||
"render.library_save_maps": "Save maps to this setup",
|
||||
"render.library_scene_bounds": "Erfasste Szenengrenzen verwenden",
|
||||
"render.library_scenes": "Szenenspezifikationen",
|
||||
"render.library_search": "Bibliothek durchsuchen",
|
||||
"render.library_selene": "SELENE: opake Oberflächen",
|
||||
"render.library_specification_only": "Nur Spezifikation: keine vollständig ausführbare Voreinstellung. Erforderliche Effekte, Ressourcen und Objektrollen müssen noch implementiert und gebunden werden.",
|
||||
"render.library_surfaces": "Erstellte PBR-Oberflächen",
|
||||
"render.library_target": "Zielkörper",
|
||||
"render.library_title": "Render-Ressourcenbibliothek",
|
||||
"render.library_uniform_ack": "Einheitliche Näherung ohne räumliche oder erweiterte Effekte verwenden",
|
||||
"render.material_color_replace": "Basismap ersetzt Körperfarbe",
|
||||
"render.material_limits": "Höchstens 128 Körperüberschreibungen, 32 getrennte Glaskörper, 64 Beschichtungen und 128 Texturbindungen. Identische Maps teilen Speicher; Geräte- und Gesamtspeichergrenzen bleiben aktiv.",
|
||||
"render.material_metallic_map": "Metallanteil-Map (linear)",
|
||||
"render.material_roughness_map": "Rauheitsmap (linear)",
|
||||
"render.material_v_up": "Textur-V zeigt im Bild nach oben",
|
||||
"render.navigation_help": "Die Kamera bewegt sich wie im Konstruieren (Einstellungen › Navigation). Setup speichern übernimmt die aktuelle Kamera.",
|
||||
"render.opaque_image_summary": "Vorschau-PNG · opakes PBR · Transparenz durch Blending",
|
||||
"render.opaque_preview_help": "Opakes PBR und einfache Farben; Transparenz durch Blending. Brechung, HDRI und finales Rendering sind noch nicht verfügbar.",
|
||||
"render.output_format_status": "PNG enthält das exakte fertige Anzeigebild. Optionales GPU-EXR bewahrt rohe lineare FLOAT32-Strahldichte vor Filtern. Export ersetzt nie durch Vorschaucache oder anderen Auftrag.",
|
||||
"render.passive_stage": "Render-only stage",
|
||||
"render.passive_stage_help": "Finite passive panels belong only to rendered images. They do not add CAD bodies or mass. Placement is fixed after applying; intersecting product bounds refuse. CPU does not provide diffuse bounce lighting.",
|
||||
"render.perspective": "Perspektivische Kamera",
|
||||
"render.physical_acknowledge": "I understand this partial reference needs grade, condition and source review",
|
||||
"render.physical_associations": "Physical candidates and roles",
|
||||
"render.physical_copy_plan": "Copy plan with sources and omissions",
|
||||
"render.physical_material_name": "Proposed reference material name",
|
||||
"render.physical_omissions": "Properties not representable in the native card",
|
||||
"render.physical_plan_only": "Read-only plan prepared. No material was created or assigned. Keep its extended profile and omissions when reviewing the native card.",
|
||||
"render.physical_prepare_plan": "Prepare creation plan (does not apply)",
|
||||
"render.physical_separate": "Appearance is not material identification. These are sourced references, not certified effective properties. Existing mass, engineering materials and optics are unchanged.",
|
||||
"render.physical_unknown": "No numerical candidate is established. Grade, formulation or construction must be specified; unknown is not zero.",
|
||||
"render.preview": "Integrierte Vorschau",
|
||||
"render.preview_help": "Rasterdarstellung mit gemischter Transparenz. Brechung, HDRI und finales Rendering sind noch nicht verfügbar.",
|
||||
"render.reload_settings": "Gespeicherte Einstellungen laden",
|
||||
"render.replace_saved_surface": "Replace an existing saved binding for this target",
|
||||
"render.restore_camera": "Gespeicherte Kamera laden",
|
||||
"render.result_reduced_display": "Anzeigevorschau auf die Geräte-/Cachegrenze reduziert. PNG und Export behalten die volle Auflösung; native 100%-Ansicht ist hier nicht verfügbar.",
|
||||
"render.return_design": "Zurück zu Design",
|
||||
"render.ribbon_backgrounds": "Hintergründe",
|
||||
"render.ribbon_check": "Prüfen",
|
||||
"render.ribbon_check_help": "Genau den aktuellen Renderentwurf prüfen. Kein stiller Rendererwechsel, keine Verkleinerung oder Ersetzung.",
|
||||
"render.ribbon_edit_lights": "Licht ändern",
|
||||
"render.ribbon_environment_help": "HDR-Beleuchtung, Kamerahintergründe und physische Bühnen sind getrennte Einstellungen mit ausdrücklicher Ersetzungsbestätigung.",
|
||||
"render.ribbon_finish_dialog": "Zuerst den Datei- oder Bestätigungsdialog abschließen.",
|
||||
"render.ribbon_frame": "Einpassen",
|
||||
"render.ribbon_hdr_library": "HDR-Bibliothek",
|
||||
"render.ribbon_job_active": "Ein Render läuft bereits; vor einem neuen Auftrag ausdrücklich abbrechen.",
|
||||
"render.ribbon_light_rigs": "Lichtaufbauten",
|
||||
"render.ribbon_lights_help": "Lichtaufbauten prüfen oder unterstützte Lichter bearbeiten; erst ausdrücklich Anwenden ersetzt die Einrichtung.",
|
||||
"render.ribbon_materials": "Materialien",
|
||||
"render.ribbon_materials_help": "Gemeinsamen Erscheinungsinspektor und Originaloberflächen öffnen. Optik ändert keine technischen Eigenschaften stillschweigend.",
|
||||
"render.ribbon_no_active_job": "Kein aktiver Render zum Abbrechen.",
|
||||
"render.ribbon_no_result": "Vor dem Export muss ein Render abgeschlossen sein.",
|
||||
"render.ribbon_no_saved_camera": "Vor dem Wiederherstellen eine Aufnahme speichern.",
|
||||
"render.ribbon_saved_camera": "Gespeicherte Kamera",
|
||||
"render.ribbon_shots": "Aufnahmen",
|
||||
"render.ribbon_studios": "Studios",
|
||||
"render.ribbon_studios_help": "Implementierte Studios durchsuchen und Ersetzungen vor dem Anwenden prüfen. Konzepte bleiben gekennzeichnet.",
|
||||
"render.ribbon_surface_maps": "Oberflächen",
|
||||
"render.save_setup": "Setup speichern",
|
||||
"render.saved_maps_runtime_conflict": "Saved surface bindings cannot be mixed with temporary optical overrides or an unsaved HDR. Save the HDR or remove the temporary inputs explicitly.",
|
||||
"render.saved_surface_remove": "Remove saved binding",
|
||||
"render.saved_surfaces": "Saved surface bindings",
|
||||
"render.saved_surfaces_help": "Pinned built-in maps follow the selected occurrence in local millimeters. A replaced or missing target is an error, never an automatic reassignment.",
|
||||
"render.setup_name": "Name des Setups",
|
||||
"render.stage_panel_count": "Saved panels",
|
||||
"render.unavailable_preview": "Dieses Setup verlangt ein nicht verfügbares Vorschauprofil.",
|
||||
"render.unsaved": "Ungespeicherte Vorschauänderungen",
|
||||
"render.use_preview": "Integrierte Vorschau verwenden",
|
||||
@@ -1370,7 +1232,6 @@
|
||||
"thread.designation": "Bezeichnung",
|
||||
"thread.iso_metric": "ISO metrisch",
|
||||
"thread.select_size": "Größe wählen",
|
||||
"timeline.feature_hint": "Doppelklicken zum Bearbeiten · Ziehen zum Neuordnen · Rechtsklicken für weitere Optionen",
|
||||
"timeline.group": "Neuen Merkmalsordner anlegen",
|
||||
"timeline.move_many": "Zeitleistenelemente verschieben",
|
||||
"timeline.move_to_group": "Merkmale in Ordner verschieben",
|
||||
@@ -1382,10 +1243,6 @@
|
||||
"toolbar.customize_note": "Registerkarte {tab}. Markieren Sie einen Befehl, um ihm eine Schaltfläche in der Werkzeugleiste zu geben. Sie können auch eine Zeile aus dem Aufklappmenü eines Panels auf die Werkzeugleiste ziehen, eine Schaltfläche zum Entfernen aus der Werkzeugleiste ziehen oder die Stecknadel einer Menüzeile verwenden.",
|
||||
"toolbar.image": "BILD",
|
||||
"toolbar.render": "RENDERN",
|
||||
"toolbar.render_appearance": "ERSCHEINUNG",
|
||||
"toolbar.render_camera": "KAMERA",
|
||||
"toolbar.render_environment": "UMGEBUNG",
|
||||
"toolbar.render_lighting": "BELEUCHTUNG",
|
||||
"toolbar.render_scene": "SZENE",
|
||||
"trace.points": "{count} Punkte entlang der Bahn",
|
||||
"trace.stopped_by_contact": "Endet bei Probe {sample}: {a} würde {b} durchdringen",
|
||||
@@ -1584,7 +1441,7 @@
|
||||
"ui.agent_chat_pop_out": "In eigenem Fenster öffnen, das Sie auf einen anderen Bildschirm verschieben können",
|
||||
"ui.agent_chat_previous_picture": "Vorheriges Bild",
|
||||
"ui.agent_chat_privacy": "Nichts geht irgendwohin außer an den Agenten, den Sie unter Ihrem eigenen Konto ausführen.",
|
||||
"ui.agent_chat_privacy_note": "Der Chat sendet von sich aus nichts irgendwohin: Der Agent, den Sie starten, spricht unter Ihrem eigenen Konto mit seinem eigenen Dienst. SolidCraft sieht und speichert nie einen Schlüssel oder ein Passwort.",
|
||||
"ui.agent_chat_privacy_note": "SolidCraft sendet nichts irgendwohin: Der Agent, den Sie starten, spricht unter Ihrem eigenen Konto mit seinem eigenen Dienst. SolidCraft sieht und speichert nie einen Schlüssel oder ein Passwort.",
|
||||
"ui.agent_chat_queue": "Einreihen",
|
||||
"ui.agent_chat_queue_full": "Zu viele Nachrichten warten: Entfernen Sie eine oder warten Sie auf den Agenten.",
|
||||
"ui.agent_chat_queued": "Eingereiht",
|
||||
@@ -1648,7 +1505,6 @@
|
||||
"ui.ai_http_busy": "der Endpunkt ist ausgelastet oder am Limit; versuchen Sie es später",
|
||||
"ui.ai_http_key": "der Endpunkt hat den Schlüssel abgelehnt: prüfen Sie ihn unter Einstellungen > KI",
|
||||
"ui.ai_http_not_found": "Endpunkt oder Modell nicht gefunden: prüfen Sie Basis-URL und Modellnamen",
|
||||
"ui.ai_http_too_large": "die Unterhaltung ist für diesen Endpunkt zu groß; beginnen Sie einen neuen Chat",
|
||||
"ui.ai_key": "API-Schlüssel",
|
||||
"ui.ai_key_hint": "optional; im Schlüsselbund des Systems gespeichert",
|
||||
"ui.ai_key_needs_https": "Ein Schlüssel wird nur über HTTPS oder an diesen Computer gesendet.",
|
||||
@@ -1676,6 +1532,7 @@
|
||||
"ui.ai_permissions": "Berechtigungen",
|
||||
"ui.ai_preset": "Vorlage",
|
||||
"ui.ai_remove": "Entfernen",
|
||||
"ui.ai_request_limit": "nach 24 Anfragen für eine Nachricht angehalten; senden Sie eine weitere Nachricht, um fortzufahren",
|
||||
"ui.ai_reset": "Zurücksetzen",
|
||||
"ui.ai_settings": "KI-Einstellungen…",
|
||||
"ui.ai_show_features": "KI-Funktionen anzeigen",
|
||||
@@ -1769,7 +1626,6 @@
|
||||
"ui.command_done": "{command} fertig",
|
||||
"ui.command_palette_hint": "Befehl eingeben, z. B. {example} — oder S drücken, um zu suchen",
|
||||
"ui.concentric": "Konzentrisch",
|
||||
"ui.cone": "Kegel",
|
||||
"ui.conic_curve": "Kegelschnitt",
|
||||
"ui.constant": "Konstant",
|
||||
"ui.constraints_conflict": "Bedingungen konfliktreich",
|
||||
@@ -1828,11 +1684,6 @@
|
||||
"ui.draft": "Abziehen",
|
||||
"ui.drawing": "Zeichnung",
|
||||
"ui.drill_point": "Bohrpunkt",
|
||||
"ui.easyprint_consent": "EasyPrint ist der Slicer von Printables im Browser. Um das Modell dort zu öffnen, lädt SolidCraft es auf die Server von Printables hoch. Der Link läuft nach 30 Minuten ab, und Sie müssen bei Printables angemeldet sein. Fortfahren?",
|
||||
"ui.easyprint_failed": "Upload zu EasyPrint fehlgeschlagen: {error}",
|
||||
"ui.easyprint_opened": "In EasyPrint geöffnet",
|
||||
"ui.easyprint_row": "EasyPrint (Printables, im Browser)",
|
||||
"ui.easyprint_uploading": "Wird zu Printables hochgeladen …",
|
||||
"ui.edit": "Bearbeiten",
|
||||
"ui.editJointLimits": "Gelenkgrenzen bearbeiten",
|
||||
"ui.edit_section_analysis": "Querschnittsanalyse bearbeiten",
|
||||
@@ -1861,8 +1712,6 @@
|
||||
"ui.filter_commands_or_keys": "Befehle oder Tasten filtern",
|
||||
"ui.findInTimeline": "In der Zeitleiste suchen",
|
||||
"ui.findInWindow": "Im Fenster suchen",
|
||||
"ui.fine": "Fein",
|
||||
"ui.finest": "Sehr fein",
|
||||
"ui.finish_sketch": "Skizze beenden",
|
||||
"ui.fit": "Einpassen",
|
||||
"ui.fix_unfix": "Fixieren/Lösen",
|
||||
@@ -1892,7 +1741,6 @@
|
||||
"ui.heading_circular_pattern": "RADIALES MUSTER",
|
||||
"ui.heading_coil": "SCHRAUBE",
|
||||
"ui.heading_combine": "KOMBINIEREN",
|
||||
"ui.heading_cone": "KEGEL",
|
||||
"ui.heading_configure": "KONFIGURIEREN",
|
||||
"ui.heading_constraints": "RANDBEDINGUNGEN",
|
||||
"ui.heading_construct": "KONSTRUKTION",
|
||||
@@ -1952,7 +1800,6 @@
|
||||
"ui.heading_torus": "TORUS",
|
||||
"ui.heading_utilities": "WERKZEUGE",
|
||||
"ui.heading_utility": "HILFSMITTEL",
|
||||
"ui.heading_weldments": "SCHWEISSTEILE",
|
||||
"ui.help": "Hilfe",
|
||||
"ui.hide": "Ausblenden",
|
||||
"ui.hideAxes": "Achsen ausblenden",
|
||||
@@ -2047,7 +1894,6 @@
|
||||
"ui.one_side": "Einseitig",
|
||||
"ui.open": "Öffnen",
|
||||
"ui.open_ends_tip": "Berührt nichts",
|
||||
"ui.open_linked_design": "Verknüpftes Design öffnen",
|
||||
"ui.open_preferences": "Einstellungen öffnen",
|
||||
"ui.open_recent": "Zuletzt verwendet",
|
||||
"ui.operation": "Operation",
|
||||
@@ -2094,7 +1940,6 @@
|
||||
"ui.project_face_edges_sketch": "Flächenkanten in neue Skizzen projizieren",
|
||||
"ui.properties": "Eigenschaften",
|
||||
"ui.pull_direction": "Entformungsrichtung",
|
||||
"ui.quality": "Qualität",
|
||||
"ui.quantity": "Anzahl",
|
||||
"ui.quantity_2": "Anzahl 2",
|
||||
"ui.quit": "Beenden",
|
||||
@@ -2134,7 +1979,9 @@
|
||||
"ui.right": "Rechts",
|
||||
"ui.save": "Speichern",
|
||||
"ui.save_as": "Speichern unter",
|
||||
"ui.save_mesh_no_slicer": "Kein Slicer gefunden: „Benutzerdefiniert…“ wählen und das Slicer-Programm angeben, oder „An Slicer senden“ ausschalten",
|
||||
"ui.save_mesh_pick_bodies": "Wähle die Körper zum Speichern aus",
|
||||
"ui.save_mesh_temp_failed": "{folder} konnte nicht angelegt werden: {error}",
|
||||
"ui.saved_to": "Gespeichert: {path}",
|
||||
"ui.sbox_pin": "Oben im S-Feld anheften",
|
||||
"ui.sbox_unpin": "Lösen",
|
||||
@@ -2149,6 +1996,7 @@
|
||||
"ui.selection_shift_add_only": "Umschalt+Klick fügt hinzu, ohne zu entfernen",
|
||||
"ui.selection_shift_add_only_tip": "An: Umschalt fügt Elemente hinzu, Strg/Cmd schaltet um. Aus (Standard): Umschalt und Strg/Cmd schalten beide um.",
|
||||
"ui.send": "Senden",
|
||||
"ui.send_to_slicer": "An Slicer senden",
|
||||
"ui.sent_to_slicer": "An {slicer} gesendet",
|
||||
"ui.sheet_gap": "Spalt",
|
||||
"ui.shell": "Hülle",
|
||||
@@ -2193,7 +2041,6 @@
|
||||
"ui.spline": "Spline",
|
||||
"ui.split_body": "Körper trennen",
|
||||
"ui.stand_up_tip": "Nach dem Öffnen einer STEP- oder IGES-Datei mit Z nach oben eine Vierteldrehung um X anbieten",
|
||||
"ui.standard": "Standard",
|
||||
"ui.start": "Start",
|
||||
"ui.start_over": "Von vorn beginnen",
|
||||
"ui.start_page": "Startseite",
|
||||
@@ -2246,9 +2093,7 @@
|
||||
"ui.untitled": "Ohne Titel",
|
||||
"ui.up_axis_note": "Welche Achse in den Ansichten nach oben zeigt (Achsenansicht, Home, Ansehen). Designs bleiben unverändert.",
|
||||
"ui.updateToCurrentView": "Auf aktuelle Ansicht aktualisieren",
|
||||
"ui.upload": "Hochladen",
|
||||
"ui.view_centre": "Ansichtszentrum",
|
||||
"ui.viewport_theme_follow": "Dem Design folgen",
|
||||
"ui.visual_shaded_hidden_edges": "Schattiert mit verdeckten Kanten",
|
||||
"ui.visual_shaded_with_edges": "Schattiert mit Kanten",
|
||||
"ui.visual_style": "Ansichtsstil",
|
||||
@@ -2298,7 +2143,6 @@
|
||||
"update.prefs_flatpak": "Flatpak aktualisiert diese Installation ({version}): {command} ausführen oder die Softwareverwaltung nutzen. SolidCraft sucht nicht selbst.",
|
||||
"update.prefs_label": "Updates",
|
||||
"update.prefs_note": "Dies ist SolidCraft {version} ({kind}). Die Suche fragt GitHub nach dem neuesten Release und sendet sonst nichts.",
|
||||
"update.prefs_web": "Laden Sie die Seite neu, um die neueste Web-App zu erhalten. Ist das Netzwerk nicht verfügbar oder kann das Update nicht rechtzeitig vollständig heruntergeladen werden, wird die gespeicherte Version geöffnet. Ein laufender Download wird für das nächste Neuladen fortgesetzt.",
|
||||
"update.release_page": "Release-Seite öffnen",
|
||||
"update.restart": "Jetzt neu starten",
|
||||
"update.restart_later": "Später",
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
"Modeled": "Modelled",
|
||||
"cmd.construct_point_center": "Point At Centre Of Circle/Sphere/Torus",
|
||||
"cmd.inspect_center_of_mass": "Centre of Mass",
|
||||
"cmd.render_aging_describe": "Layered Surface Aging",
|
||||
"cmd.render_effects_describe": "Lens and Creative Effects",
|
||||
"cmd.sketch_arc_center_point": "Centre Point Arc",
|
||||
"cmd.sketch_circle_center": "Centre Diameter Circle",
|
||||
"cmd.sketch_rectangle_center": "Centre Rectangle",
|
||||
@@ -16,81 +14,7 @@
|
||||
"cmd.sketch_slot_center_point": "Centre Point Slot",
|
||||
"cmd.sketch_slot_center_to_center": "Centre to Centre Slot",
|
||||
"pattern.alignment_hint": "Counts include the original. Centre balances copies; Flip chooses the extra side for even totals.",
|
||||
"render.aging_add": "Add aging layer",
|
||||
"render.aging_airborne": "Airborne dust / participating medium",
|
||||
"render.aging_airborne_help": "Airborne dust is a separate finite homogeneous scattering volume. It changes transport and raw EXR, not the surface-aging stack. Existing volume/glass/denoising restrictions remain explicit; no atmosphere is added automatically.",
|
||||
"render.aging_all_bodies": "All eligible opaque bodies",
|
||||
"render.aging_chips": "Chipped paint",
|
||||
"render.aging_class_any": "All opaque material classes",
|
||||
"render.aging_class_dielectric": "Nonmetallic surfaces only",
|
||||
"render.aging_class_metal": "Metallic surfaces only",
|
||||
"render.aging_color": "Layer colour · sRGB",
|
||||
"render.aging_coverage": "Coverage",
|
||||
"render.aging_dust": "Deposited dust",
|
||||
"render.aging_empty": "No aging layers: the original surfaces remain unchanged.",
|
||||
"render.aging_fading": "Faded coating",
|
||||
"render.aging_grime": "Grime & staining",
|
||||
"render.aging_help": "Combine up to eight optical layers, in order. Rust and patina are authored appearances, not age/corrosion simulations. Scale is in world millimetres. No geometry, mass or engineering property is changed; glass and passive stages are excluded.",
|
||||
"render.aging_metallic": "Layer metallic fraction",
|
||||
"render.aging_origin": "Field origin · world mm",
|
||||
"render.aging_patina": "Copper-green patina",
|
||||
"render.aging_refresh_targets": "Inspect current bodies",
|
||||
"render.aging_roughness": "Layer roughness",
|
||||
"render.aging_rust": "Oxidized iron / rust",
|
||||
"render.aging_scale": "Pattern scale",
|
||||
"render.aging_scratches": "Fine scratches",
|
||||
"render.aging_target": "Target & coordinates",
|
||||
"render.aging_target_help": "A named target must still match its captured index and name. Global masks are world-anchored; they do not follow moving parts. This is neither cavity detection nor true edge wear. Filter the list to reach additional bodies.",
|
||||
"render.aging_target_search": "Filter body names",
|
||||
"render.aging_upward": "Upward-facing deposition",
|
||||
"render.backdrop_x": "Centre X",
|
||||
"render.backdrop_y": "Centre Y",
|
||||
"render.background_camera_section": "Camera background · PNG presentation",
|
||||
"render.background_completed_frozen": "The displayed result is a frozen completed image. Editing a background does not repaint that result automatically. Use Regrade for camera-only changes, or Render for a new physical stage.",
|
||||
"render.background_hdr_separate": "The current HDR resources illuminate and reflect on surfaces; they are not automatically displayed as a panorama behind the subject. Changing a gradient does not rotate or recolour the lighting.",
|
||||
"render.background_lighting_section": "HDR illumination · separate from camera background",
|
||||
"render.background_no_alpha_override": "A camera background is not added to transparent output silently. Raw-master regrading uses captured coverage and does not reapply denoising; the source EXR remains unchanged.",
|
||||
"render.background_regrade": "Apply to retained raw master",
|
||||
"render.background_stage_occlusion": "This shot includes physical stage geometry. A large floor or wall can fill the frame and hide a camera-only background. Clear or resize the stage deliberately, or change its physical reflectance. Regrading cannot remove captured geometry.",
|
||||
"render.background_workflow": "Choose the background you intend: a camera-only colour/gradient, a physical stage that receives light, or HDR illumination. They are independent. Live Preview is a modelling view; render a new image or regrade a retained master to see camera finishing.",
|
||||
"render.chromatic_offset": "Chromatic edge offset",
|
||||
"render.effects_gpu_only": "These effects require GPU path tracing. Changing the page never switches the renderer automatically.",
|
||||
"render.effects_help": "Combine ordered colour looks and image-space lens effects. Rendering and raw-master regrading use the same saved settings. Raw EXR stays unchanged. These are original artistic controls, not calibrated lens or branded-film emulations.",
|
||||
"render.effects_reset_lens": "Reset lens effects",
|
||||
"render.effects_seed": "Pattern seed",
|
||||
"render.film_grain": "Photographic grain",
|
||||
"render.flare_ghosts": "Ghost rings per source",
|
||||
"render.flare_halo": "Flare halo",
|
||||
"render.flare_streak": "Anamorphic-style streak",
|
||||
"render.flare_strength": "Highlight flare strength",
|
||||
"render.flare_threshold": "Scene-linear flare threshold",
|
||||
"render.layer_down": "Move down",
|
||||
"render.layer_enabled": "Layer enabled",
|
||||
"render.layer_remove": "Remove layer",
|
||||
"render.layer_strength": "Layer strength",
|
||||
"render.layer_up": "Move up",
|
||||
"render.lens_effects_help": "Flare uses up to four separated bright sources in the actual linear image. Ghosts, halo, streaks, chromatic offsets and radial distortion are artistic image-space approximations. Spatial effects need opaque output; outside samples extend the image edge. Grain is deterministic. No physics, EXR or geometry is altered.",
|
||||
"render.lens_effects_title": "Lens-inspired & photographic",
|
||||
"render.look_add": "Add colour look",
|
||||
"render.look_bleach": "Bleach-bypass-inspired",
|
||||
"render.look_cool": "Cool",
|
||||
"render.look_cyanotype": "Cyanotype-inspired",
|
||||
"render.look_faded": "Faded print",
|
||||
"render.look_mono": "Monochrome",
|
||||
"render.look_sepia": "Sepia",
|
||||
"render.look_strength": "Look strength",
|
||||
"render.look_teal_orange": "Teal / orange",
|
||||
"render.look_title": "Creative look stack",
|
||||
"render.look_warm": "Warm",
|
||||
"render.opaque_preview_help": "Opaque PBR and simple colours; blended transparency. Refraction, HDRI and final rendering are not available yet.",
|
||||
"render.page_aging": "Aging & contamination",
|
||||
"render.page_background": "Backgrounds",
|
||||
"render.page_effects": "Lens & finishing effects",
|
||||
"render.radial_distortion": "Radial distortion",
|
||||
"render.ribbon_aging": "Aging",
|
||||
"render.ribbon_effects": "Effects",
|
||||
"render.sharpening": "Detail sharpening",
|
||||
"render.soft_focus": "Soft focus",
|
||||
"sketch_text.align_center": "Centre",
|
||||
"ui.3d_mouse_step_pull": "Pull the cap towards you, hold it, then let go.",
|
||||
"ui.center": "Centre",
|
||||
|
||||
+6
-904
File diff suppressed because it is too large
Load Diff
+6
-668
@@ -72,7 +72,6 @@
|
||||
"Choose…": "Elegir…",
|
||||
"Circular": "Circular",
|
||||
"Circumference": "Circunferencia",
|
||||
"Clockwise": "Horario",
|
||||
"Close draft": "Cerrar borrador",
|
||||
"Collisions": "Colisiones",
|
||||
"Colour": "Color",
|
||||
@@ -85,7 +84,6 @@
|
||||
"Contact Sets": "Conjuntos de contacto",
|
||||
"Contact sets": "Conjuntos de contacto",
|
||||
"Continuity": "Continuidad",
|
||||
"Copies the definition only. Assign it from Physical Material when ready.": "Copia solo la definición. Asígnala desde Material físico cuando esté lista.",
|
||||
"Copy Error Message": "Copiar mensaje de error",
|
||||
"Copy Warning Message": "Copiar mensaje de advertencia",
|
||||
"Copy all licences": "Copiar todas las licencias",
|
||||
@@ -175,7 +173,6 @@
|
||||
"Home view": "Vista de inicio",
|
||||
"Horiz./Vert.": "Horiz./Vert.",
|
||||
"IMAGES": "IMÁGENES",
|
||||
"Import material into design": "Importar material al diseño",
|
||||
"Import…": "Importar…",
|
||||
"In This Design": "En este diseño",
|
||||
"In place (surfaces)": "En su posición (superficies)",
|
||||
@@ -211,7 +208,6 @@
|
||||
"Mass Center": "Centro de masas",
|
||||
"Matched restrictions check the final values.": "Las restricciones que se cumplen comprueban los valores finales.",
|
||||
"Material": "Material",
|
||||
"Material copied. Existing body assignments are unchanged.": "Material copiado. Se conservan las asignaciones actuales de los cuerpos.",
|
||||
"Max": "Máx.",
|
||||
"Mid Radius": "Radio intermedio",
|
||||
"Middle double-click": "Doble clic central",
|
||||
@@ -228,7 +224,6 @@
|
||||
"Move later": "Mover después",
|
||||
"Move up": "Mover arriba",
|
||||
"Name": "Nombre",
|
||||
"Name on import": "Nombre al importar",
|
||||
"Nearest": "Más cercano",
|
||||
"New": "Nuevo",
|
||||
"New Comp.": "Nuevo comp.",
|
||||
@@ -238,7 +233,6 @@
|
||||
"No crash reports.": "No hay informes de fallos.",
|
||||
"No matching appearance presets.": "No hay preajustes de apariencia coincidentes.",
|
||||
"No matching appearances": "No hay apariencias coincidentes",
|
||||
"No matching physical materials.": "No hay materiales físicos coincidentes.",
|
||||
"No visible bodies or sketches to export.": "No hay cuerpos ni bocetos visibles para exportar.",
|
||||
"None": "Ninguno",
|
||||
"Not available yet": "Todavía no disponible",
|
||||
@@ -260,7 +254,6 @@
|
||||
"Path": "Trayectoria",
|
||||
"Pattern": "Patrón",
|
||||
"Per Edge": "Por arista",
|
||||
"Physical material definitions": "Definiciones de materiales físicos",
|
||||
"Pick the tool bodies": "Selecciona los cuerpos herramienta",
|
||||
"Picked": "Seleccionado",
|
||||
"Picked point": "Punto seleccionado",
|
||||
@@ -329,7 +322,6 @@
|
||||
"Search commands": "Buscar comandos",
|
||||
"Search this library": "Buscar en esta biblioteca",
|
||||
"Section": "Sección",
|
||||
"Section position": "Posición de la sección",
|
||||
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Seleccionar otro: mantén pulsado el botón izquierdo sobre el modelo (o haz clic derecho › Seleccionar otro…) para ver todo lo que hay bajo el cursor; vuelve a pulsar en el mismo lugar para seleccionar el siguiente elemento. Haz doble clic con el botón central para volver a Inicio (Preferencias › Navegación).",
|
||||
"Select bodies (a face picks its body)": "Selecciona cuerpos (al seleccionar una cara se selecciona su cuerpo)",
|
||||
"Select bodies or faces instead": "Selecciona cuerpos o caras en su lugar",
|
||||
@@ -341,7 +333,6 @@
|
||||
"Show Agent Cursor": "Mostrar cursor del agente",
|
||||
"Show in folder": "Mostrar en carpeta",
|
||||
"Show the pointer an AI agent moves (#35)": "Mostrar el puntero que mueve un agente de IA (#35)",
|
||||
"Show tool at cursor": "Mostrar la herramienta junto al cursor",
|
||||
"Signed axes": "Ejes con signo",
|
||||
"Size": "Tamaño",
|
||||
"Sketch constraints": "Restricciones del boceto",
|
||||
@@ -354,11 +345,9 @@
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft se cerró antes de guardar estos diseños. Puedes abrir su último guardado automático.",
|
||||
"Specialise": "Especializar",
|
||||
"Speed": "Velocidad",
|
||||
"Square": "Cuadrado",
|
||||
"Stand it up": "Poner en vertical",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Poner en vertical: un cuarto de vuelta alrededor del eje X, como una única operación Mover que puedes deshacer.",
|
||||
"Stand this part up?": "¿Poner esta pieza en vertical?",
|
||||
"Start angle": "Ángulo inicial",
|
||||
"Start bulge": "Abombamiento inicial",
|
||||
"Start continuity": "Continuidad inicial",
|
||||
"Step": "Paso",
|
||||
@@ -423,7 +412,6 @@
|
||||
"angle, height": "ángulo, altura",
|
||||
"appearance.dielectric_ior": "Índice de refracción dieléctrico",
|
||||
"appearance.metallic": "Metalicidad",
|
||||
"appearance.only_available": "Solo apariencias disponibles",
|
||||
"appearance.pbr_help": "Visualiza esta superficie en Renderizado o en un PNG del producto. Sin refracción ni transmisión.",
|
||||
"appearance.pbr_opaque": "PBR opaco",
|
||||
"appearance.roughness": "Rugosidad",
|
||||
@@ -463,7 +451,6 @@
|
||||
"cmd.canvas_delete": "Eliminar lienzo",
|
||||
"cmd.canvas_edit": "Editar lienzo",
|
||||
"cmd.canvas_insert": "Lienzo",
|
||||
"cmd.canvas_to_sketch": "Boceto desde lienzo",
|
||||
"cmd.component_activate": "Activar componente",
|
||||
"cmd.component_break_link": "Romper vínculo",
|
||||
"cmd.component_capture_position": "Capturar posición",
|
||||
@@ -541,7 +528,6 @@
|
||||
"cmd.file_export": "Exportar",
|
||||
"cmd.file_insert_mesh": "Insertar malla",
|
||||
"cmd.file_insert_step": "Insertar STEP",
|
||||
"cmd.file_open_in_easyprint": "Abrir con EasyPrint",
|
||||
"cmd.file_save_mesh": "Guardar como malla",
|
||||
"cmd.file_slicers": "Laminadores instalados",
|
||||
"cmd.frame_member": "Miembro estructural",
|
||||
@@ -587,7 +573,6 @@
|
||||
"cmd.material_create": "Nuevo material físico",
|
||||
"cmd.material_delete": "Eliminar material físico",
|
||||
"cmd.material_edit": "Editar material físico",
|
||||
"cmd.material_inspect": "Inspeccionar material físico",
|
||||
"cmd.material_list": "Listar materiales",
|
||||
"cmd.material_properties_directional": "Módulo elástico direccional",
|
||||
"cmd.material_properties_validate": "Validar datos del material físico",
|
||||
@@ -633,45 +618,14 @@
|
||||
"cmd.plastic_rest": "Apoyo",
|
||||
"cmd.plastic_rules": "Listar reglas de plástico",
|
||||
"cmd.plastic_snap_fit": "Encaje a presión",
|
||||
"cmd.render_aging_describe": "Envejecimiento por capas",
|
||||
"cmd.render_backends": "Motores de renderizado",
|
||||
"cmd.render_camera_fit": "Encuadrar sujeto de render",
|
||||
"cmd.render_camera_get": "Inspeccionar cámara de render",
|
||||
"cmd.render_cancel": "Cancelar renderizado",
|
||||
"cmd.render_effects_describe": "Efectos de lente y creativos",
|
||||
"cmd.render_export": "Exportar imagen renderizada",
|
||||
"cmd.render_hdr_inspect": "Inspeccionar fuente HDR",
|
||||
"cmd.render_library_appearance_apply": "Aplicar aspecto opaco de la biblioteca",
|
||||
"cmd.render_library_describe": "Inspeccionar recurso de renderizado",
|
||||
"cmd.render_library_environment_apply": "Aplicar entorno HDR incluido",
|
||||
"cmd.render_library_list": "Explorar la biblioteca de renderizado",
|
||||
"cmd.render_library_physical_prepare": "Prepare Physical Reference",
|
||||
"cmd.render_library_preview": "Inspect Original Surface Map",
|
||||
"cmd.render_library_studio_apply": "Aplicar iluminación de estudio",
|
||||
"cmd.render_library_studio_prepare": "Preparar iluminación de estudio",
|
||||
"cmd.render_library_surface_bind": "Save Surface Binding",
|
||||
"cmd.render_library_surface_prepare": "Preparar mapas de superficie",
|
||||
"cmd.render_library_surface_unbind": "Remove Surface Binding",
|
||||
"cmd.render_library_targets": "Buscar destinos de recursos de renderizado",
|
||||
"cmd.render_optics_bind": "Guardar asignación óptica",
|
||||
"cmd.render_optics_unbind": "Quitar asignación óptica",
|
||||
"cmd.render_preflight": "Comprobar solicitud de renderizado",
|
||||
"cmd.render_preset_apply": "Apply Native Studio",
|
||||
"cmd.render_preset_resolve": "Review Native Studio",
|
||||
"cmd.render_quality_prepare": "Review Fixed Sampling Budget",
|
||||
"cmd.render_result_pin": "Conservar el resultado mostrado",
|
||||
"cmd.render_result_regrade": "Reeditar máster lineal",
|
||||
"cmd.render_scene": "Inspeccionar escena de renderizado",
|
||||
"cmd.render_settings_apply": "Aplicar cámara de renderizado",
|
||||
"cmd.render_settings_capture_hdr": "Guardar configuración de renderizado con HDR",
|
||||
"cmd.render_settings_get": "Ajustes de renderizado",
|
||||
"cmd.render_settings_set": "Guardar ajustes de renderizado",
|
||||
"cmd.render_shot_duplicate": "Duplicate Render Shot",
|
||||
"cmd.render_shot_remove": "Remove Render Shot",
|
||||
"cmd.render_shot_rename": "Rename Render Shot",
|
||||
"cmd.render_stage_backdrop": "Guardar fondo físico de render",
|
||||
"cmd.render_stage_clear": "Remove Render Stage",
|
||||
"cmd.render_stage_ground": "Save Render Ground",
|
||||
"cmd.render_start": "Renderizar imagen",
|
||||
"cmd.render_status": "Progreso del renderizado",
|
||||
"cmd.select_clear": "Borrar selección",
|
||||
@@ -998,49 +952,6 @@
|
||||
"manage.material_hint": "Seleccione cuerpos y elija su material físico.",
|
||||
"manage.parameters_hint": "Edite expresiones y unidades. Los cambios se aplican al pulsar Intro o salir de un campo.",
|
||||
"manage.rules_hint": "Elija una regla para editarla o cree una nueva. Confirme para guardar la regla.",
|
||||
"material.editor.add_property": "Añadir propiedad",
|
||||
"material.editor.add_source": "Añadir fuente",
|
||||
"material.editor.assigned_density": "Densidad asignada",
|
||||
"material.editor.axes": "Ejes del material",
|
||||
"material.editor.axes_numeric": "1 / 2 / 3",
|
||||
"material.editor.axes_wood": "L / R / T",
|
||||
"material.editor.copy_name": "Copia de {name}",
|
||||
"material.editor.density_range": "La densidad asignada debe ser mayor que 0 y no superar 1000 g/cm³.",
|
||||
"material.editor.designation": "Designación",
|
||||
"material.editor.duplicate": "Duplicar",
|
||||
"material.editor.engineering_card": "Ficha de propiedades físicas",
|
||||
"material.editor.family": "Familia del material",
|
||||
"material.editor.glass": "Vidrio",
|
||||
"material.editor.illustrative": "Ilustrativa",
|
||||
"material.editor.invalid_property": "Tipo de propiedad no admitido.",
|
||||
"material.editor.isotropic": "Isótropo",
|
||||
"material.editor.measurement": "Medición",
|
||||
"material.editor.metal": "Metal",
|
||||
"material.editor.moisture": "Humedad en base seca",
|
||||
"material.editor.name_required": "Introduce un nombre de 1 a 128 caracteres.",
|
||||
"material.editor.no_draft": "No hay ningún borrador de material abierto.",
|
||||
"material.editor.number_required": "Introduce un valor numérico finito.",
|
||||
"material.editor.orthotropic": "Ortótropo",
|
||||
"material.editor.other": "Otra",
|
||||
"material.editor.polymer": "Polímero",
|
||||
"material.editor.preserved_appearance": "Apariencia conservada",
|
||||
"material.editor.public_reference": "Referencia pública",
|
||||
"material.editor.reference": "Referencia",
|
||||
"material.editor.save_or_discard": "Guarda o descarta el borrador antes de asignar un material.",
|
||||
"material.editor.saved": "Material guardado. Las asignaciones a cuerpos no han cambiado.",
|
||||
"material.editor.scope": "Guardar almacena este material sin asignarlo a cuerpos. Las propiedades y condiciones vacías siguen siendo desconocidas. La densidad de la ficha no cambia la densidad asignada ni la masa.",
|
||||
"material.editor.source_id": "ID de la fuente",
|
||||
"material.editor.source_kind": "Tipo de fuente",
|
||||
"material.editor.source_title": "Título de la fuente",
|
||||
"material.editor.sources": "Fuentes",
|
||||
"material.editor.stale": "Este material cambió mientras el borrador estaba abierto. Descártalo y vuelve a abrirlo antes de guardar.",
|
||||
"material.editor.state": "Estado del material",
|
||||
"material.editor.strain_rate": "Velocidad de deformación",
|
||||
"material.editor.symmetry": "Simetría elástica",
|
||||
"material.editor.temperature": "Temperatura",
|
||||
"material.editor.unknown_card": "Las propiedades físicas son desconocidas. Puedes guardar solo la densidad asignada y la apariencia existente.",
|
||||
"material.editor.user_supplied": "Aportada por la usuaria",
|
||||
"material.editor.wood": "Madera",
|
||||
"measure.at": "En",
|
||||
"measure.item": "Elemento {number}",
|
||||
"messages.ago_h": "hace {n} h",
|
||||
@@ -1059,7 +970,6 @@
|
||||
"motion.key_here": "Insertar fotograma clave aquí",
|
||||
"motion.key_here_hint": "Añadir un fotograma clave en este paso con el valor actual de la unión",
|
||||
"motion.no_collisions": "Sin colisiones durante el estudio",
|
||||
"motion.plot_quantity": "Cantidad",
|
||||
"motion.step_back": "Paso atrás",
|
||||
"motion.step_forward": "Paso adelante",
|
||||
"motion.to_end": "Al final",
|
||||
@@ -1096,248 +1006,21 @@
|
||||
"recovery.just_now": "justo ahora",
|
||||
"recovery.min_ago": "hace {count} min",
|
||||
"recovery.never_saved": "nunca guardado",
|
||||
"render.adaptive_absolute": "Umbral de error RGB absoluto",
|
||||
"render.adaptive_coverage": "Objetivo de error de cobertura",
|
||||
"render.adaptive_enable": "Asignar muestras con diagnóstico independiente",
|
||||
"render.adaptive_help": "Solo GPU. Se descartan las muestras de diagnóstico independientes y se asignan al menos 16 muestras finales por píxel, hasta el límite Muestras por píxel. La varianza RGB y de cobertura orienta la asignación; pueden omitirse trayectorias raras y quedar ruido al alcanzar el límite. El informe cuenta el trabajo real, sin garantía de ruido ni tiempo.",
|
||||
"render.adaptive_minimum": "Mínimo de muestras finales",
|
||||
"render.adaptive_pending": "Disponibles en GPU la asignación adaptativa con diagnóstico independiente y los estratos de Fresnel. Siguen pendientes la convergencia certificada, las garantías de eventos raros y el transporte adaptativo CPU.",
|
||||
"render.adaptive_pilots": "Muestras de diagnóstico por píxel",
|
||||
"render.adaptive_relative": "Objetivo de error RGB relativo",
|
||||
"render.add_ground": "Add fitted ground to setup",
|
||||
"render.advanced_material_scope": "Disponible un medio homogéneo GPU finito con límites explícitos. Siguen pendientes las rejillas de densidad, vidrio y volumen combinados, superficies subsuperficiales, desplazamiento, transporte espectral y conceptos avanzados sin implementar.",
|
||||
"render.aging_add": "Añadir capa de envejecimiento",
|
||||
"render.aging_airborne": "Polvo en suspensión / medio participante",
|
||||
"render.aging_airborne_help": "El polvo suspendido es un volumen homogéneo finito e independiente. Cambia el transporte y el EXR, no la pila de superficies. Se mantienen las restricciones de volumen, vidrio y reducción de ruido; nunca se añade una atmósfera automáticamente.",
|
||||
"render.aging_all_bodies": "Todos los cuerpos opacos aptos",
|
||||
"render.aging_chips": "Pintura desconchada",
|
||||
"render.aging_class_any": "Todas las clases opacas",
|
||||
"render.aging_class_dielectric": "Solo superficies no metálicas",
|
||||
"render.aging_class_metal": "Solo superficies metálicas",
|
||||
"render.aging_color": "Color de capa · sRGB",
|
||||
"render.aging_coverage": "Cobertura",
|
||||
"render.aging_dust": "Polvo depositado",
|
||||
"render.aging_empty": "Sin capas: se conservan las superficies originales.",
|
||||
"render.aging_fading": "Recubrimiento descolorido",
|
||||
"render.aging_grime": "Suciedad y manchas",
|
||||
"render.aging_help": "Combina hasta ocho capas ópticas, en orden. Óxido y pátina son apariencias, no simulaciones de edad o corrosión. La escala usa milímetros globales. No cambian geometría, masa ni propiedades de ingeniería; vidrio y escenario quedan excluidos.",
|
||||
"render.aging_metallic": "Fracción metálica de capa",
|
||||
"render.aging_origin": "Origen del campo · mm globales",
|
||||
"render.aging_patina": "Pátina verde de cobre",
|
||||
"render.aging_refresh_targets": "Inspeccionar cuerpos actuales",
|
||||
"render.aging_roughness": "Rugosidad de capa",
|
||||
"render.aging_rust": "Óxido de hierro",
|
||||
"render.aging_scale": "Escala del patrón",
|
||||
"render.aging_scratches": "Arañazos finos",
|
||||
"render.aging_target": "Destino y coordenadas",
|
||||
"render.aging_target_help": "El destino debe conservar el índice y nombre capturados. Las máscaras se anclan al mundo, no siguen piezas móviles. No detectan cavidades ni desgaste geométrico de aristas. Filtra la lista para encontrar otros cuerpos.",
|
||||
"render.aging_target_search": "Filtrar nombres de cuerpos",
|
||||
"render.aging_upward": "Depósito en caras superiores",
|
||||
"render.all_controls_help": "Inspección de todos los controles. Las secciones permiten editar por contexto sin cambiar la petición subyacente.",
|
||||
"render.appearance_physics_separate": "El aspecto visual no asigna densidad, propiedades térmicas ni otros datos de ingeniería.",
|
||||
"render.availability_native": "Composición nativa; revisar antes de aplicar",
|
||||
"render.availability_projection": "Proyección compatible; consulta alcance y omisiones",
|
||||
"render.availability_reference": "Datos de referencia; no es una asignación automática",
|
||||
"render.backdrop_alpha_conflict": "Elige el fondo o la salida PNG transparente; el renderizador no ignorará ninguna opción.",
|
||||
"render.backdrop_enable": "Usar fondo degradado solo para la cámara",
|
||||
"render.backdrop_first": "Color inicial / central",
|
||||
"render.backdrop_help": "Este fondo de pantalla no ilumina, proyecta sombras, refleja ni representa terreno. Se excluye del EXR sin procesar. Geometría, luces y entorno HDR son independientes.",
|
||||
"render.backdrop_horizontal": "Degradado horizontal",
|
||||
"render.backdrop_radial": "Degradado radial",
|
||||
"render.backdrop_radius": "Radio",
|
||||
"render.backdrop_second": "Color final / del borde",
|
||||
"render.backdrop_vertical": "Degradado vertical",
|
||||
"render.backdrop_x": "Centro X",
|
||||
"render.backdrop_y": "Centro Y",
|
||||
"render.background": "Fondo de la escena",
|
||||
"render.background_camera_section": "Fondo de cámara · presentación PNG",
|
||||
"render.background_completed_frozen": "El resultado mostrado es una imagen terminada e inmutable. Editar el fondo no repinta automáticamente ese resultado. Usa Reeditar para cambios de cámara o Renderizar para un escenario físico nuevo.",
|
||||
"render.background_hdr_separate": "Los HDR actuales iluminan y se reflejan en las superficies; no se muestran automáticamente como una panorámica detrás del objeto. Cambiar el degradado no gira ni recolorea la iluminación.",
|
||||
"render.background_lighting_section": "Iluminación HDR · independiente del fondo de cámara",
|
||||
"render.background_no_alpha_override": "El fondo de cámara no se añade silenciosamente a una salida transparente. La reedición usa la cobertura capturada y no vuelve a reducir ruido; el EXR original se conserva.",
|
||||
"render.background_not_light": "El color de fondo es presentación, no una fuente de luz adicional.",
|
||||
"render.background_regrade": "Aplicar al máster bruto retenido",
|
||||
"render.background_stage_occlusion": "Esta toma incluye geometría de escenario. Un suelo o pared grandes pueden llenar el encuadre y ocultar el fondo de cámara. Elimina o ajusta el escenario de forma explícita, o cambia su reflectancia. Reeditar no elimina geometría capturada.",
|
||||
"render.background_workflow": "Elige el tipo de fondo: color/degradado solo para la cámara, escenario físico que recibe luz o iluminación HDR. Son independientes. Vista previa es una vista de modelado; renderiza o reedita un máster retenido para ver el acabado de cámara.",
|
||||
"render.base_appearance": "Aspecto editable del cuerpo",
|
||||
"render.base_apply": "Aplicar aspecto al cuerpo",
|
||||
"render.base_apply_help": "Asignación visual deshacible al cuerpo exacto revisado. Los aspectos por cara y materiales físicos permanecen separados.",
|
||||
"render.base_color": "Color base · sRGB",
|
||||
"render.base_metallic": "Fracción metálica",
|
||||
"render.base_opaque_only": "Este editor conserva la semántica opaca. Utiliza los controles de vidrio para transmisión óptica.",
|
||||
"render.bloom_enable": "Resplandor de presentación",
|
||||
"render.bloom_help": "Resplandor creativo del resultado a partir de altas luces lineales, extendido con tres pasadas normalizadas. Radio en píxeles de salida. Se añade en luz lineal de pantalla antes de sRGB; no simula lentes ni volúmenes ni mejora la convergencia. El EXR original no cambia.",
|
||||
"render.bloom_opaque": "El resplandor requiere salida opaca. Se rechaza el PNG transparente, sin aplanarlo silenciosamente.",
|
||||
"render.bloom_radius": "Radio del resplandor",
|
||||
"render.bloom_strength": "Intensidad del resplandor",
|
||||
"render.bloom_threshold": "Umbral lineal de altas luces",
|
||||
"render.browse_capabilities": "Consultar catálogo completo de capacidades",
|
||||
"render.browse_environments": "Explorar entornos HDR originales",
|
||||
"render.browse_native_scenes": "Explorar estudios nativos",
|
||||
"render.browse_physical": "Consultar referencias físicas",
|
||||
"render.browse_quality": "Consultar especificaciones de calidad",
|
||||
"render.browse_rigs": "Explorar conjuntos de luces",
|
||||
"render.browse_scene_specs": "Explorar especificaciones de escenas avanzadas",
|
||||
"render.browse_surfaces": "Explorar superficies originales",
|
||||
"render.browser_library": "Biblioteca",
|
||||
"render.browser_model": "Modelo",
|
||||
"render.camera_distance": "Distancia de la cámara al objetivo",
|
||||
"render.camera_exact_help": "Milímetros del mundo; ángulos mostrados en grados y guardados en radianes. La navegación utiliza la misma cámara que el visor.",
|
||||
"render.camera_fov": "Campo de visión vertical",
|
||||
"render.camera_pitch": "Inclinación de cámara",
|
||||
"render.camera_roll": "Giro",
|
||||
"render.camera_set": "Establecer cámara de render",
|
||||
"render.camera_target": "Posición del objetivo · mm",
|
||||
"render.camera_yaw": "Azimut",
|
||||
"render.chromatic_offset": "Separación cromática",
|
||||
"render.clear_stage": "Remove saved stage",
|
||||
"render.copy_request": "Copiar JSON",
|
||||
"render.cpu_aa_help": "Más muestras suavizan los bordes y aumentan el tiempo de renderizado.",
|
||||
"render.cpu_area_add": "Añadir luz rectangular CPU",
|
||||
"render.cpu_area_editor": "Luces rectangulares CPU",
|
||||
"render.cpu_area_editor_help": "Vectores de lado completos en milímetros; U por V señala la cara emisora. No son los semilados de las luces GPU.",
|
||||
"render.cpu_area_full_u": "Lado U completo · mm",
|
||||
"render.cpu_area_full_v": "Lado V completo · mm",
|
||||
"render.cpu_area_samples": "Muestras por luz de área",
|
||||
"render.cpu_binding_confirm": "Confirmar este cuerpo",
|
||||
"render.cpu_binding_stale": "La escena ha cambiado. Confirma de nuevo este cuerpo de los reflejos antes de renderizar.",
|
||||
"render.cpu_body_choose": "Elegir un cuerpo",
|
||||
"render.cpu_body_filter": "Filtrar cuerpos por nombre",
|
||||
"render.cpu_body_filter_more": "Afina el filtro para ver más cuerpos.",
|
||||
"render.cpu_body_missing": "Un cuerpo de los reflejos falta, está oculto, es ambiguo o su nombre supera 128 bytes. Vuelve a elegirlo.",
|
||||
"render.cpu_budget": "Perfil de recursos de CPU",
|
||||
"render.cpu_budget_bodies": "Máximo de cuerpos",
|
||||
"render.cpu_budget_compact": "Compacto (límites anteriores)",
|
||||
"render.cpu_budget_details": "Límites de recursos",
|
||||
"render.cpu_budget_help": "Selecciona límites mayores de forma explícita. La resolución y las muestras nunca se reducen automáticamente.",
|
||||
"render.cpu_budget_memory": "Memoria máxima del trabajo",
|
||||
"render.cpu_budget_not_guarantee": "Son límites de admisión, no una reserva de RAM ni una garantía de velocidad. Geometría, acumulación y vistas previas comparten el presupuesto de memoria; el renderizado se puede cancelar.",
|
||||
"render.cpu_budget_resize": "La configuración supera este perfil de CPU o su presupuesto de muestras. Acepta unas dimensiones menores de forma explícita o selecciona un perfil superior.",
|
||||
"render.cpu_budget_studio": "Estudio (imágenes grandes)",
|
||||
"render.cpu_budget_triangles": "Máximo de triángulos",
|
||||
"render.cpu_budget_workstation": "Estación de trabajo (escenas pesadas)",
|
||||
"render.cpu_captured_hdr": "HDR capturado para esta imagen",
|
||||
"render.cpu_check_capture_help": "Comprobar e Iniciar verifican la misma huella del archivo o usan el HDR del diseño. Cada imagen conserva su fuente capturada.",
|
||||
"render.cpu_check_changed": "Los controles o el modelo han cambiado desde esta comprobación. Comprueba de nuevo antes de renderizar.",
|
||||
"render.cpu_constant_help": "La radiancia constante llega a los rayos reflejados o transmitidos. No aporta iluminación difusa directa.",
|
||||
"render.cpu_custom_directions": "Sustituir el preajuste por luces direccionales",
|
||||
"render.cpu_direction": "Dirección hacia la luz (X, Y, Z)",
|
||||
"render.cpu_directional": "Luz direccional",
|
||||
"render.cpu_directional_add": "Añadir luz direccional",
|
||||
"render.cpu_environment": "Entorno CPU",
|
||||
"render.cpu_environment_background": "El fondo de la imagen se mantiene separado de la iluminación del entorno.",
|
||||
"render.cpu_environment_bounds": "La intensidad CPU debe ser 0–64, el giro HDR finito dentro de ±1 000 000 radianes y las muestras HDR 1, 4, 16 o 64.",
|
||||
"render.cpu_environment_constant": "Entorno constante para rayos secundarios",
|
||||
"render.cpu_environment_none": "Sin entorno CPU",
|
||||
"render.cpu_environment_samples": "Muestras de iluminación HDR",
|
||||
"render.cpu_exposure_help": "La exposición CPU debe estar entre −16 y 16 EV. Una exposición GPU fuera de ese rango se conserva hasta que la cambies.",
|
||||
"render.cpu_fixed": "Imagen fija",
|
||||
"render.cpu_hdr_help": "Radiance .hdr, panorama 2:1, hasta 2048 × 1024 y 16 MiB. La CPU exige primarias sRGB/D65 explícitas en el archivo; rechaza primarias sin definir y EXR.",
|
||||
"render.cpu_hdr_reflection_help": "Los reflejos rugosos con HDR requieren superficies totalmente metálicas en el cuerpo y sus caras. Comprobar indica las combinaciones no admitidas.",
|
||||
"render.cpu_key_rim": "Principal y contorno",
|
||||
"render.cpu_last_check": "Última comprobación CPU",
|
||||
"render.cpu_light_help": "Hasta ocho luces direccionales. Una lista personalizada vacía las apaga; elegir un preajuste restaura sus luces. El color es RGB lineal y la intensidad es relativa, no vatios.",
|
||||
"render.cpu_light_intensity": "Intensidad relativa (0–64)",
|
||||
"render.cpu_light_minimum": "Distancia mínima de atenuación",
|
||||
"render.cpu_light_position": "Posición en el mundo (X, Y, Z)",
|
||||
"render.cpu_light_reference": "Distancia de referencia de la intensidad",
|
||||
"render.cpu_light_remove": "Quitar luz",
|
||||
"render.cpu_lighting": "Preajuste de iluminación",
|
||||
"render.cpu_lights": "Luces CPU",
|
||||
"render.cpu_linear_color": "Color lineal (R, G, B)",
|
||||
"render.cpu_neutral": "Neutro",
|
||||
"render.cpu_opaque_required": "El trazado CPU requiere un fondo opaco. Desactiva Fondo transparente para continuar.",
|
||||
"render.cpu_options_pending": "Este panel CPU todavía no puede editar emisión, efectos de superficie, vidrio ni anisotropía. Borra esas opciones de la API antes de usarlo.",
|
||||
"render.cpu_point": "Luz puntual",
|
||||
"render.cpu_point_add": "Añadir luz puntual",
|
||||
"render.cpu_point_help": "Hasta ocho luces puntuales, añadidas a la iluminación direccional elegida. Las distancias están en mm; la mínima no puede superar la de referencia.",
|
||||
"render.cpu_primary_samples": "Muestras de borde por píxel",
|
||||
"render.cpu_progressive": "CPU progresivo",
|
||||
"render.cpu_progressive_help": "Acumula el integrador CPU actual en pasadas de una muestra primaria. Los reflejos seleccionados requieren una muestra; luces de área, dieléctricos y anisotropía requieren Imagen fija.",
|
||||
"render.cpu_progressive_limits": "Máximo de 1.048.576 píxeles y 16.777.216 evaluaciones de píxel × muestra. Cambiar las muestras nunca redimensiona la imagen automáticamente.",
|
||||
"render.cpu_progressive_seed": "Semilla de muestreo CPU",
|
||||
"render.cpu_progressive_spp": "muestras por píxel",
|
||||
"render.cpu_reflection_add": "Añadir reflejo rugoso",
|
||||
"render.cpu_reflection_depth": "Profundidad de reflejos (1–8)",
|
||||
"render.cpu_reflection_empty": "Sin cuerpos seleccionados: los reflejos de la escena están desactivados.",
|
||||
"render.cpu_reflection_help": "Elige cuerpos y selecciona Desactivado, Espejo o Rugoso. Rugoso usa la rugosidad asignada al material. Más muestras y rebotes aumentan el tiempo; el transporte especular es acotado y no calcula iluminación global.",
|
||||
"render.cpu_reflection_limit": "Se admiten hasta 64 cuerpos con reflejos.",
|
||||
"render.cpu_reflection_mirror": "Espejo",
|
||||
"render.cpu_reflection_off": "Desactivado",
|
||||
"render.cpu_reflection_rough": "Rugoso",
|
||||
"render.cpu_reflection_samples": "Muestras de reflejos (1–64)",
|
||||
"render.cpu_reflections": "Reflejos CPU",
|
||||
"render.cpu_runtime_help": "Los ajustes numéricos de luces y entorno se guardan con la configuración. Los reflejos por cuerpo y los archivos HDR siguen siendo temporales. Se conservan los valores existentes de luces de área y conos de foco.",
|
||||
"render.cpu_sampling_mode": "Modo de muestreo CPU",
|
||||
"render.cpu_shadows": "Proyectar sombras",
|
||||
"render.cpu_summary": "Trazado CPU · reflejos · HDR · luces direccionales y puntuales",
|
||||
"render.cpu_warm_cool": "Cálido y frío",
|
||||
"render.diag_bodies": "Cuerpos visibles",
|
||||
"render.diag_job_details": "Detalles del trabajo capturado",
|
||||
"render.diag_last_check": "Última comprobación",
|
||||
"render.diag_memory": "Estimación lógica de memoria",
|
||||
"render.diag_not_implemented": "Límites explícitos de capacidades",
|
||||
"render.diag_preflight_details": "Detalles de la comprobación",
|
||||
"render.diag_request": "Petición actual exacta",
|
||||
"render.diag_triangles": "Triángulos",
|
||||
"render.discard_continue": "Descartar borrador y continuar",
|
||||
"render.draft_confirm_help": "El borrador contiene cambios pendientes. Guárdalos antes de cambiar, descártalos explícitamente o sigue editando.",
|
||||
"render.draft_confirm_title": "Conservar los ajustes editados",
|
||||
"render.effects_gpu_only": "Estos efectos requieren trazado GPU. Cambiar de pestaña no cambia automáticamente el motor.",
|
||||
"render.effects_help": "Combina filtros de color ordenados y efectos de lente en imagen. Render y reedición del máster usan los mismos ajustes guardados. El EXR bruto no cambia. Son controles artísticos originales, no emulaciones calibradas de lentes o películas comerciales.",
|
||||
"render.effects_reset_lens": "Restablecer efectos de lente",
|
||||
"render.effects_seed": "Semilla del patrón",
|
||||
"render.export_display": "Exportar: PNG de visualización",
|
||||
"render.export_linear": "Exportar: EXR lineal",
|
||||
"render.exr_pending": "La GPU puede conservar EXR lineal original. La compresión, las imágenes multicapa y los pases auxiliares siguen sin estar disponibles.",
|
||||
"render.film_grain": "Grano fotográfico",
|
||||
"render.fit": "Ajustar producto",
|
||||
"render.flare_ghosts": "Anillos por fuente",
|
||||
"render.flare_halo": "Halo de flare",
|
||||
"render.flare_streak": "Estela estilo anamórfico",
|
||||
"render.flare_strength": "Intensidad del flare",
|
||||
"render.flare_threshold": "Umbral lineal del flare",
|
||||
"render.gpu_depth": "Profundidad máxima del trazado",
|
||||
"render.gpu_options_help": "Las muestras, la profundidad, la semilla y la exposición se guardan con esta configuración. Solo se guarda el método de imagen activo.",
|
||||
"render.gpu_path_failed": "Trazado GPU no disponible",
|
||||
"render.gpu_path_first_setup": "La primera preparación en esta GPU puede tardar varios minutos; puedes seguir usando SolidCraft. Los siguientes inicios reutilizan los sombreadores compilados cuando el controlador los conserva.",
|
||||
"render.gpu_path_preparing": "Trazado GPU: compilando sombreadores…",
|
||||
"render.gpu_seed": "Semilla aleatoria",
|
||||
"render.gpu_unavailable": "El trazado GPU aún no está disponible en este dispositivo.",
|
||||
"render.ground_apply": "Ajustar y guardar suelo",
|
||||
"render.ground_gap": "Separación geométrica explícita",
|
||||
"render.ground_linear_rgb": "Reflectancia difusa · RGB lineal",
|
||||
"render.ground_margin": "Margen de extensión del suelo",
|
||||
"render.hdr_details": "Detalles de la fuente HDR",
|
||||
"render.hdr_embedded": "HDR guardado dentro del diseño",
|
||||
"render.hdr_embedded_help": "El renderizado usa los bytes incluidos en el diseño; el archivo original ya no es necesario.",
|
||||
"render.hdr_file_captured": "Archivo HDR comprobado. Guardar incluirá estos bytes; si el archivo cambia, compruébalo de nuevo explícitamente.",
|
||||
"render.hdr_inspect": "Comprobar archivo HDR",
|
||||
"render.hdr_inspect_required": "Selecciona y comprueba un archivo HDR antes de guardar o renderizar.",
|
||||
"render.hdr_primaries_declared": "La fuente declara sus primarias de color.",
|
||||
"render.hdr_primaries_srgb": "Primarias de color: sRGB/D65",
|
||||
"render.hdr_primaries_undeclared": "La fuente no declara primarias; revisa los requisitos de color del motor.",
|
||||
"render.hdr_resource_missing": "El recurso HDR guardado no existe en este diseño.",
|
||||
"render.image_advanced": "Opciones avanzadas de GPU",
|
||||
"render.image_cancelled": "Renderizado cancelado",
|
||||
"render.image_cancelling": "Cancelando",
|
||||
"render.image_completed": "Imagen lista para exportar",
|
||||
"render.image_encoding": "Codificando PNG",
|
||||
"render.image_export": "Exportar imagen terminada",
|
||||
"render.image_exposure": "Exposición",
|
||||
"render.image_export": "Exportar PNG",
|
||||
"render.image_failed": "Error de renderizado",
|
||||
"render.image_height": "Altura",
|
||||
"render.image_help": "Imagen de vista previa por CPU · PNG · transparencia aproximada",
|
||||
"render.image_host_missing": "No hay un motor de imagen instalado.",
|
||||
"render.image_method": "Método de imagen",
|
||||
"render.image_overwrite": "Sustituir archivo existente",
|
||||
"render.image_preparing": "Preparando imagen",
|
||||
"render.image_progressive": "Renderizado progresivo",
|
||||
"render.image_queued": "Esperando para renderizar",
|
||||
"render.image_reduce": "Prueba con una imagen más pequeña o una escena más sencilla.",
|
||||
"render.image_running": "Renderizando imagen",
|
||||
"render.image_samples": "Muestras por píxel",
|
||||
"render.image_scene_stale": "Termina la operación de modelado o actualiza la escena antes de comprobar o iniciar una imagen.",
|
||||
"render.image_stale": "Esta imagen utiliza la escena capturada antes de los cambios posteriores.",
|
||||
"render.image_start": "Iniciar renderizado",
|
||||
"render.image_supported": "La solicitud de renderizado es compatible; la cola y la memoria disponible se comprobarán de nuevo al iniciar.",
|
||||
@@ -1347,347 +1030,20 @@
|
||||
"render.image_unknown": "Estado del renderizado no disponible",
|
||||
"render.image_use_largest": "Usar el tamaño máximo para esta proporción:",
|
||||
"render.image_width": "Anchura",
|
||||
"render.include_rig_cards": "Include passive cards and flags",
|
||||
"render.inspect_scene": "Actualizar resumen de la escena",
|
||||
"render.keep_editing": "Seguir editando",
|
||||
"render.layer_down": "Bajar",
|
||||
"render.layer_enabled": "Capa activada",
|
||||
"render.layer_remove": "Eliminar capa",
|
||||
"render.layer_strength": "Intensidad de capa",
|
||||
"render.layer_up": "Subir",
|
||||
"render.lens_effects_help": "El flare usa hasta cuatro altas luces separadas de la imagen lineal real. Anillos, halo, estelas, separación cromática y distorsión son aproximaciones artísticas en imagen. Los efectos espaciales requieren salida opaca; las muestras exteriores extienden el borde. El grano es determinista. No cambian física, EXR ni geometría.",
|
||||
"render.lens_effects_title": "Lente y fotografía",
|
||||
"render.library_all": "Catálogo",
|
||||
"render.library_all_states": "Todos los estados",
|
||||
"render.library_applied": "Variante del recurso aplicada. Comprueba la solicitud completa antes de renderizar.",
|
||||
"render.library_apply_hdr": "Guardar con este entorno HDR",
|
||||
"render.library_apply_maps": "Usar mapas de superficie GPU",
|
||||
"render.library_apply_studio": "Guardar la variante de iluminación",
|
||||
"render.library_apply_uniform": "Aplicar aspecto opaco uniforme",
|
||||
"render.library_authored_help": "Los recursos originales son ilustrativos, no datos de ingeniería medidos. Cada acción aplica únicamente la variante indicada.",
|
||||
"render.library_blocked": "Bloqueado / requiere calibración",
|
||||
"render.library_capabilities": "Especificaciones de capacidades",
|
||||
"render.library_center": "Centro de la iluminación",
|
||||
"render.library_clear_search": "Limpiar",
|
||||
"render.library_details": "Especificación y procedencia",
|
||||
"render.library_emitters_only_ack": "Usar solo emisores; omitir las tarjetas pasivas indicadas",
|
||||
"render.library_empty": "No hay recursos coincidentes. Prueba otra búsqueda o filtro.",
|
||||
"render.library_environment_concepts": "Conceptos de entornos",
|
||||
"render.library_extent": "Extensión del objeto",
|
||||
"render.library_favorite": "Añadir favorito",
|
||||
"render.library_favorites": "Favoritos",
|
||||
"render.library_hdr": "Entornos HDR incluidos",
|
||||
"render.library_hdr_help": "Incorpora el HDR original a la configuración guardada. Convierte explícitamente el origen azimutal; tras guardar no se necesita ningún archivo externo.",
|
||||
"render.library_inspect_only": "Selecciona para inspeccionar; no se aplica nada automáticamente. Favoritos e historial pertenecen a la sesión de este documento.",
|
||||
"render.library_light_concepts": "Conceptos de iluminación",
|
||||
"render.library_maps_help": "Built-in maps can be saved with pinned sources and local occurrence mapping. Temporary optical overrides remain a separate mode.",
|
||||
"render.library_material_concepts": "Conceptos de materiales",
|
||||
"render.library_native_scenes": "Estudios nativos",
|
||||
"render.library_next": "Página siguiente",
|
||||
"render.library_omitted": "No incluido",
|
||||
"render.library_physical_profiles": "Physical reference profiles",
|
||||
"render.library_previous": "Página anterior",
|
||||
"render.library_quality_concepts": "Especificaciones de calidad",
|
||||
"render.library_recent": "Recientes",
|
||||
"render.library_replace_environment": "Sustituir explícitamente el entorno existente",
|
||||
"render.library_replace_studio": "Replace saved lighting, environment and stage",
|
||||
"render.library_results": "Recursos encontrados",
|
||||
"render.library_rig_help": "Fit once at an explicit center and physical extent. Complete rigs include passive cards; calibration blockers still refuse. Conservative emitter/card clearance is checked before saving.",
|
||||
"render.library_rigs": "Configuraciones de iluminación creadas",
|
||||
"render.library_save_maps": "Save maps to this setup",
|
||||
"render.library_scene_bounds": "Usar los límites de la escena capturada",
|
||||
"render.library_scenes": "Especificaciones de escenas",
|
||||
"render.library_search": "Buscar en la biblioteca",
|
||||
"render.library_select_hint": "Elige un recurso en la Biblioteca para ver sus acciones disponibles y sus límites.",
|
||||
"render.library_selene": "Aspectos opacos de SELENE",
|
||||
"render.library_selene_shots": "Especificaciones de tomas de SELENE",
|
||||
"render.library_short_blocked": "No disponible",
|
||||
"render.library_short_native": "Composición nativa",
|
||||
"render.library_short_projection": "Proyección compatible",
|
||||
"render.library_short_reference": "Solo referencia",
|
||||
"render.library_short_specification": "Solo especificación",
|
||||
"render.library_source_preview": "Cargar vista del mapa original",
|
||||
"render.library_source_preview_help": "Mapa de color base original. No es una vista renderizada ni una medición del material.",
|
||||
"render.library_specification_only": "Solo especificación: no es un preajuste ejecutable completo. Los efectos, recursos y roles del objeto requieren implementación y asociación explícitas.",
|
||||
"render.library_specs": "Solo especificaciones",
|
||||
"render.library_status": "Disponibilidad",
|
||||
"render.library_surfaces": "Superficies PBR creadas",
|
||||
"render.library_target": "Cuerpo de destino",
|
||||
"render.library_title": "Biblioteca de recursos de renderizado",
|
||||
"render.library_unfavorite": "Quitar favorito",
|
||||
"render.library_uniform_ack": "Usar la aproximación uniforme sin efectos espaciales ni avanzados",
|
||||
"render.library_usable": "Proyecciones ejecutables",
|
||||
"render.linear_help": "Conserva un máster RGBA de 32 bits en coma flotante antes de la reducción de ruido, exposición, mapeo tonal, filtros y fondo. sRGB lineal/D65, premultiplicado por cobertura. Requiere memoria adicional. Sin compresión ni pases extra.",
|
||||
"render.linear_retain": "Conservar EXR lineal sin procesar",
|
||||
"render.linear_unavailable": "La imagen terminada seleccionada no conserva EXR. Activa la opción antes de renderizar; no se puede recuperar la radiancia original de un PNG.",
|
||||
"render.look_add": "Añadir filtro de color",
|
||||
"render.look_bleach": "Inspirado en bleach bypass",
|
||||
"render.look_cool": "Frío",
|
||||
"render.look_cyanotype": "Inspirado en cianotipia",
|
||||
"render.look_faded": "Copia desvanecida",
|
||||
"render.look_mono": "Monocromo",
|
||||
"render.look_sepia": "Sepia",
|
||||
"render.look_strength": "Intensidad del filtro",
|
||||
"render.look_teal_orange": "Verde azulado / naranja",
|
||||
"render.look_title": "Pila de filtros creativos",
|
||||
"render.look_warm": "Cálido",
|
||||
"render.mapping_scale": "Multiplicador de repetición física",
|
||||
"render.material_absorption": "Absorción R · G · B (por mm)",
|
||||
"render.material_add": "Añadir o editar este cuerpo",
|
||||
"render.material_assignments": "Asignaciones por cuerpo",
|
||||
"render.material_binding_enable": "Incluir los ajustes de este cuerpo",
|
||||
"render.material_box": "Caja alineada con los ejes",
|
||||
"render.material_browse": "Elegir imagen…",
|
||||
"render.material_certification": "El vidrio cerrado usa la distancia geométrica. La preparación GPU valida topología, orientación, solapamientos y límites ópticos; Comprobar solo admite la petición.",
|
||||
"render.material_choose_body": "Elegir un cuerpo de la escena",
|
||||
"render.material_closed": "Malla sólida cerrada",
|
||||
"render.material_coat": "Recubrimiento sobre el material base",
|
||||
"render.material_coat_enable": "Activar recubrimiento",
|
||||
"render.material_coat_help": "Recubrimiento dieléctrico ilustrativo sobre la apariencia opaca existente. Un peso cero conserva la base.",
|
||||
"render.material_color_map": "Imagen de color base (sRGB)",
|
||||
"render.material_color_replace": "El mapa base sustituye el color del cuerpo",
|
||||
"render.material_custom": "Óptica ilustrativa personalizada",
|
||||
"render.material_enable": "Usar estos ajustes de materiales GPU",
|
||||
"render.material_filter": "Filtrar cuerpos de la escena…",
|
||||
"render.material_filter_help": "Se muestran hasta 128 coincidencias. Ajusta el filtro para escenas grandes.",
|
||||
"render.material_glass": "Vidrio y absorción óptica",
|
||||
"render.material_glass_enable": "Usar vidrio físico en este cuerpo",
|
||||
"render.material_glass_exclusive": "Este motor no admite vidrio con recubrimiento ni mapas en el mismo cuerpo.",
|
||||
"render.material_glass_help": "Recetas originales e ilustrativas. Las muestras indican transmitancia, no un render. La absorción es −ln(transmitancia lineal) / distancia de referencia; no se cambia el alfa.",
|
||||
"render.material_gpu_required": "Estos materiales activos requieren trazado GPU. Selecciona GPU o desactiva los ajustes antes de Comprobar o Iniciar.",
|
||||
"render.material_ior": "Índice de refracción (IOR)",
|
||||
"render.material_limits": "Máximo de 128 modificaciones de cuerpos, 32 vidrios separados, 64 recubrimientos y 128 asociaciones de texturas. Los mapas idénticos comparten memoria; se mantienen los límites reales del dispositivo y de memoria total.",
|
||||
"render.material_map_invalid": "Las rutas de mapas deben ser locales y acotadas; la intensidad de normales debe ser finita y estar entre 0 y 1.",
|
||||
"render.material_map_required": "Elige una imagen de color o de normales antes de usar mapas.",
|
||||
"render.material_maps": "Mapas planos de color y normales",
|
||||
"render.material_maps_enable": "Activar mapas para este cuerpo opaco",
|
||||
"render.material_maps_help": "PNG/JPEG hasta 1024×1024; máximo total de 16 MiB codificados. U y V son vectores ortogonales de repetición en mm globales (máximo 128 repeticiones). Color sRGB y normales OpenGL +Y. Los archivos se capturan al Comprobar/Iniciar.",
|
||||
"render.material_metallic_map": "Mapa de metalicidad (lineal)",
|
||||
"render.material_normal_map": "Imagen de normales (OpenGL, +Y)",
|
||||
"render.material_normal_strength": "Intensidad de normales (0–1)",
|
||||
"render.material_origin": "Origen del mapa X · Y · Z (mm)",
|
||||
"render.material_refresh": "Actualizar cuerpos de la escena",
|
||||
"render.material_refresh_needed": "La escena ha cambiado. Actualiza la lista antes de añadir un cuerpo.",
|
||||
"render.material_remove": "Eliminar esta asignación",
|
||||
"render.material_repeat_u": "Una repetición U X · Y · Z (mm)",
|
||||
"render.material_repeat_v": "Una repetición V X · Y · Z (mm)",
|
||||
"render.material_revalidate": "Revalidar este cuerpo",
|
||||
"render.material_revalidate_needed": "Esta asignación pertenece a una revisión anterior. Revalida la identidad exacta del cuerpo antes de renderizar.",
|
||||
"render.material_roughness": "Rugosidad del recubrimiento (0,045–1)",
|
||||
"render.material_roughness_map": "Mapa de rugosidad (lineal)",
|
||||
"render.material_shadow": "Transmitir rayos de sombra rectos",
|
||||
"render.material_target_missing": "El ordinal y el nombre del cuerpo ya no coinciden. Elimina esta asignación y elige de nuevo el cuerpo deseado.",
|
||||
"render.material_thickness": "Espesor de la lámina (mm)",
|
||||
"render.material_thin": "Lámina delgada",
|
||||
"render.material_title": "Materiales de la imagen GPU",
|
||||
"render.material_transient": "Ajustes ópticos temporales de esta pestaña. No se guardan en el diseño, la configuración de render ni la biblioteca de materiales.",
|
||||
"render.material_v_up": "La coordenada V apunta hacia arriba en la imagen",
|
||||
"render.material_weight": "Peso del recubrimiento (0–1)",
|
||||
"render.method_cpu": "Trazado CPU",
|
||||
"render.method_gpu": "Trazado GPU",
|
||||
"render.method_limit_raster": "Raster utiliza la vista opaca de estudio fija. Selecciona un método de trazado de rayos para los controles ópticos compatibles.",
|
||||
"render.method_raster": "Raster CPU",
|
||||
"render.navigation_help": "La cámara se mueve como en Diseño (Preferencias › Navegación). Guardar configuración registra la cámara actual.",
|
||||
"render.nested_enable": "Dieléctricos anidados certificados",
|
||||
"render.nested_help": "Hasta 16 capas contenidas: cada padre debe ser convexo y cada hijo estar estrictamente dentro. Se rechazan intersecciones, contactos, láminas finas y sombras transmitidas en línea recta. La cámara interior usa la jerarquía certificada. No crea la superficie del líquido ni fusiona fronteras CAD en contacto.",
|
||||
"render.opaque_image_summary": "PNG de vista previa · PBR opaco · transparencia combinada",
|
||||
"render.opaque_preview_help": "PBR opaco y colores sencillos; transparencia combinada. La refracción, HDRI y el renderizado final aún no están disponibles.",
|
||||
"render.optics_remove": "Quitar óptica guardada",
|
||||
"render.optics_replace": "Sustituir explícitamente la óptica guardada del objetivo",
|
||||
"render.optics_save": "Guardar esta óptica en la toma",
|
||||
"render.optics_saved": "Objetivos ópticos guardados",
|
||||
"render.optics_saved_help": "La identidad exacta de definición, operación e instancia se conserva al reabrir sin conexión. Los objetivos ausentes, sustituidos u ocultos se rechazan; los materiales de ingeniería no cambian. Guarda o descarta los cambios numéricos antes de quitar un objetivo.",
|
||||
"render.output_format_status": "PNG es la imagen final exacta. El EXR opcional de GPU conserva radiancia lineal FLOAT32 antes de los filtros. La exportación no sustituye el resultado por la previsualización ni por otro trabajo.",
|
||||
"render.output_lock_ratio": "Bloquear proporción",
|
||||
"render.output_over_budget": "La imagen supera el presupuesto seleccionado. Elige explícitamente otro tamaño o perfil; no se reduce automáticamente.",
|
||||
"render.output_ratio_refused": "La dimensión vinculada supera el límite del método. Se conserva el tamaño anterior; desbloquea la proporción o elige un valor menor.",
|
||||
"render.output_swap_orientation": "Intercambiar ancho y alto",
|
||||
"render.page_aging": "Envejecimiento y depósitos",
|
||||
"render.page_all": "Todos los controles",
|
||||
"render.page_appearance": "Aspecto",
|
||||
"render.page_background": "Fondos",
|
||||
"render.page_camera": "Cámara",
|
||||
"render.page_diagnostics": "Diagnóstico",
|
||||
"render.page_effects": "Lente y acabados",
|
||||
"render.page_environment": "Entorno",
|
||||
"render.page_lighting": "Iluminación",
|
||||
"render.page_output": "Salida",
|
||||
"render.page_scene": "Escena",
|
||||
"render.passive_stage": "Suelo y escenario pasivo",
|
||||
"render.passive_stage_help": "Finite passive panels belong only to rendered images. They do not add CAD bodies or mass. Placement is fixed after applying; intersecting product bounds refuse. CPU does not provide diffuse bounce lighting.",
|
||||
"render.perspective": "Cámara en perspectiva",
|
||||
"render.physical_acknowledge": "I understand this partial reference needs grade, condition and source review",
|
||||
"render.physical_associations": "Physical candidates and roles",
|
||||
"render.physical_background_help": "Suelo y pared físicos pasivos, distintos del fondo de cámara. El ciclorama usa 24 franjas poligonales; no es infinito ni una superficie analíticamente suave. Solo sustituye el escenario con tu confirmación.",
|
||||
"render.physical_copy_plan": "Copy plan with sources and omissions",
|
||||
"render.physical_cyclorama": "Ajustar ciclorama poligonal",
|
||||
"render.physical_floor_wall": "Ajustar suelo y pared físicos",
|
||||
"render.physical_material_name": "Proposed reference material name",
|
||||
"render.physical_omissions": "Properties not representable in the native card",
|
||||
"render.physical_plan_only": "Read-only plan prepared. No material was created or assigned. Keep its extended profile and omissions when reviewing the native card.",
|
||||
"render.physical_prepare_plan": "Prepare creation plan (does not apply)",
|
||||
"render.physical_separate": "Appearance is not material identification. These are sourced references, not certified effective properties. Existing mass, engineering materials and optics are unchanged.",
|
||||
"render.physical_unknown": "No numerical candidate is established. Grade, formulation or construction must be specified; unknown is not zero.",
|
||||
"render.post_contrast": "Contraste",
|
||||
"render.post_gain_b": "Ganancia azul",
|
||||
"render.post_gain_g": "Ganancia verde",
|
||||
"render.post_gain_r": "Ganancia roja",
|
||||
"render.post_help": "Solo presentación creativa. Las ganancias RGB preceden al mapeo tonal; contraste, saturación y viñeta se aplican después en luz lineal de pantalla. Renderizar crea muestras nuevas. Reeditar utiliza el máster bruto retenido sin reducir ruido ni lanzar rayos nuevos.",
|
||||
"render.post_reset": "Restablecer filtros",
|
||||
"render.post_saturation": "Saturación",
|
||||
"render.post_title": "Filtros de presentación",
|
||||
"render.post_vignette": "Viñeta",
|
||||
"render.preview": "Vista previa integrada",
|
||||
"render.preview_help": "Sombreado por rasterización y transparencia combinada. La refracción, HDRI y el renderizado final aún no están disponibles.",
|
||||
"render.preview_image_settings_help": "La vista previa mantiene una iluminación de estudio fija. La exposición y la iluminación de imagen se aplican al trabajo al Comprobar o Iniciar.",
|
||||
"render.preview_retry": "Reintentar vista previa",
|
||||
"render.preview_unavailable_help": "La vista previa no está disponible. Los ajustes guardados y los controles de imagen siguen accesibles. Cambia la escena o reintenta expresamente; una escena rechazada no se reconstruye en cada fotograma.",
|
||||
"render.preview_upload": "Datos de vértices de la vista previa",
|
||||
"render.preview_upload_help": "Bytes exactos de los vértices de vista previa, no memoria total de GPU o proceso. No se simplifica la geometría. Los trabajos de imagen tienen admisión e iluminación propias.",
|
||||
"render.progressive_image_summary": "Vista previa PNG · PBR opaco · muestreo progresivo",
|
||||
"render.quality_ack": "Aceptar la proyección de presupuesto fijo y sus omisiones",
|
||||
"render.quality_apply": "Usar este presupuesto fijo",
|
||||
"render.quality_projection_help": "Los perfiles Q incluyen requisitos futuros. Esta acción proyecta solo un presupuesto PNG de muestras fijas y omite parada adaptativa, salida lineal y límites de transmisión independientes.",
|
||||
"render.quality_review": "Revisar presupuesto de muestras",
|
||||
"render.queue_pending": "Esta interfaz no contiene cola automática de producción ni renderizador de animaciones. Las imágenes terminadas se exportan por separado.",
|
||||
"render.radial_distortion": "Distorsión radial",
|
||||
"render.regrade_action": "Reeditar máster bruto (sin reducción de ruido)",
|
||||
"render.regrade_help": "Crea un PNG independiente a partir del máster bruto del resultado seleccionado y estos controles de presentación. No traza rayos, cambia tamaño, cámara o luces capturados, ni sobrescribe el EXR. No vuelve a reducir el ruido: el máster no conserva las guías del filtro. El resultado se exporta mediante los controles habituales.",
|
||||
"render.reload_settings": "Recargar ajustes guardados",
|
||||
"render.replace_ground": "Confirmar sustitución del escenario existente",
|
||||
"render.replace_saved_surface": "Replace an existing saved binding for this target",
|
||||
"render.restore_camera": "Restaurar cámara guardada",
|
||||
"render.result_actual_help": "Un píxel de la imagen por cada píxel de la pantalla.",
|
||||
"render.result_capture": "Revisión capturada",
|
||||
"render.result_current": "La revisión capturada del modelo está actualizada.",
|
||||
"render.result_empty": "Todavía no hay ninguna imagen terminada. Comprueba los ajustes e inicia un renderizado.",
|
||||
"render.result_fit": "Ajustar imagen",
|
||||
"render.result_job": "Trabajo",
|
||||
"render.result_navigation": "El desplazamiento y el zoom siguen las preferencias de Navegación. Espacio + arrastrar desplaza la imagen.",
|
||||
"render.result_open": "Abrir la imagen terminada en el lienzo principal.",
|
||||
"render.result_preview": "Vista previa en vivo",
|
||||
"render.result_provisional": "Imagen CPU provisional",
|
||||
"render.result_provisional_help": "Este avance no se puede exportar. Exportar conserva el último resultado terminado; al cancelar se vuelve a ese resultado.",
|
||||
"render.result_reduced_display": "Vista reducida al límite del dispositivo o de la caché. El PNG conservado y la exportación mantienen la resolución completa; esta vista reducida no permite inspección nativa al 100%.",
|
||||
"render.result_retained": "Se muestra la última imagen terminada; el nuevo renderizado todavía no la ha sustituido.",
|
||||
"render.result_sequence": "Avance",
|
||||
"render.result_title": "Resultado del renderizado",
|
||||
"render.return_design": "Volver a Diseño",
|
||||
"render.review_stale": "Esta revisión ha caducado. Resuelve o comprueba de nuevo antes de aplicar.",
|
||||
"render.ribbon_aging": "Envejecimiento",
|
||||
"render.ribbon_backgrounds": "Fondos",
|
||||
"render.ribbon_check": "Comprobar",
|
||||
"render.ribbon_check_help": "Valida exactamente el borrador que enviará Render. Sin cambiar de motor, reducir resolución ni sustituir recursos silenciosamente.",
|
||||
"render.ribbon_edit_lights": "Editar luces",
|
||||
"render.ribbon_effects": "Efectos",
|
||||
"render.ribbon_environment_help": "La iluminación HDR, el fondo exclusivo de cámara y las superficies físicas son controles independientes, con confirmación para sustituirlos.",
|
||||
"render.ribbon_finish_dialog": "Finaliza antes el diálogo de archivo o confirmación.",
|
||||
"render.ribbon_frame": "Encuadrar",
|
||||
"render.ribbon_hdr_library": "Biblioteca HDR",
|
||||
"render.ribbon_job_active": "Ya hay un render en curso; cancélalo explícitamente antes de iniciar otro.",
|
||||
"render.ribbon_light_rigs": "Iluminación",
|
||||
"render.ribbon_lights_help": "Revisa conjuntos originales o edita luces compatibles; la configuración actual no se sustituye hasta aplicar el cambio.",
|
||||
"render.ribbon_materials": "Materiales",
|
||||
"render.ribbon_materials_help": "Abre el inspector compartido de apariencia y la biblioteca de superficies originales. Los cambios ópticos no alteran propiedades de ingeniería sin avisar.",
|
||||
"render.ribbon_no_active_job": "No hay ningún render activo que cancelar.",
|
||||
"render.ribbon_no_result": "Completa un render antes de exportar su imagen.",
|
||||
"render.ribbon_no_saved_camera": "Guarda una toma antes de recuperar su cámara.",
|
||||
"render.ribbon_saved_camera": "Cámara guardada",
|
||||
"render.ribbon_shots": "Tomas",
|
||||
"render.ribbon_studios": "Estudios",
|
||||
"render.ribbon_studios_help": "Explora composiciones de estudio implementadas y revisa lo que sustituyen antes de aplicarlas. Las escenas conceptuales siguen identificadas.",
|
||||
"render.ribbon_surface_maps": "Mapas",
|
||||
"render.sampling_title": "Muestreo adaptativo y transmisión",
|
||||
"render.save_continue": "Guardar y continuar",
|
||||
"render.save_setup": "Guardar configuración",
|
||||
"render.saved_maps_runtime_conflict": "Saved surface bindings cannot be mixed with temporary optical overrides or an unsaved HDR. Save the HDR or remove the temporary inputs explicitly.",
|
||||
"render.saved_surface_remove": "Remove saved binding",
|
||||
"render.saved_surfaces": "Saved surface bindings",
|
||||
"render.saved_surfaces_help": "Pinned built-in maps follow the selected occurrence in local millimeters. A replaced or missing target is an error, never an automatic reassignment.",
|
||||
"render.scene_apply": "Aplicar estudio revisado",
|
||||
"render.scene_consent": "Confirmar sustitución de iluminación, entorno, exposición y escenario",
|
||||
"render.scene_preserved_fields": "Se conservan cámara, muestreo, dimensiones, asignaciones ópticas, geometría CAD y materiales físicos.",
|
||||
"render.scene_preserves_design": "Compón iluminación y escenario pasivo para el objeto visible. Geometría, materiales físicos y cámara se mantienen separados.",
|
||||
"render.scene_replaces": "Sustituye",
|
||||
"render.scene_review": "Resolver y revisar",
|
||||
"render.scene_review_help": "Revisa la composición nativa completa antes de aplicar. No se sustituyen las especificaciones SCENE avanzadas sin resolver.",
|
||||
"render.scene_scope_details": "Alcance de la composición",
|
||||
"render.setup_legacy_environment": "Esta configuración antigua usa rotación o intensidad de entorno heredadas. Se conservan esos valores; conviértelos explícitamente antes de guardar un trabajo de imagen completo.",
|
||||
"render.setup_name": "Nombre de configuración",
|
||||
"render.setup_numeric_help": "Guardar conserva el método de imagen activo y todos sus ajustes numéricos. Recargar o elegir otra configuración sustituye este borrador; la cámara se restaura por separado.",
|
||||
"render.setup_offline_help": "Puedes editar y guardar este método sin tener su motor disponible. Comprobar e Iniciar requieren un motor disponible.",
|
||||
"render.setup_runtime_unsupported": "Todavía no se puede guardar este trabajo completo: contiene mapas o asignaciones a cuerpos de la escena. Elimina esas entradas temporales antes de guardar una configuración portable.",
|
||||
"render.setup_save_missing": "No se ha encontrado la configuración guardada después de completar el comando.",
|
||||
"render.sharpening": "Realce de detalle",
|
||||
"render.shot_duplicate": "Duplicar toma guardada",
|
||||
"render.shot_manager": "Tomas guardadas",
|
||||
"render.shot_new_name": "Nombre de la nueva toma",
|
||||
"render.shot_remove": "Eliminar toma guardada",
|
||||
"render.shot_remove_help": "Elimina solo estos ajustes guardados. La geometría y las imágenes terminadas se conservan; se puede deshacer.",
|
||||
"render.shot_remove_title": "¿Eliminar toma guardada?",
|
||||
"render.shot_rename": "Renombrar toma guardada",
|
||||
"render.shot_saved_only": "Guarda primero el borrador. Duplicar y renombrar conservan los ajustes y las referencias de recursos.",
|
||||
"render.sky_ground": "Suelo · RGB lineal",
|
||||
"render.sky_horizon": "Horizonte · RGB lineal",
|
||||
"render.sky_zenith": "Cénit · RGB lineal",
|
||||
"render.soft_focus": "Enfoque suave",
|
||||
"render.stage_panel_count": "Saved panels",
|
||||
"render.studio_add_area": "Añadir luz de área",
|
||||
"render.studio_ambient": "Radiancia constante del entorno",
|
||||
"render.studio_ambient_rgb": "Radiancia constante del entorno (RGB lineal)",
|
||||
"render.studio_aperture": "Radio de apertura",
|
||||
"render.studio_area": "Luz de área",
|
||||
"render.studio_area_help": "Hasta cuatro rectángulos de una cara. Los semiejes perpendiculares U y V definen el tamaño; U × V apunta hacia la cara emisora. Coordenadas en milímetros del mundo.",
|
||||
"render.studio_center": "Centro X, Y, Z (mm)",
|
||||
"render.studio_denoise": "Reducir ruido en la imagen terminada",
|
||||
"render.studio_denoise_help": "Filtro final que respeta los bordes; puede suavizar detalles. El vidrio y los reflejos especulares nítidos no se filtran.",
|
||||
"render.studio_direction": "Dirección hacia el sol (X, Y, Z)",
|
||||
"render.studio_environment": "Entorno",
|
||||
"render.studio_environment_bounds": "La rotación del entorno debe ser finita dentro de ±1 000 000 radianes y la intensidad debe estar entre 0 y 100.",
|
||||
"render.studio_environment_none": "Sin mapa de entorno",
|
||||
"render.studio_focus": "Distancia de enfoque en el eje",
|
||||
"render.studio_half_u": "Semieje U (mm)",
|
||||
"render.studio_half_v": "Semieje V (mm)",
|
||||
"render.studio_hdr": "Archivo Radiance HDR",
|
||||
"render.studio_hdr_browse": "Elegir HDR…",
|
||||
"render.studio_hdr_help": "Radiance .hdr, panorama 2:1, hasta 2048 × 1024. EXR no está disponible. La suposición de color sólo se aplica cuando no se especifican primarias.",
|
||||
"render.studio_hdr_required": "Elige un archivo .hdr local.",
|
||||
"render.studio_hdr_srgb": "Interpretar primarias sin especificar como sRGB lineal",
|
||||
"render.studio_intensity": "Intensidad del entorno",
|
||||
"render.studio_lens": "Profundidad de campo",
|
||||
"render.studio_lens_perspective": "La profundidad de campo requiere una cámara en perspectiva.",
|
||||
"render.studio_lighting": "Iluminación de estudio",
|
||||
"render.studio_lighting_enable": "Usar iluminación de estudio personalizada",
|
||||
"render.studio_needs_review": "Revisar método y ajustes",
|
||||
"render.studio_radiance": "Radiancia RGB lineal",
|
||||
"render.studio_ready": "Preparado para comprobar",
|
||||
"render.studio_remove_area": "Eliminar luz de área",
|
||||
"render.studio_rotation": "Rotación del entorno",
|
||||
"render.studio_runtime_help": "Los ajustes numéricos del estudio y los HDR comprobados se pueden guardar con la configuración. Las asignaciones ópticas o de materiales por cuerpo siguen siendo temporales y no cambian las apariencias guardadas.",
|
||||
"render.studio_saved": "Ajustes guardados / cámara actual",
|
||||
"render.studio_sky": "Cielo artístico",
|
||||
"render.studio_sky_help": "Gradiente de cielo para iluminar la escena; no es un modelo atmosférico calibrado.",
|
||||
"render.studio_sun": "Sol",
|
||||
"render.studio_sun_radius": "Radio angular",
|
||||
"render.studio_title": "Estudio de renderizado",
|
||||
"render.studio_tone_map": "Mapeo de tonos",
|
||||
"render.subsurface_enable": "Recorrido aleatorio de dispersión interna",
|
||||
"render.subsurface_help": "Solo dieléctricos cerrados. Dispersión y absorción RGB son coeficientes separados por mm. Las trayectorias se dispersan dentro de la geometría y se refractan o reflejan en la frontera. Desactiva sombras rectas, reducción de ruido de superficies y volumen exterior; usa un estrato Fresnel. No es un filtro de difusión, rejilla de densidad ni material medido. Los medios densos pueden requerir más muestras y profundidad.",
|
||||
"render.subsurface_rate": "Tasa de colisiones de dispersión",
|
||||
"render.transmission_scope": "El vidrio GPU admite medios separados certificados. No se admiten medios anidados sin restricciones ni un estimador de transmisión sin ruido.",
|
||||
"render.transmission_strata": "Estratos de Fresnel condicionales",
|
||||
"render.transmission_strata_help": "1 conserva la secuencia original. 2 o 4 subtrayectorias estratifican la primera decisión de reflexión o transmisión del vidrio, sin cambiar Snell ni la absorción. Aumentan el trabajo; no son un filtro, soporte de medios anidados ni una garantía de mayor velocidad.",
|
||||
"render.unavailable_preview": "Esta configuración solicita un perfil de vista previa no disponible.",
|
||||
"render.unsaved": "Cambios de vista previa sin guardar",
|
||||
"render.use_preview": "Usar vista previa integrada",
|
||||
"render.volume_albedo": "Albedo de dispersión · RGB lineal",
|
||||
"render.volume_anisotropy": "Coseno medio de fase",
|
||||
"render.volume_enable": "Activar medio participante",
|
||||
"render.volume_extinction": "Coeficiente de extinción",
|
||||
"render.volume_help": "Transporte GPU dentro de una caja finita, con atenuación de Beer y dispersión HG en trayectorias de cámara e indirectas. Utiliza luces explícitas de estudio, HDR o cielo, con atenuación de sombras. El coseno positivo dispersa hacia delante. Deben desactivarse explícitamente el vidrio y la reducción de ruido de superficies. Extinción cero conserva el transporte. No es atmósfera lunar, rejilla de densidad, emisión volumétrica, transporte espectral ni propiedad certificada.",
|
||||
"render.volume_max": "Límites máximos · mm globales",
|
||||
"render.volume_min": "Límites mínimos · mm globales",
|
||||
"render.volume_title": "Volumen homogéneo acotado",
|
||||
"sample.bracket.about": "Brida base con pestañas laterales y reborde, según una regla de 2 mm",
|
||||
"sample.bracket.name": "Soporte de chapa",
|
||||
"sample.enclosure.about": "Caja vaciada con labio, salientes para tornillos, apoyo y encaje a presión",
|
||||
@@ -1761,9 +1117,6 @@
|
||||
"thread.designation": "Designación",
|
||||
"thread.iso_metric": "Métrica ISO",
|
||||
"thread.select_size": "Seleccionar tamaño",
|
||||
"timeline.delete_folders_tip": "Conservar sus operaciones y carpetas anidadas en el historial",
|
||||
"timeline.feature_error": "Error: {message}",
|
||||
"timeline.feature_warning": "Advertencia: {message}",
|
||||
"timeline.group": "Nueva carpeta de operaciones",
|
||||
"timeline.move_many": "Mover elementos de la línea de tiempo",
|
||||
"timeline.move_to_group": "Mover operaciones a la carpeta",
|
||||
@@ -1775,10 +1128,6 @@
|
||||
"toolbar.customize_note": "Pestaña {tab}. Marque un comando para añadir su botón a la barra de herramientas. También puede arrastrar una fila del desplegable de un panel a la barra, arrastrar un botón fuera de la barra para quitarlo o usar la chincheta de una fila del desplegable.",
|
||||
"toolbar.image": "IMAGEN",
|
||||
"toolbar.render": "RENDERIZAR",
|
||||
"toolbar.render_appearance": "APARIENCIA",
|
||||
"toolbar.render_camera": "CÁMARA",
|
||||
"toolbar.render_environment": "ENTORNO",
|
||||
"toolbar.render_lighting": "ILUMINACIÓN",
|
||||
"toolbar.render_scene": "ESCENA",
|
||||
"trace.points": "{count} puntos a lo largo de la trayectoria",
|
||||
"trace.stopped_by_contact": "Termina en la muestra {sample}: {a} atravesaría {b}",
|
||||
@@ -1977,7 +1326,7 @@
|
||||
"ui.agent_chat_pop_out": "Abrir en su propia ventana, que puedes mover a otra pantalla",
|
||||
"ui.agent_chat_previous_picture": "Imagen anterior",
|
||||
"ui.agent_chat_privacy": "Solo se envían datos al agente que ejecutes, con tu propia cuenta.",
|
||||
"ui.agent_chat_privacy_note": "El chat no envía nada por sí mismo a otros destinos: el agente que ejecutas se comunica con su propio servicio utilizando tu cuenta. SolidCraft nunca ve ni almacena claves o contraseñas.",
|
||||
"ui.agent_chat_privacy_note": "SolidCraft no envía nada a otros destinos: el agente que ejecutas se comunica con su propio servicio utilizando tu cuenta. SolidCraft nunca ve ni almacena claves o contraseñas.",
|
||||
"ui.agent_chat_queue": "Poner en cola",
|
||||
"ui.agent_chat_queue_full": "Hay demasiados mensajes en espera: elimina uno o espera a que termine el agente.",
|
||||
"ui.agent_chat_queued": "En cola",
|
||||
@@ -2043,7 +1392,6 @@
|
||||
"ui.ai_http_busy": "el servicio está ocupado o ha superado su límite; inténtalo más tarde",
|
||||
"ui.ai_http_key": "el servicio ha rechazado la clave: compruébala en Preferencias > IA",
|
||||
"ui.ai_http_not_found": "no existe ese servicio o modelo: comprueba la URL base y el nombre del modelo",
|
||||
"ui.ai_http_too_large": "la conversación es demasiado grande para este endpoint; empieza un chat nuevo",
|
||||
"ui.ai_key": "Clave de API",
|
||||
"ui.ai_key_hint": "opcional; se guarda en el llavero del sistema",
|
||||
"ui.ai_key_needs_https": "La clave solo se envía mediante HTTPS o a este ordenador.",
|
||||
@@ -2071,6 +1419,7 @@
|
||||
"ui.ai_permissions": "Permisos",
|
||||
"ui.ai_preset": "Preajuste",
|
||||
"ui.ai_remove": "Eliminar",
|
||||
"ui.ai_request_limit": "se ha detenido tras 24 solicitudes en un mensaje; envía otro para continuar",
|
||||
"ui.ai_reset": "Restablecer",
|
||||
"ui.ai_settings": "Ajustes de IA…",
|
||||
"ui.ai_show_features": "Mostrar funciones de IA",
|
||||
@@ -2164,7 +1513,6 @@
|
||||
"ui.command_done": "{command} completado",
|
||||
"ui.command_palette_hint": "Escribe un comando, p. ej., {example}, o pulsa S para buscar",
|
||||
"ui.concentric": "Concéntrico",
|
||||
"ui.cone": "Cono",
|
||||
"ui.conic_curve": "Curva cónica",
|
||||
"ui.constant": "Constante",
|
||||
"ui.constraints_conflict": "Conflicto entre restricciones",
|
||||
@@ -2223,11 +1571,6 @@
|
||||
"ui.draft": "Ángulo de salida",
|
||||
"ui.drawing": "Dibujo",
|
||||
"ui.drill_point": "Punta de broca",
|
||||
"ui.easyprint_consent": "EasyPrint es el slicer de Printables en el navegador. Para abrir el modelo allí, SolidCraft lo sube a los servidores de Printables. El enlace caduca a los 30 minutos y tendrás que haber iniciado sesión en Printables. ¿Continuar?",
|
||||
"ui.easyprint_failed": "No se pudo subir a EasyPrint: {error}",
|
||||
"ui.easyprint_opened": "Abierto en EasyPrint",
|
||||
"ui.easyprint_row": "EasyPrint (Printables, en el navegador)",
|
||||
"ui.easyprint_uploading": "Subiendo a Printables…",
|
||||
"ui.edit": "Editar",
|
||||
"ui.editJointLimits": "Editar límites de unión",
|
||||
"ui.edit_section_analysis": "Editar análisis de sección",
|
||||
@@ -2253,8 +1596,6 @@
|
||||
"ui.filter_commands_or_keys": "Filtrar comandos o teclas",
|
||||
"ui.findInTimeline": "Buscar en la línea de tiempo",
|
||||
"ui.findInWindow": "Buscar en la ventana",
|
||||
"ui.fine": "Fina",
|
||||
"ui.finest": "Muy fina",
|
||||
"ui.finish_sketch": "Finalizar boceto",
|
||||
"ui.fit": "Ajustar",
|
||||
"ui.fix_unfix": "Fijar/liberar",
|
||||
@@ -2284,7 +1625,6 @@
|
||||
"ui.heading_circular_pattern": "PATRÓN CIRCULAR",
|
||||
"ui.heading_coil": "HÉLICE",
|
||||
"ui.heading_combine": "COMBINAR",
|
||||
"ui.heading_cone": "CONO",
|
||||
"ui.heading_configure": "CONFIGURAR",
|
||||
"ui.heading_constraints": "RESTRICCIONES",
|
||||
"ui.heading_construct": "CONSTRUIR",
|
||||
@@ -2344,7 +1684,6 @@
|
||||
"ui.heading_torus": "TORO",
|
||||
"ui.heading_utilities": "UTILIDADES",
|
||||
"ui.heading_utility": "UTILIDAD",
|
||||
"ui.heading_weldments": "SOLDADURA",
|
||||
"ui.help": "Ayuda",
|
||||
"ui.hide": "Ocultar",
|
||||
"ui.hideAxes": "Ocultar ejes",
|
||||
@@ -2357,7 +1696,6 @@
|
||||
"ui.history_first": "Ir al principio",
|
||||
"ui.history_forward": "Avanzar un paso",
|
||||
"ui.history_last": "Ir al final",
|
||||
"ui.history_marker_tip": "Marcador del historial: arrastra para retroceder el modelo; las nuevas operaciones se insertan aquí",
|
||||
"ui.history_title": "Historial",
|
||||
"ui.hole": "Agujero",
|
||||
"ui.hole_tap_type": "Tipo de roscado del agujero",
|
||||
@@ -2482,7 +1820,6 @@
|
||||
"ui.project_face_edges_sketch": "Proyectar aristas de caras en bocetos nuevos",
|
||||
"ui.properties": "Propiedades",
|
||||
"ui.pull_direction": "Dirección de tracción",
|
||||
"ui.quality": "Calidad",
|
||||
"ui.quantity": "Cantidad",
|
||||
"ui.quantity_2": "Cantidad 2",
|
||||
"ui.quit": "Salir",
|
||||
@@ -2521,7 +1858,9 @@
|
||||
"ui.right": "Derecha",
|
||||
"ui.save": "Guardar",
|
||||
"ui.save_as": "Guardar como",
|
||||
"ui.save_mesh_no_slicer": "No se ha encontrado ningún laminador: elige Personalizado… y selecciona tu laminador, o desactiva Enviar al laminador",
|
||||
"ui.save_mesh_pick_bodies": "Selecciona los cuerpos que se van a guardar",
|
||||
"ui.save_mesh_temp_failed": "No se ha podido crear {folder}: {error}",
|
||||
"ui.saved_to": "Guardado en {path}",
|
||||
"ui.sbox_pin": "Fijar arriba en la búsqueda S",
|
||||
"ui.sbox_unpin": "Desfijar",
|
||||
@@ -2536,6 +1875,7 @@
|
||||
"ui.selection_shift_add_only": "Mayús+clic añade sin quitar",
|
||||
"ui.selection_shift_add_only_tip": "Activado: Mayús añade elementos, Ctrl/Cmd alterna. Desactivado (predeterminado): Mayús y Ctrl/Cmd alternan.",
|
||||
"ui.send": "Enviar",
|
||||
"ui.send_to_slicer": "Enviar al laminador",
|
||||
"ui.sent_to_slicer": "Enviado a {slicer}",
|
||||
"ui.sheet_gap": "Separación",
|
||||
"ui.shell": "Vaciado",
|
||||
@@ -2574,7 +1914,6 @@
|
||||
"ui.spline": "Spline",
|
||||
"ui.split_body": "Dividir cuerpo",
|
||||
"ui.stand_up_tip": "Tras abrir un archivo STEP o IGES con Z hacia arriba, ofrecer un cuarto de vuelta alrededor de X",
|
||||
"ui.standard": "Estándar",
|
||||
"ui.start": "Iniciar",
|
||||
"ui.start_over": "Volver a empezar",
|
||||
"ui.start_page": "Página de inicio",
|
||||
@@ -2627,7 +1966,6 @@
|
||||
"ui.untitled": "Sin título",
|
||||
"ui.up_axis_note": "Eje que apunta hacia arriba en las vistas (vista de eje, Inicio, Mirar hacia). Los diseños no cambian.",
|
||||
"ui.updateToCurrentView": "Actualizar a vista actual",
|
||||
"ui.upload": "Subir",
|
||||
"ui.view_centre": "Centro de la vista",
|
||||
"ui.visual_shaded_hidden_edges": "Sombreado con aristas ocultas",
|
||||
"ui.visual_shaded_with_edges": "Sombreado con aristas",
|
||||
|
||||
+9
-35
@@ -72,7 +72,6 @@
|
||||
"Choose…": "Choisir…",
|
||||
"Circular": "Circulaire",
|
||||
"Circumference": "Circonférence",
|
||||
"Clockwise": "Horaire",
|
||||
"Close draft": "Fermer le brouillon",
|
||||
"Collisions": "Collisions",
|
||||
"Colour": "Couleur",
|
||||
@@ -270,7 +269,7 @@
|
||||
"Preview Row": "Aperçu de la ligne",
|
||||
"Preview leaves the current design unchanged.": "L'aperçu ne modifie pas la conception actuelle.",
|
||||
"Profiles/Curves": "Profils/Courbes",
|
||||
"Quantity": "Quantité",
|
||||
"Quantity": "Grandeur",
|
||||
"RECENT": "RÉCENTS",
|
||||
"REFERENCE": "RÉFÉRENCE",
|
||||
"RULED SURFACE": "SURFACE RÉGLÉE",
|
||||
@@ -323,7 +322,6 @@
|
||||
"Search commands": "Rechercher des commandes",
|
||||
"Search this library": "Rechercher dans cette bibliothèque",
|
||||
"Section": "Coupe",
|
||||
"Section position": "Position de la section",
|
||||
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Sélectionner un autre élément : maintenir le bouton gauche immobile sur le modèle (ou clic droit › Sélectionner un autre…) pour lister tout ce qui se trouve sous le curseur ; cliquer de nouveau au même endroit pour sélectionner l'élément suivant. Double-cliquer avec le bouton du milieu pour revenir à l'accueil (Préférences › Navigation).",
|
||||
"Select bodies (a face picks its body)": "Sélectionner des corps (une face sélectionne son corps)",
|
||||
"Select bodies or faces instead": "Sélectionner plutôt des corps ou des faces",
|
||||
@@ -335,7 +333,6 @@
|
||||
"Show Agent Cursor": "Afficher le curseur de l'agent",
|
||||
"Show in folder": "Afficher dans le dossier",
|
||||
"Show the pointer an AI agent moves (#35)": "Afficher le pointeur déplacé par un agent IA (#35)",
|
||||
"Show tool at cursor": "Afficher l'outil près du curseur",
|
||||
"Signed axes": "Axes signés",
|
||||
"Size": "Taille",
|
||||
"Sketch constraints": "Contraintes d'esquisse",
|
||||
@@ -348,11 +345,9 @@
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft s'est fermé avant l'enregistrement de ces conceptions. Leur dernier enregistrement automatique peut être ouvert.",
|
||||
"Specialise": "Spécialiser",
|
||||
"Speed": "Vitesse",
|
||||
"Square": "Carré",
|
||||
"Stand it up": "Redresser",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Redresser : un quart de tour autour de X, sous forme d'une fonction Déplacer que vous pouvez annuler.",
|
||||
"Stand this part up?": "Redresser cette pièce ?",
|
||||
"Start angle": "Angle de départ",
|
||||
"Start bulge": "Renflement de début",
|
||||
"Start continuity": "Continuité de début",
|
||||
"Step": "Étape",
|
||||
@@ -417,7 +412,6 @@
|
||||
"angle, height": "angle, hauteur",
|
||||
"appearance.dielectric_ior": "Indice de réfraction (diélectrique)",
|
||||
"appearance.metallic": "Métallique",
|
||||
"appearance.only_available": "Apparences disponibles uniquement",
|
||||
"appearance.pbr_help": "Voir cette surface dans l'espace Rendu ou dans un PNG du produit. Sans réfraction ni transmission.",
|
||||
"appearance.pbr_opaque": "PBR opaque",
|
||||
"appearance.roughness": "Rugosité",
|
||||
@@ -457,7 +451,6 @@
|
||||
"cmd.canvas_delete": "Supprimer le canevas",
|
||||
"cmd.canvas_edit": "Modifier le canevas",
|
||||
"cmd.canvas_insert": "Canevas",
|
||||
"cmd.canvas_to_sketch": "Esquisse depuis le canevas",
|
||||
"cmd.component_activate": "Activer le composant",
|
||||
"cmd.component_break_link": "Rompre le lien",
|
||||
"cmd.component_capture_position": "Capturer la position",
|
||||
@@ -535,7 +528,6 @@
|
||||
"cmd.file_export": "Exporter",
|
||||
"cmd.file_insert_mesh": "Insérer un maillage",
|
||||
"cmd.file_insert_step": "Insérer un STEP",
|
||||
"cmd.file_open_in_easyprint": "Ouvrir avec EasyPrint",
|
||||
"cmd.file_save_mesh": "Enregistrer comme maillage",
|
||||
"cmd.file_slicers": "Slicers installés",
|
||||
"cmd.frame_member": "Élément de structure",
|
||||
@@ -581,7 +573,6 @@
|
||||
"cmd.material_create": "Nouveau matériau physique",
|
||||
"cmd.material_delete": "Supprimer le matériau physique",
|
||||
"cmd.material_edit": "Modifier le matériau physique",
|
||||
"cmd.material_inspect": "Inspecter le matériau physique",
|
||||
"cmd.material_list": "Lister les matériaux",
|
||||
"cmd.material_properties_directional": "Module d'élasticité directionnel",
|
||||
"cmd.material_properties_validate": "Valider les données du matériau physique",
|
||||
@@ -870,17 +861,11 @@
|
||||
"help.mit": "Licence MIT",
|
||||
"help.notice": "NOTICE",
|
||||
"help.solid.chamfer": "Coupe les arêtes sélectionnées par un biseau plat de la distance indiquée. À utiliser pour casser des arêtes vives ou faciliter l'entrée d'une goupille ou d'une vis.",
|
||||
"help.solid.combine": "Joint des corps, les retire d'un corps cible ou ne garde que le volume qu'ils ont en commun, et peut conserver les corps outils. À utiliser pour fusionner des pièces, tailler une forme avec une autre ou repérer où deux corps se recouvrent.",
|
||||
"help.solid.extrude": "Pousse le profil d'une esquisse en 3D sur une distance, en ajoutant ou en retirant de la matière. À utiliser pour transformer un contour plat en bloc, en bossage ou en poche.",
|
||||
"help.solid.fillet": "Arrondit les arêtes sélectionnées par un raccord circulaire lisse du rayon indiqué. À utiliser pour adoucir des arêtes vives par souci de solidité, de confort ou d'aspect.",
|
||||
"help.solid.hole": "Perce un trou rond du diamètre indiqué en un point choisi d'une face. À utiliser pour les passages de vis, les goupilles et autres ouvertures rondes simples.",
|
||||
"help.solid.loft": "Raccorde en douceur deux profils d'esquisse ou plus, ou un profil et un point, par un solide lissé. À utiliser pour les transitions, entonnoirs, poignées et toute forme dont la section change sur sa longueur.",
|
||||
"help.solid.mirror": "Crée une copie symétrique des corps ou fonctions sélectionnés par rapport à un plan, et peut la fusionner avec l'original. À utiliser pour les pièces symétriques : on modélise une moitié et on obtient les deux.",
|
||||
"help.solid.pattern.circular": "Répète les objets sélectionnés autour d'un axe, selon le nombre et l'angle total indiqués. À utiliser pour les cercles de perçages, rayons, ailettes et dents.",
|
||||
"help.solid.pattern.rectangular": "Répète les objets sélectionnés dans une ou deux directions, selon le nombre et l'espacement indiqués. À utiliser pour les rangées et grilles de trous, nervures, goupilles ou pieds.",
|
||||
"help.solid.revolve": "Fait tourner le profil d'une esquisse autour d'un axe pour obtenir un solide de révolution. À utiliser pour les arbres, boutons, bols et toute pièce ronde autour d'une ligne centrale.",
|
||||
"help.solid.shell": "Évide un solide en laissant des parois de l'épaisseur indiquée, et peut ouvrir les faces choisies. À utiliser pour les boîtes, couvercles et autres pièces à parois minces.",
|
||||
"help.solid.sweep": "Entraîne le profil d'une esquisse le long d'une trajectoire pour obtenir un solide. À utiliser pour les tuyaux, rails, poignées et toute pièce de section constante qui suit une courbe.",
|
||||
"help.third_party": "Licences tierces",
|
||||
"home.back": "Revenir à {name}",
|
||||
"home.crashes": "Rapports de plantage",
|
||||
@@ -951,7 +936,7 @@
|
||||
"issue.note_opened_copied": "Rapport copié. Collez-le dans le brouillon GitHub, joignez la capture d’écran, puis cliquez sur Submit new issue sur GitHub.",
|
||||
"issue.note_removed": "Capture d’écran retirée de ce brouillon.",
|
||||
"issue.note_report_copied": "Rapport copié. Collez-le dans le brouillon GitHub si nécessaire.",
|
||||
"issue.note_save_failed": "Impossible d’enregistrer la capture d’écran : {error}",
|
||||
"issue.note_save_failed": "Impossible d’enregistrer la capture d’écran : {error}",
|
||||
"issue.note_saved": "Capture d’écran enregistrée. Joignez le PNG dans le brouillon GitHub.",
|
||||
"issue.save_screenshot": "Enregistrer la capture d’écran…",
|
||||
"joint.from_bodies_hint": "Chaque corps sélectionné passe dans un nouveau composant à lui et reste où il est.",
|
||||
@@ -985,7 +970,6 @@
|
||||
"motion.key_here": "Placer une image clé ici",
|
||||
"motion.key_here_hint": "Ajouter une image clé à cette étape avec la valeur actuelle de la liaison",
|
||||
"motion.no_collisions": "Aucune collision au cours de l'étude",
|
||||
"motion.plot_quantity": "Grandeur",
|
||||
"motion.step_back": "Étape précédente",
|
||||
"motion.step_forward": "Étape suivante",
|
||||
"motion.to_end": "À la fin",
|
||||
@@ -1082,7 +1066,7 @@
|
||||
"search.parameter": "Paramètre",
|
||||
"search.recent": "Récents",
|
||||
"search.run": "Exécuter la commande",
|
||||
"search.suggest": "Vouliez-vous dire {label} ?",
|
||||
"search.suggest": "Vouliez-vous dire {label} ?",
|
||||
"search.try": "Saisissez un nom de commande, une entité de cette conception ou un paramètre. Pour exécuter une commande avec des valeurs, saisissez son id et un objet JSON : {example}",
|
||||
"search.valid": "Valide",
|
||||
"settings.display_quality_note": "Les niveaux plus fins affichent plus lisses les faces et arêtes courbes, mais leur maillage prend plus de temps. Seule la vue change, jamais la conception ni les exports.",
|
||||
@@ -1097,7 +1081,7 @@
|
||||
"settings.reset_intro": "Ces réglages ne fonctionnent pas sur cet ordinateur et empêcheraient SolidCraft de démarrer ; ils ont donc été modifiés :",
|
||||
"settings.reset_title": "Des réglages ont été modifiés",
|
||||
"settings.supersampling_note": "Dessine la vue à une résolution plus élevée puis la réduit : arêtes, lignes et grille plus lisses sur les écrans de définition inférieure à la 4K. Cela coûte du temps GPU : environ 2,25× plus de pixels à 1,5× et 4× plus à 2×.",
|
||||
"shortcut.json_error": "les paramètres doivent être au format JSON : {error}",
|
||||
"shortcut.json_error": "les paramètres doivent être au format JSON : {error}",
|
||||
"shortcut.no_command": "la commande `{id}` n’existe pas ; le texte avant le JSON doit être un ID de commande",
|
||||
"shortcut.pin": "Épingler en haut de la boîte S",
|
||||
"shortcut.unpin": "Désépingler",
|
||||
@@ -1342,7 +1326,7 @@
|
||||
"ui.agent_chat_pop_out": "Ouvrir dans une fenêtre séparée, déplaçable vers un autre écran",
|
||||
"ui.agent_chat_previous_picture": "Image précédente",
|
||||
"ui.agent_chat_privacy": "Rien n'est envoyé ailleurs qu'à l'agent que vous lancez, sous votre propre compte.",
|
||||
"ui.agent_chat_privacy_note": "Le chat n'envoie rien nulle part de lui-même : l'agent que vous lancez dialogue avec son propre service, sous votre propre compte. SolidCraft ne voit ni ne stocke jamais de clé ni de mot de passe.",
|
||||
"ui.agent_chat_privacy_note": "SolidCraft n'envoie rien nulle part : l'agent que vous lancez dialogue avec son propre service, sous votre propre compte. SolidCraft ne voit ni ne stocke jamais de clé ni de mot de passe.",
|
||||
"ui.agent_chat_queue": "Mettre en file",
|
||||
"ui.agent_chat_queue_full": "Trop de messages sont en attente : retirez-en un ou attendez l'agent.",
|
||||
"ui.agent_chat_queued": "En file d'attente",
|
||||
@@ -1406,7 +1390,6 @@
|
||||
"ui.ai_http_busy": "le point d'accès est occupé ou a dépassé sa limite ; réessayez plus tard",
|
||||
"ui.ai_http_key": "le point d'accès a refusé la clé : vérifiez-la dans Préférences › IA",
|
||||
"ui.ai_http_not_found": "point d'accès ou modèle introuvable : vérifiez l'URL de base et le nom du modèle",
|
||||
"ui.ai_http_too_large": "la conversation est trop volumineuse pour ce point d'accès ; commencez une nouvelle discussion",
|
||||
"ui.ai_key": "Clé d'API",
|
||||
"ui.ai_key_hint": "facultative ; conservée dans le trousseau de clés du système",
|
||||
"ui.ai_key_needs_https": "Une clé n'est envoyée qu'en HTTPS, ou vers cet ordinateur.",
|
||||
@@ -1434,6 +1417,7 @@
|
||||
"ui.ai_permissions": "Autorisations",
|
||||
"ui.ai_preset": "Préréglage",
|
||||
"ui.ai_remove": "Retirer",
|
||||
"ui.ai_request_limit": "arrêté après 24 requêtes pour un message ; envoyez un autre message pour poursuivre",
|
||||
"ui.ai_reset": "Réinitialiser",
|
||||
"ui.ai_settings": "Réglages IA…",
|
||||
"ui.ai_show_features": "Afficher les fonctionnalités IA",
|
||||
@@ -1527,7 +1511,6 @@
|
||||
"ui.command_done": "{command} terminé",
|
||||
"ui.command_palette_hint": "Tapez une commande, ex. {example} — ou S pour chercher",
|
||||
"ui.concentric": "Concentricité",
|
||||
"ui.cone": "Cône",
|
||||
"ui.conic_curve": "Courbe conique",
|
||||
"ui.constant": "Constant",
|
||||
"ui.constraints_conflict": "Contraintes en conflit",
|
||||
@@ -1586,11 +1569,6 @@
|
||||
"ui.draft": "Dépouille",
|
||||
"ui.drawing": "Mise en plan",
|
||||
"ui.drill_point": "Pointe de foret",
|
||||
"ui.easyprint_consent": "EasyPrint est le slicer de Printables dans le navigateur. Pour y ouvrir le modèle, SolidCraft l'envoie sur les serveurs de Printables. Le lien expire au bout de 30 minutes et vous devrez être connecté à Printables. Continuer ?",
|
||||
"ui.easyprint_failed": "Échec de l'envoi vers EasyPrint : {error}",
|
||||
"ui.easyprint_opened": "Ouvert dans EasyPrint",
|
||||
"ui.easyprint_row": "EasyPrint (Printables, dans le navigateur)",
|
||||
"ui.easyprint_uploading": "Envoi vers Printables…",
|
||||
"ui.edit": "Modifier",
|
||||
"ui.editJointLimits": "Modifier les limites de liaison",
|
||||
"ui.edit_section_analysis": "Modifier l'analyse de coupe",
|
||||
@@ -1616,8 +1594,6 @@
|
||||
"ui.filter_commands_or_keys": "Filtrer les commandes ou les touches",
|
||||
"ui.findInTimeline": "Trouver dans la chronologie",
|
||||
"ui.findInWindow": "Trouver dans la fenêtre",
|
||||
"ui.fine": "Fine",
|
||||
"ui.finest": "Très fine",
|
||||
"ui.finish_sketch": "Terminer l'esquisse",
|
||||
"ui.fit": "Cadrer",
|
||||
"ui.fix_unfix": "Fixer/Libérer",
|
||||
@@ -1647,7 +1623,6 @@
|
||||
"ui.heading_circular_pattern": "RÉPÉTITION CIRCULAIRE",
|
||||
"ui.heading_coil": "HÉLICE",
|
||||
"ui.heading_combine": "COMBINER",
|
||||
"ui.heading_cone": "CÔNE",
|
||||
"ui.heading_configure": "CONFIGURER",
|
||||
"ui.heading_constraints": "CONTRAINTES",
|
||||
"ui.heading_construct": "CONSTRUIRE",
|
||||
@@ -1707,7 +1682,6 @@
|
||||
"ui.heading_torus": "TORE",
|
||||
"ui.heading_utilities": "UTILITAIRES",
|
||||
"ui.heading_utility": "UTILITAIRE",
|
||||
"ui.heading_weldments": "MÉCANO-SOUDURE",
|
||||
"ui.help": "Aide",
|
||||
"ui.hide": "Masquer",
|
||||
"ui.hideAxes": "Masquer les axes",
|
||||
@@ -1844,7 +1818,6 @@
|
||||
"ui.project_face_edges_sketch": "Projeter les arêtes de la face dans les nouvelles esquisses",
|
||||
"ui.properties": "Propriétés",
|
||||
"ui.pull_direction": "Direction de démoulage",
|
||||
"ui.quality": "Qualité",
|
||||
"ui.quantity": "Quantité",
|
||||
"ui.quantity_2": "Quantité 2",
|
||||
"ui.quit": "Quitter",
|
||||
@@ -1883,7 +1856,9 @@
|
||||
"ui.right": "Droite",
|
||||
"ui.save": "Enregistrer",
|
||||
"ui.save_as": "Enregistrer sous",
|
||||
"ui.save_mesh_no_slicer": "Aucun slicer trouvé : choisissez Personnalisé… et sélectionnez votre slicer, ou désactivez Envoyer au slicer",
|
||||
"ui.save_mesh_pick_bodies": "Sélectionner les corps à enregistrer",
|
||||
"ui.save_mesh_temp_failed": "Impossible de créer {folder} : {error}",
|
||||
"ui.saved_to": "Enregistré : {path}",
|
||||
"ui.sbox_pin": "Épingler en haut de la boîte S",
|
||||
"ui.sbox_unpin": "Désépingler",
|
||||
@@ -1898,6 +1873,7 @@
|
||||
"ui.selection_shift_add_only": "Maj+clic ajoute sans retirer",
|
||||
"ui.selection_shift_add_only_tip": "Activé : Maj ajoute des éléments, Ctrl/Cmd bascule. Désactivé (par défaut) : Maj et Ctrl/Cmd basculent tous deux.",
|
||||
"ui.send": "Envoyer",
|
||||
"ui.send_to_slicer": "Envoyer au slicer",
|
||||
"ui.sent_to_slicer": "Envoyé à {slicer}",
|
||||
"ui.sheet_gap": "Jeu",
|
||||
"ui.shell": "Coque",
|
||||
@@ -1936,7 +1912,6 @@
|
||||
"ui.spline": "Spline",
|
||||
"ui.split_body": "Scinder le corps",
|
||||
"ui.stand_up_tip": "Après l'ouverture d'un fichier STEP ou IGES avec Z vers le haut, proposer un quart de tour autour de X",
|
||||
"ui.standard": "Standard",
|
||||
"ui.start": "Début",
|
||||
"ui.start_over": "Recommencer",
|
||||
"ui.start_page": "Page d’accueil",
|
||||
@@ -1989,7 +1964,6 @@
|
||||
"ui.untitled": "Sans titre",
|
||||
"ui.up_axis_note": "Axe orienté vers le haut dans les vues (vue d'axe, Accueil, Vue normale). Les conceptions ne sont pas modifiées.",
|
||||
"ui.updateToCurrentView": "Mettre à jour avec la vue actuelle",
|
||||
"ui.upload": "Envoyer",
|
||||
"ui.view_centre": "Centre de la vue",
|
||||
"ui.visual_shaded_hidden_edges": "Ombré avec arêtes cachées",
|
||||
"ui.visual_shaded_with_edges": "Ombré avec arêtes",
|
||||
|
||||
+5
-25
@@ -72,7 +72,6 @@
|
||||
"Choose…": "Escoller…",
|
||||
"Circular": "Circular",
|
||||
"Circumference": "Circunferencia",
|
||||
"Clockwise": "Horario",
|
||||
"Close draft": "Pechar o borrador",
|
||||
"Collisions": "Colisións",
|
||||
"Colour": "Cor",
|
||||
@@ -323,7 +322,6 @@
|
||||
"Search commands": "Buscar ordes",
|
||||
"Search this library": "Buscar nesta biblioteca",
|
||||
"Section": "Sección",
|
||||
"Section position": "Posición da sección",
|
||||
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Seleccionar outro: mantén o botón esquerdo quieto sobre o modelo (ou clic dereito › Seleccionar outro…) para listar todo o que hai baixo o cursor; fai clic de novo no mesmo punto para seleccionar o seguinte. Fai dobre clic co botón central para ir ao Inicio (Preferencias › Navegación).",
|
||||
"Select bodies (a face picks its body)": "Selecciona corpos (unha cara escolle o seu corpo)",
|
||||
"Select bodies or faces instead": "Seleccionar corpos ou caras no seu lugar",
|
||||
@@ -335,7 +333,6 @@
|
||||
"Show Agent Cursor": "Mostrar o cursor do axente",
|
||||
"Show in folder": "Mostrar no cartafol",
|
||||
"Show the pointer an AI agent moves (#35)": "Mostrar o punteiro que move un axente de IA (#35)",
|
||||
"Show tool at cursor": "Mostrar a ferramenta xunto ao cursor",
|
||||
"Signed axes": "Eixes con signo",
|
||||
"Size": "Tamaño",
|
||||
"Sketch constraints": "Restricións do bosquexo",
|
||||
@@ -348,11 +345,9 @@
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft pechouse antes de gardar estes deseños. Pódese abrir a súa última gravación automática.",
|
||||
"Specialise": "Especializar",
|
||||
"Speed": "Velocidade",
|
||||
"Square": "Cadrado",
|
||||
"Stand it up": "Poñela de pé",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Poñela de pé: un cuarto de volta arredor de X, como unha operación Mover que podes desfacer.",
|
||||
"Stand this part up?": "Poñer esta peza de pé?",
|
||||
"Start angle": "Ángulo inicial",
|
||||
"Start bulge": "Abaulamento inicial",
|
||||
"Start continuity": "Continuidade inicial",
|
||||
"Step": "Paso",
|
||||
@@ -417,7 +412,6 @@
|
||||
"angle, height": "ángulo, altura",
|
||||
"appearance.dielectric_ior": "IOR dieléctrico",
|
||||
"appearance.metallic": "Metálico",
|
||||
"appearance.only_available": "Só aparencias dispoñibles",
|
||||
"appearance.pbr_help": "Ve esta superficie en Render ou nun PNG de produto. Sen refracción nin transmisión.",
|
||||
"appearance.pbr_opaque": "PBR opaco",
|
||||
"appearance.roughness": "Rugosidade",
|
||||
@@ -457,7 +451,6 @@
|
||||
"cmd.canvas_delete": "Eliminar o lenzo",
|
||||
"cmd.canvas_edit": "Editar o lenzo",
|
||||
"cmd.canvas_insert": "Lenzo",
|
||||
"cmd.canvas_to_sketch": "Bosquexo desde o lenzo",
|
||||
"cmd.component_activate": "Activar o compoñente",
|
||||
"cmd.component_break_link": "Romper a ligazón",
|
||||
"cmd.component_capture_position": "Capturar a posición",
|
||||
@@ -535,7 +528,6 @@
|
||||
"cmd.file_export": "Exportar",
|
||||
"cmd.file_insert_mesh": "Inserir malla",
|
||||
"cmd.file_insert_step": "Inserir STEP",
|
||||
"cmd.file_open_in_easyprint": "Abrir con EasyPrint",
|
||||
"cmd.file_save_mesh": "Gardar como malla",
|
||||
"cmd.file_slicers": "Laminadores instalados",
|
||||
"cmd.frame_member": "Membro estrutural",
|
||||
@@ -581,7 +573,6 @@
|
||||
"cmd.material_create": "Material físico novo",
|
||||
"cmd.material_delete": "Eliminar o material físico",
|
||||
"cmd.material_edit": "Editar o material físico",
|
||||
"cmd.material_inspect": "Inspeccionar o material físico",
|
||||
"cmd.material_list": "Listar os materiais",
|
||||
"cmd.material_properties_directional": "Módulo elástico direccional",
|
||||
"cmd.material_properties_validate": "Validar os datos do material físico",
|
||||
@@ -973,7 +964,6 @@
|
||||
"motion.key_here": "Inserir fotograma clave aquí",
|
||||
"motion.key_here_hint": "Engadir un fotograma clave neste paso co valor actual da unión",
|
||||
"motion.no_collisions": "Sen colisións durante o estudo",
|
||||
"motion.plot_quantity": "Cantidade",
|
||||
"motion.step_back": "Paso atrás",
|
||||
"motion.step_forward": "Paso adiante",
|
||||
"motion.to_end": "Ao final",
|
||||
@@ -1328,7 +1318,7 @@
|
||||
"ui.agent_chat_pop_out": "Abrir nunha xanela propia, que podes mover a outra pantalla",
|
||||
"ui.agent_chat_previous_picture": "Imaxe anterior",
|
||||
"ui.agent_chat_privacy": "Nada vai a ningún sitio agás ao axente que executas, coa túa propia conta.",
|
||||
"ui.agent_chat_privacy_note": "O chat non envía nada por si mesmo a ningún sitio: o axente que executas fala co seu propio servizo coa túa propia conta. SolidCraft nunca ve nin garda ningunha clave nin contrasinal.",
|
||||
"ui.agent_chat_privacy_note": "SolidCraft non envía nada a ningún sitio: o axente que executas fala co seu propio servizo coa túa propia conta. SolidCraft nunca ve nin garda ningunha clave nin contrasinal.",
|
||||
"ui.agent_chat_queue": "Pór na cola",
|
||||
"ui.agent_chat_queue_full": "Hai demasiadas mensaxes agardando: quita unha ou agarda polo axente.",
|
||||
"ui.agent_chat_queued": "Na cola",
|
||||
@@ -1392,7 +1382,6 @@
|
||||
"ui.ai_http_busy": "o punto de acceso está ocupado ou superou o seu límite; téntao de novo máis tarde",
|
||||
"ui.ai_http_key": "o punto de acceso rexeitou a clave: compróbaa en Preferencias > IA",
|
||||
"ui.ai_http_not_found": "non existe ese punto de acceso ou modelo: comproba o URL base e o nome do modelo",
|
||||
"ui.ai_http_too_large": "a conversa é demasiado grande para este punto de acceso; comeza un chat novo",
|
||||
"ui.ai_key": "Clave da API",
|
||||
"ui.ai_key_hint": "opcional; gárdase no chaveiro do sistema",
|
||||
"ui.ai_key_needs_https": "A clave só se envía por HTTPS ou a este ordenador.",
|
||||
@@ -1420,6 +1409,7 @@
|
||||
"ui.ai_permissions": "Permisos",
|
||||
"ui.ai_preset": "Axuste predefinido",
|
||||
"ui.ai_remove": "Quitar",
|
||||
"ui.ai_request_limit": "detívose tras 24 solicitudes para unha mensaxe; envía outra mensaxe para continuar",
|
||||
"ui.ai_reset": "Restablecer",
|
||||
"ui.ai_settings": "Axustes de IA…",
|
||||
"ui.ai_show_features": "Mostrar as funcións de IA",
|
||||
@@ -1513,7 +1503,6 @@
|
||||
"ui.command_done": "{command} feito",
|
||||
"ui.command_palette_hint": "Escribe unha orde, p. ex. {example} — ou preme S para buscar",
|
||||
"ui.concentric": "Concéntrica",
|
||||
"ui.cone": "Cono",
|
||||
"ui.conic_curve": "Curva cónica",
|
||||
"ui.constant": "Constante",
|
||||
"ui.constraints_conflict": "Conflito de restricións",
|
||||
@@ -1572,11 +1561,6 @@
|
||||
"ui.draft": "Ángulo de desmoldaxe",
|
||||
"ui.drawing": "Debuxo",
|
||||
"ui.drill_point": "Punta de broca",
|
||||
"ui.easyprint_consent": "EasyPrint é o slicer de Printables no navegador. Para abrir o modelo alí, SolidCraft cárgao nos servidores de Printables. A ligazón caduca aos 30 minutos e terás que ter a sesión iniciada en Printables. Continuar?",
|
||||
"ui.easyprint_failed": "Non se puido cargar a EasyPrint: {error}",
|
||||
"ui.easyprint_opened": "Aberto en EasyPrint",
|
||||
"ui.easyprint_row": "EasyPrint (Printables, no navegador)",
|
||||
"ui.easyprint_uploading": "Cargando a Printables…",
|
||||
"ui.edit": "Editar",
|
||||
"ui.editJointLimits": "Editar os límites da unión",
|
||||
"ui.edit_section_analysis": "Editar a análise de sección",
|
||||
@@ -1602,8 +1586,6 @@
|
||||
"ui.filter_commands_or_keys": "Filtrar ordes ou teclas",
|
||||
"ui.findInTimeline": "Buscar na liña de tempo",
|
||||
"ui.findInWindow": "Buscar na xanela",
|
||||
"ui.fine": "Fina",
|
||||
"ui.finest": "Moi fina",
|
||||
"ui.finish_sketch": "Rematar o bosquexo",
|
||||
"ui.fit": "Axustar",
|
||||
"ui.fix_unfix": "Fixar/Liberar",
|
||||
@@ -1633,7 +1615,6 @@
|
||||
"ui.heading_circular_pattern": "PATRÓN CIRCULAR",
|
||||
"ui.heading_coil": "HÉLICE",
|
||||
"ui.heading_combine": "COMBINAR",
|
||||
"ui.heading_cone": "CONO",
|
||||
"ui.heading_configure": "CONFIGURAR",
|
||||
"ui.heading_constraints": "RESTRICIÓNS",
|
||||
"ui.heading_construct": "CONSTRUÍR",
|
||||
@@ -1693,7 +1674,6 @@
|
||||
"ui.heading_torus": "TORO",
|
||||
"ui.heading_utilities": "UTILIDADES",
|
||||
"ui.heading_utility": "UTILIDADE",
|
||||
"ui.heading_weldments": "SOLDADURA",
|
||||
"ui.help": "Axuda",
|
||||
"ui.hide": "Ocultar",
|
||||
"ui.hideAxes": "Ocultar os eixes",
|
||||
@@ -1830,7 +1810,6 @@
|
||||
"ui.project_face_edges_sketch": "Proxectar as arestas da cara en bosquexos novos",
|
||||
"ui.properties": "Propiedades",
|
||||
"ui.pull_direction": "Dirección de extracción",
|
||||
"ui.quality": "Calidade",
|
||||
"ui.quantity": "Cantidade",
|
||||
"ui.quantity_2": "Cantidade 2",
|
||||
"ui.quit": "Saír",
|
||||
@@ -1869,7 +1848,9 @@
|
||||
"ui.right": "Dereita",
|
||||
"ui.save": "Gardar",
|
||||
"ui.save_as": "Gardar como",
|
||||
"ui.save_mesh_no_slicer": "Non se atopou ningún laminador: escolle Personalizado… e selecciona o teu programa laminador, ou desactiva Enviar ao laminador",
|
||||
"ui.save_mesh_pick_bodies": "Selecciona os corpos que queres gardar",
|
||||
"ui.save_mesh_temp_failed": "Non se puido crear {folder}: {error}",
|
||||
"ui.saved_to": "Gardado {path}",
|
||||
"ui.sbox_pin": "Fixar arriba na caixa S",
|
||||
"ui.sbox_unpin": "Desfixar",
|
||||
@@ -1884,6 +1865,7 @@
|
||||
"ui.selection_shift_add_only": "Maiús+clic engade sen quitar",
|
||||
"ui.selection_shift_add_only_tip": "Activado: Maiús engade elementos, Ctrl/Cmd alterna. Desactivado (predeterminado): Maiús e Ctrl/Cmd alternan ambos.",
|
||||
"ui.send": "Enviar",
|
||||
"ui.send_to_slicer": "Enviar ao laminador",
|
||||
"ui.sent_to_slicer": "Enviado a {slicer}",
|
||||
"ui.sheet_gap": "Separación",
|
||||
"ui.shell": "Baleirado",
|
||||
@@ -1922,7 +1904,6 @@
|
||||
"ui.spline": "Spline",
|
||||
"ui.split_body": "Dividir o corpo",
|
||||
"ui.stand_up_tip": "Despois de abrir un ficheiro STEP ou IGES con Z cara arriba, ofrecer un cuarto de volta arredor de X",
|
||||
"ui.standard": "Estándar",
|
||||
"ui.start": "Inicio",
|
||||
"ui.start_over": "Volver comezar",
|
||||
"ui.start_page": "Páxina de inicio",
|
||||
@@ -1975,7 +1956,6 @@
|
||||
"ui.untitled": "Sen título",
|
||||
"ui.up_axis_note": "Que eixe apunta cara arriba nas vistas (vista de eixe, Inicio, Mirar cara a). Os deseños non cambian.",
|
||||
"ui.updateToCurrentView": "Actualizar á vista actual",
|
||||
"ui.upload": "Cargar",
|
||||
"ui.view_centre": "Centro da vista",
|
||||
"ui.visual_shaded_hidden_edges": "Sombreado con arestas ocultas",
|
||||
"ui.visual_shaded_with_edges": "Sombreado con arestas",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+5
-25
@@ -72,7 +72,6 @@
|
||||
"Choose…": "Válasszon…",
|
||||
"Circular": "Kör alakú",
|
||||
"Circumference": "Kerület",
|
||||
"Clockwise": "Óramutató járásával megegyező",
|
||||
"Close draft": "Vázlat bezárása",
|
||||
"Collisions": "Ütközések",
|
||||
"Colour": "Szín",
|
||||
@@ -322,7 +321,6 @@
|
||||
"Search commands": "Parancsok keresése",
|
||||
"Search this library": "Keresés ebben a könyvtárban",
|
||||
"Section": "Metszet",
|
||||
"Section position": "Szelvény helyzete",
|
||||
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Más kijelölése: tartsa nyomva a bal gombot egy helyen a modellen (vagy jobb kattintás › Más kijelölése…) a kurzor alatti elemek listázásához; kattintson újra ugyanarra a helyre a következő elem kijelöléséhez. A középső gomb dupla kattintása a kezdőnézetre vált (Beállítások › Navigáció).",
|
||||
"Select bodies (a face picks its body)": "Testek kijelölése (egy lap a testét jelöli ki)",
|
||||
"Select bodies or faces instead": "Inkább testek vagy lapok kijelölése",
|
||||
@@ -334,7 +332,6 @@
|
||||
"Show Agent Cursor": "Ügynök kurzorának megjelenítése",
|
||||
"Show in folder": "Megjelenítés a mappában",
|
||||
"Show the pointer an AI agent moves (#35)": "Az AI-ügynök által mozgatott mutató megjelenítése (#35)",
|
||||
"Show tool at cursor": "Eszköz megjelenítése a kurzornál",
|
||||
"Signed axes": "Előjeles tengelyek",
|
||||
"Size": "Méret",
|
||||
"Sketch constraints": "Vázlatkényszerek",
|
||||
@@ -347,11 +344,9 @@
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "A SolidCraft bezárult, mielőtt ezek a tervek mentve lettek volna. Az utolsó automatikus mentésük megnyitható.",
|
||||
"Specialise": "Specializálás",
|
||||
"Speed": "Sebesség",
|
||||
"Square": "Négyzet",
|
||||
"Stand it up": "Felállítás",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Felállítás: negyedfordulat az X körül, egyetlen visszavonható Mozgatás elemként.",
|
||||
"Stand this part up?": "Felállítja ezt az alkatrészt?",
|
||||
"Start angle": "Kezdőszög",
|
||||
"Start bulge": "Kezdő ívelés",
|
||||
"Start continuity": "Kezdő folytonosság",
|
||||
"Step": "Lépés",
|
||||
@@ -416,7 +411,6 @@
|
||||
"angle, height": "szög, magasság",
|
||||
"appearance.dielectric_ior": "Dielektromos törésmutató",
|
||||
"appearance.metallic": "Fémes",
|
||||
"appearance.only_available": "Csak elérhető megjelenések",
|
||||
"appearance.pbr_help": "Tekintse meg ezt a felületet Renderelésben vagy termék-PNG-ben. Nincs fénytörés vagy áteresztés.",
|
||||
"appearance.pbr_opaque": "Átlátszatlan PBR",
|
||||
"appearance.roughness": "Érdesség",
|
||||
@@ -456,7 +450,6 @@
|
||||
"cmd.canvas_delete": "Vászon törlése",
|
||||
"cmd.canvas_edit": "Vászon szerkesztése",
|
||||
"cmd.canvas_insert": "Vászon",
|
||||
"cmd.canvas_to_sketch": "Vázlat vászonból",
|
||||
"cmd.component_activate": "Komponens aktiválása",
|
||||
"cmd.component_break_link": "Kapcsolat megszakítása",
|
||||
"cmd.component_capture_position": "Pozíció rögzítése",
|
||||
@@ -534,7 +527,6 @@
|
||||
"cmd.file_export": "Exportálás",
|
||||
"cmd.file_insert_mesh": "Háló beszúrása",
|
||||
"cmd.file_insert_step": "STEP beszúrása",
|
||||
"cmd.file_open_in_easyprint": "Megnyitás EasyPrinttel",
|
||||
"cmd.file_save_mesh": "Mentés hálóként",
|
||||
"cmd.file_slicers": "Telepített szeletelők",
|
||||
"cmd.frame_member": "Szerkezeti elem",
|
||||
@@ -580,7 +572,6 @@
|
||||
"cmd.material_create": "Új fizikai anyag",
|
||||
"cmd.material_delete": "Fizikai anyag törlése",
|
||||
"cmd.material_edit": "Fizikai anyag szerkesztése",
|
||||
"cmd.material_inspect": "Fizikai anyag vizsgálata",
|
||||
"cmd.material_list": "Anyagok listázása",
|
||||
"cmd.material_properties_directional": "Irányfüggő rugalmassági modulus",
|
||||
"cmd.material_properties_validate": "Fizikai anyagadatok ellenőrzése",
|
||||
@@ -972,7 +963,6 @@
|
||||
"motion.key_here": "Kulcs ide",
|
||||
"motion.key_here_hint": "Kulcs hozzáadása ennél a lépésnél a csukló jelenlegi értékével",
|
||||
"motion.no_collisions": "Nincs ütközés a vizsgálat során",
|
||||
"motion.plot_quantity": "Mennyiség",
|
||||
"motion.step_back": "Lépés vissza",
|
||||
"motion.step_forward": "Lépés tovább",
|
||||
"motion.to_end": "A végére",
|
||||
@@ -1271,7 +1261,7 @@
|
||||
"ui.agent_chat_pop_out": "Megnyitás saját ablakban, amelyet másik képernyőre mozgathat",
|
||||
"ui.agent_chat_previous_picture": "Előző kép",
|
||||
"ui.agent_chat_privacy": "Semmi sem megy sehová, csak az Ön által futtatott ügynökhöz, az Ön saját fiókja alatt.",
|
||||
"ui.agent_chat_privacy_note": "A csevegés önmagától semmit sem küld sehová: az Ön által futtatott ügynök a saját szolgáltatásával kommunikál, az Ön saját fiókja alatt. A SolidCraft soha nem lát és nem tárol kulcsot vagy jelszót.",
|
||||
"ui.agent_chat_privacy_note": "A SolidCraft semmit sem küld sehová: az Ön által futtatott ügynök a saját szolgáltatásával kommunikál, az Ön saját fiókja alatt. A SolidCraft soha nem lát és nem tárol kulcsot vagy jelszót.",
|
||||
"ui.agent_chat_queue": "Sorba",
|
||||
"ui.agent_chat_queue_full": "Túl sok üzenet vár: távolítson el egyet, vagy várja meg az ügynököt.",
|
||||
"ui.agent_chat_queued": "Sorban",
|
||||
@@ -1335,7 +1325,6 @@
|
||||
"ui.ai_http_busy": "a végpont foglalt vagy túllépte a korlátját; próbálja később",
|
||||
"ui.ai_http_key": "a végpont elutasította a kulcsot: ellenőrizze a Beállítások > AI menüben",
|
||||
"ui.ai_http_not_found": "nincs ilyen végpont vagy modell: ellenőrizze az alap-URL-t és a modell nevét",
|
||||
"ui.ai_http_too_large": "a beszélgetés túl nagy ehhez a végponthoz; kezdjen új csevegést",
|
||||
"ui.ai_key": "API-kulcs",
|
||||
"ui.ai_key_hint": "opcionális; a rendszer kulcstartójában tárolódik",
|
||||
"ui.ai_key_needs_https": "A kulcs csak HTTPS-en vagy erre a számítógépre kerül elküldésre.",
|
||||
@@ -1363,6 +1352,7 @@
|
||||
"ui.ai_permissions": "Jogosultságok",
|
||||
"ui.ai_preset": "Előbeállítás",
|
||||
"ui.ai_remove": "Eltávolítás",
|
||||
"ui.ai_request_limit": "egy üzenetre eső 24 kérés után leállt; küldjön újabb üzenetet a folytatáshoz",
|
||||
"ui.ai_reset": "Visszaállítás",
|
||||
"ui.ai_settings": "AI-beállítások…",
|
||||
"ui.ai_show_features": "AI-funkciók megjelenítése",
|
||||
@@ -1455,7 +1445,6 @@
|
||||
"ui.command_done": "{command} kész",
|
||||
"ui.command_palette_hint": "Írjon be egy parancsot, pl. {example} – vagy nyomja meg az S-t a kereséshez",
|
||||
"ui.concentric": "Koncentrikus",
|
||||
"ui.cone": "Kúp",
|
||||
"ui.conic_curve": "Kúpgörbe",
|
||||
"ui.constant": "Állandó",
|
||||
"ui.constraints_conflict": "Kényszerek ütköznek",
|
||||
@@ -1514,11 +1503,6 @@
|
||||
"ui.draft": "Dőlés",
|
||||
"ui.drawing": "Rajz",
|
||||
"ui.drill_point": "Fúrócsúcs",
|
||||
"ui.easyprint_consent": "Az EasyPrint a Printables szeletelője a böngészőben. A modell ott való megnyitásához a SolidCraft feltölti azt a Printables szervereire. A hivatkozás 30 perc után lejár, és be kell jelentkeznie a Printablesbe. Folytatja?",
|
||||
"ui.easyprint_failed": "Az EasyPrintre feltöltés sikertelen: {error}",
|
||||
"ui.easyprint_opened": "Megnyitva az EasyPrintben",
|
||||
"ui.easyprint_row": "EasyPrint (Printables, a böngészőben)",
|
||||
"ui.easyprint_uploading": "Feltöltés a Printablesbe…",
|
||||
"ui.edit": "Szerkesztés",
|
||||
"ui.editJointLimits": "Csuklókorlátok szerkesztése",
|
||||
"ui.edit_section_analysis": "Metszetelemzés szerkesztése",
|
||||
@@ -1544,8 +1528,6 @@
|
||||
"ui.filter_commands_or_keys": "Parancsok vagy billentyűk szűrése",
|
||||
"ui.findInTimeline": "Keresés az idővonalon",
|
||||
"ui.findInWindow": "Keresés az ablakban",
|
||||
"ui.fine": "Finom",
|
||||
"ui.finest": "Legfinomabb",
|
||||
"ui.finish_sketch": "Vázlat befejezése",
|
||||
"ui.fit": "Illesztés",
|
||||
"ui.fix_unfix": "Rögzítés/feloldás",
|
||||
@@ -1575,7 +1557,6 @@
|
||||
"ui.heading_circular_pattern": "KÖRMINTA",
|
||||
"ui.heading_coil": "SPIRÁL",
|
||||
"ui.heading_combine": "EGYESÍTÉS",
|
||||
"ui.heading_cone": "KÚP",
|
||||
"ui.heading_configure": "KONFIGURÁLÁS",
|
||||
"ui.heading_constraints": "KÉNYSZEREK",
|
||||
"ui.heading_construct": "SZERKESZTÉS",
|
||||
@@ -1635,7 +1616,6 @@
|
||||
"ui.heading_torus": "TÓRUSZ",
|
||||
"ui.heading_utilities": "SEGÉDESZKÖZÖK",
|
||||
"ui.heading_utility": "SEGÉDESZKÖZ",
|
||||
"ui.heading_weldments": "HEGESZTÉS",
|
||||
"ui.help": "Súgó",
|
||||
"ui.hide": "Elrejtés",
|
||||
"ui.hideAxes": "Tengelyek elrejtése",
|
||||
@@ -1772,7 +1752,6 @@
|
||||
"ui.project_face_edges_sketch": "Lapélek vetítése új vázlatokba",
|
||||
"ui.properties": "Tulajdonságok",
|
||||
"ui.pull_direction": "Húzási irány",
|
||||
"ui.quality": "Minőség",
|
||||
"ui.quantity": "Mennyiség",
|
||||
"ui.quantity_2": "2. mennyiség",
|
||||
"ui.quit": "Kilépés",
|
||||
@@ -1811,7 +1790,9 @@
|
||||
"ui.right": "Jobb",
|
||||
"ui.save": "Mentés",
|
||||
"ui.save_as": "Mentés másként",
|
||||
"ui.save_mesh_no_slicer": "Nem található szeletelő: válassza az Egyéni… lehetőséget és jelölje ki a szeletelőprogramot, vagy kapcsolja ki a Küldés szeletelőnek beállítást",
|
||||
"ui.save_mesh_pick_bodies": "Jelölje ki a mentendő testeket",
|
||||
"ui.save_mesh_temp_failed": "A(z) {folder} nem hozható létre: {error}",
|
||||
"ui.saved_to": "Mentve: {path}",
|
||||
"ui.sbox_pin": "Rögzítés az S doboz tetejére",
|
||||
"ui.sbox_unpin": "Rögzítés feloldása",
|
||||
@@ -1826,6 +1807,7 @@
|
||||
"ui.selection_shift_add_only": "Shift+kattintás eltávolítás nélkül hozzáad",
|
||||
"ui.selection_shift_add_only_tip": "Be: a Shift hozzáad, a Ctrl/Cmd vált. Ki (alapért.): a Shift és a Ctrl/Cmd is vált.",
|
||||
"ui.send": "Küldés",
|
||||
"ui.send_to_slicer": "Küldés szeletelőnek",
|
||||
"ui.sent_to_slicer": "Elküldve ide: {slicer}",
|
||||
"ui.sheet_gap": "Rés",
|
||||
"ui.shell": "Héjazás",
|
||||
@@ -1864,7 +1846,6 @@
|
||||
"ui.spline": "Spline",
|
||||
"ui.split_body": "Test felosztása",
|
||||
"ui.stand_up_tip": "Miután egy STEP vagy IGES fájl Z felfelé tájolással megnyílik, negyedfordulat felajánlása az X körül",
|
||||
"ui.standard": "Normál",
|
||||
"ui.start": "Kezdés",
|
||||
"ui.start_over": "Újrakezdés",
|
||||
"ui.start_page": "Kezdőoldal",
|
||||
@@ -1917,7 +1898,6 @@
|
||||
"ui.untitled": "Névtelen",
|
||||
"ui.up_axis_note": "Melyik tengely mutat felfelé a nézetekben (tengelynézet, Kezdőlap, Ránézés). A tervek változatlanok.",
|
||||
"ui.updateToCurrentView": "Frissítés az aktuális nézetre",
|
||||
"ui.upload": "Feltöltés",
|
||||
"ui.view_centre": "Nézet középpontja",
|
||||
"ui.visual_shaded_hidden_edges": "Árnyalt rejtett élekkel",
|
||||
"ui.visual_shaded_with_edges": "Árnyalt élekkel",
|
||||
|
||||
+5
-25
@@ -72,7 +72,6 @@
|
||||
"Choose…": "Pilih…",
|
||||
"Circular": "Melingkar",
|
||||
"Circumference": "Keliling",
|
||||
"Clockwise": "Searah jarum jam",
|
||||
"Close draft": "Tutup draf",
|
||||
"Collisions": "Tabrakan",
|
||||
"Colour": "Warna",
|
||||
@@ -322,7 +321,6 @@
|
||||
"Search commands": "Cari perintah",
|
||||
"Search this library": "Cari di pustaka ini",
|
||||
"Section": "Penampang",
|
||||
"Section position": "Posisi penampang",
|
||||
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Select Other: tahan tombol kiri diam di model (atau klik kanan › Select Other…) untuk mendaftar semua yang ada di bawah kursor; klik lagi di titik yang sama untuk memilih benda berikutnya. Klik dua kali tombol tengah untuk ke Home (Preferensi › Navigasi).",
|
||||
"Select bodies (a face picks its body)": "Pilih bodi (memilih muka berarti memilih bodinya)",
|
||||
"Select bodies or faces instead": "Pilih bodi atau muka saja",
|
||||
@@ -334,7 +332,6 @@
|
||||
"Show Agent Cursor": "Tampilkan Kursor Agen",
|
||||
"Show in folder": "Tampilkan di folder",
|
||||
"Show the pointer an AI agent moves (#35)": "Tampilkan penunjuk yang digerakkan agen AI (#35)",
|
||||
"Show tool at cursor": "Tampilkan alat di dekat kursor",
|
||||
"Signed axes": "Sumbu bertanda",
|
||||
"Size": "Ukuran",
|
||||
"Sketch constraints": "Batasan sketsa",
|
||||
@@ -347,11 +344,9 @@
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft tertutup sebelum desain ini disimpan. Simpan otomatis terakhirnya dapat dibuka.",
|
||||
"Specialise": "Spesialisasikan",
|
||||
"Speed": "Kecepatan",
|
||||
"Square": "Persegi",
|
||||
"Stand it up": "Tegakkan",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Tegakkan: putaran seperempat terhadap X, sebagai satu fitur Move yang dapat dibatalkan.",
|
||||
"Stand this part up?": "Tegakkan part ini?",
|
||||
"Start angle": "Sudut awal",
|
||||
"Start bulge": "Bulge awal",
|
||||
"Start continuity": "Kontinuitas awal",
|
||||
"Step": "Langkah",
|
||||
@@ -416,7 +411,6 @@
|
||||
"angle, height": "sudut, tinggi",
|
||||
"appearance.dielectric_ior": "IOR Dielektrik",
|
||||
"appearance.metallic": "Metalik",
|
||||
"appearance.only_available": "Hanya tampilan yang tersedia",
|
||||
"appearance.pbr_help": "Lihat permukaan ini di Render atau PNG produk. Tanpa refraksi atau transmisi.",
|
||||
"appearance.pbr_opaque": "PBR Opak",
|
||||
"appearance.roughness": "Kekasaran",
|
||||
@@ -456,7 +450,6 @@
|
||||
"cmd.canvas_delete": "Hapus Kanvas",
|
||||
"cmd.canvas_edit": "Edit Kanvas",
|
||||
"cmd.canvas_insert": "Kanvas",
|
||||
"cmd.canvas_to_sketch": "Kanvas ke Sketsa",
|
||||
"cmd.component_activate": "Aktifkan Komponen",
|
||||
"cmd.component_break_link": "Putuskan Tautan",
|
||||
"cmd.component_capture_position": "Tangkap Posisi",
|
||||
@@ -534,7 +527,6 @@
|
||||
"cmd.file_export": "Ekspor",
|
||||
"cmd.file_insert_mesh": "Sisipkan Mesh",
|
||||
"cmd.file_insert_step": "Sisipkan STEP",
|
||||
"cmd.file_open_in_easyprint": "Buka dengan EasyPrint",
|
||||
"cmd.file_save_mesh": "Simpan Sebagai Mesh",
|
||||
"cmd.file_slicers": "Slicer Terpasang",
|
||||
"cmd.frame_member": "Batang Rangka",
|
||||
@@ -580,7 +572,6 @@
|
||||
"cmd.material_create": "Material Fisik Baru",
|
||||
"cmd.material_delete": "Hapus Material Fisik",
|
||||
"cmd.material_edit": "Edit Material Fisik",
|
||||
"cmd.material_inspect": "Periksa Material Fisik",
|
||||
"cmd.material_list": "Daftar Material",
|
||||
"cmd.material_properties_directional": "Modulus Elastis Arah",
|
||||
"cmd.material_properties_validate": "Validasi Data Material Fisik",
|
||||
@@ -972,7 +963,6 @@
|
||||
"motion.key_here": "Key di sini",
|
||||
"motion.key_here_hint": "Tambah key pada langkah ini dengan nilai sambungan saat ini",
|
||||
"motion.no_collisions": "Tidak ada tabrakan sepanjang studi",
|
||||
"motion.plot_quantity": "Jumlah",
|
||||
"motion.step_back": "Mundur satu langkah",
|
||||
"motion.step_forward": "Maju satu langkah",
|
||||
"motion.to_end": "Ke akhir",
|
||||
@@ -1271,7 +1261,7 @@
|
||||
"ui.agent_chat_pop_out": "Buka di jendela sendiri, yang dapat dipindah ke layar lain",
|
||||
"ui.agent_chat_previous_picture": "Gambar sebelumnya",
|
||||
"ui.agent_chat_privacy": "Tidak ada yang dikirim ke mana pun kecuali ke agen yang Anda jalankan, di bawah akun Anda sendiri.",
|
||||
"ui.agent_chat_privacy_note": "Obrolan tidak mengirim apa pun ke mana pun dengan sendirinya: agen yang Anda jalankan berbicara ke layanannya sendiri di bawah akun Anda sendiri. SolidCraft tidak pernah melihat atau menyimpan key atau kata sandi.",
|
||||
"ui.agent_chat_privacy_note": "SolidCraft tidak mengirim apa pun ke mana pun: agen yang Anda jalankan berbicara ke layanannya sendiri di bawah akun Anda sendiri. SolidCraft tidak pernah melihat atau menyimpan key atau kata sandi.",
|
||||
"ui.agent_chat_queue": "Antre",
|
||||
"ui.agent_chat_queue_full": "Terlalu banyak pesan menunggu: hapus satu atau tunggu agen.",
|
||||
"ui.agent_chat_queued": "Mengantre",
|
||||
@@ -1335,7 +1325,6 @@
|
||||
"ui.ai_http_busy": "endpoint sibuk atau melebihi batas; coba lagi nanti",
|
||||
"ui.ai_http_key": "endpoint menolak key: periksa di Preferensi > AI",
|
||||
"ui.ai_http_not_found": "endpoint atau model tidak ada: periksa URL dasar dan nama model",
|
||||
"ui.ai_http_too_large": "percakapan terlalu besar untuk endpoint ini; mulai obrolan baru",
|
||||
"ui.ai_key": "Key API",
|
||||
"ui.ai_key_hint": "opsional; disimpan di keychain sistem",
|
||||
"ui.ai_key_needs_https": "Key hanya dikirim lewat HTTPS, atau ke komputer ini.",
|
||||
@@ -1363,6 +1352,7 @@
|
||||
"ui.ai_permissions": "Izin",
|
||||
"ui.ai_preset": "Preset",
|
||||
"ui.ai_remove": "Hapus",
|
||||
"ui.ai_request_limit": "dihentikan setelah 24 permintaan untuk satu pesan; kirim pesan lain untuk melanjutkan",
|
||||
"ui.ai_reset": "Reset",
|
||||
"ui.ai_settings": "Pengaturan AI…",
|
||||
"ui.ai_show_features": "Tampilkan fitur AI",
|
||||
@@ -1455,7 +1445,6 @@
|
||||
"ui.command_done": "{command} selesai",
|
||||
"ui.command_palette_hint": "Ketik perintah, mis. {example} — atau tekan S untuk mencari",
|
||||
"ui.concentric": "Konsentris",
|
||||
"ui.cone": "Kerucut",
|
||||
"ui.conic_curve": "Kurva Konik",
|
||||
"ui.constant": "Konstan",
|
||||
"ui.constraints_conflict": "Batasan bertentangan",
|
||||
@@ -1514,11 +1503,6 @@
|
||||
"ui.draft": "Draft",
|
||||
"ui.drawing": "Gambar Kerja",
|
||||
"ui.drill_point": "Ujung Bor",
|
||||
"ui.easyprint_consent": "EasyPrint adalah slicer Printables di browser. Untuk membuka model di sana, SolidCraft mengunggahnya ke server Printables. Tautan kedaluwarsa setelah 30 menit, dan Anda harus masuk ke Printables. Lanjutkan?",
|
||||
"ui.easyprint_failed": "Gagal mengunggah ke EasyPrint: {error}",
|
||||
"ui.easyprint_opened": "Dibuka di EasyPrint",
|
||||
"ui.easyprint_row": "EasyPrint (Printables, di browser)",
|
||||
"ui.easyprint_uploading": "Mengunggah ke Printables…",
|
||||
"ui.edit": "Edit",
|
||||
"ui.editJointLimits": "Edit Batas Sambungan",
|
||||
"ui.edit_section_analysis": "Edit Analisis Penampang",
|
||||
@@ -1544,8 +1528,6 @@
|
||||
"ui.filter_commands_or_keys": "Filter perintah atau tombol",
|
||||
"ui.findInTimeline": "Cari di Linimasa",
|
||||
"ui.findInWindow": "Cari di Jendela",
|
||||
"ui.fine": "Halus",
|
||||
"ui.finest": "Sangat halus",
|
||||
"ui.finish_sketch": "Selesai Sketsa",
|
||||
"ui.fit": "Pas",
|
||||
"ui.fix_unfix": "Kunci/Lepas Kunci",
|
||||
@@ -1575,7 +1557,6 @@
|
||||
"ui.heading_circular_pattern": "POLA MELINGKAR",
|
||||
"ui.heading_coil": "KOIL",
|
||||
"ui.heading_combine": "GABUNGKAN",
|
||||
"ui.heading_cone": "KERUCUT",
|
||||
"ui.heading_configure": "KONFIGURASI",
|
||||
"ui.heading_constraints": "BATASAN",
|
||||
"ui.heading_construct": "KONSTRUKSI",
|
||||
@@ -1635,7 +1616,6 @@
|
||||
"ui.heading_torus": "TORUS",
|
||||
"ui.heading_utilities": "UTILITAS",
|
||||
"ui.heading_utility": "UTILITAS",
|
||||
"ui.heading_weldments": "KONSTRUKSI LAS",
|
||||
"ui.help": "Bantuan",
|
||||
"ui.hide": "Sembunyikan",
|
||||
"ui.hideAxes": "Sembunyikan Sumbu",
|
||||
@@ -1772,7 +1752,6 @@
|
||||
"ui.project_face_edges_sketch": "Proyeksikan tepi muka ke sketsa baru",
|
||||
"ui.properties": "Properti",
|
||||
"ui.pull_direction": "Arah tarik",
|
||||
"ui.quality": "Kualitas",
|
||||
"ui.quantity": "Jumlah",
|
||||
"ui.quantity_2": "Jumlah 2",
|
||||
"ui.quit": "Keluar",
|
||||
@@ -1811,7 +1790,9 @@
|
||||
"ui.right": "Kanan",
|
||||
"ui.save": "Simpan",
|
||||
"ui.save_as": "Simpan Sebagai",
|
||||
"ui.save_mesh_no_slicer": "Slicer tidak ditemukan: pilih Kustom… dan tunjuk program slicer Anda, atau matikan Kirim ke slicer",
|
||||
"ui.save_mesh_pick_bodies": "Pilih bodi yang akan disimpan",
|
||||
"ui.save_mesh_temp_failed": "Tidak dapat membuat {folder}: {error}",
|
||||
"ui.saved_to": "Disimpan {path}",
|
||||
"ui.sbox_pin": "Sematkan di atas kotak S",
|
||||
"ui.sbox_unpin": "Lepas sematan",
|
||||
@@ -1826,6 +1807,7 @@
|
||||
"ui.selection_shift_add_only": "Shift+klik menambah tanpa menghapus",
|
||||
"ui.selection_shift_add_only_tip": "Aktif: Shift menambah item, Ctrl/Cmd mengalihkan. Mati (default): Shift dan Ctrl/Cmd sama-sama mengalihkan.",
|
||||
"ui.send": "Kirim",
|
||||
"ui.send_to_slicer": "Kirim ke slicer",
|
||||
"ui.sent_to_slicer": "Dikirim ke {slicer}",
|
||||
"ui.sheet_gap": "Celah",
|
||||
"ui.shell": "Shell",
|
||||
@@ -1864,7 +1846,6 @@
|
||||
"ui.spline": "Spline",
|
||||
"ui.split_body": "Belah Bodi",
|
||||
"ui.stand_up_tip": "Setelah file STEP atau IGES terbuka dengan Z ke atas, tawarkan putaran seperempat terhadap X",
|
||||
"ui.standard": "Standar",
|
||||
"ui.start": "Mulai",
|
||||
"ui.start_over": "Mulai ulang",
|
||||
"ui.start_page": "Halaman awal",
|
||||
@@ -1917,7 +1898,6 @@
|
||||
"ui.untitled": "Tanpa judul",
|
||||
"ui.up_axis_note": "Sumbu mana yang mengarah ke atas di tampilan (tampilan sumbu, Home, Look At). Desain tidak berubah.",
|
||||
"ui.updateToCurrentView": "Perbarui ke Tampilan Saat Ini",
|
||||
"ui.upload": "Unggah",
|
||||
"ui.view_centre": "Pusat tampilan",
|
||||
"ui.visual_shaded_hidden_edges": "Shaded dengan tepi tersembunyi",
|
||||
"ui.visual_shaded_with_edges": "Shaded dengan tepi",
|
||||
|
||||
+5
-59
@@ -72,7 +72,6 @@
|
||||
"Choose…": "Scegli…",
|
||||
"Circular": "Circolare",
|
||||
"Circumference": "Circonferenza",
|
||||
"Clockwise": "Orario",
|
||||
"Close draft": "Chiudi bozza",
|
||||
"Collisions": "Collisioni",
|
||||
"Colour": "Colore",
|
||||
@@ -164,7 +163,6 @@
|
||||
"From Bodies": "Dai corpi",
|
||||
"From edges": "Spigoli di partenza",
|
||||
"Full Length": "Lunghezza totale",
|
||||
"Full thread depth": "Profondità filetto completa",
|
||||
"Gap": "Gioco",
|
||||
"Grid": "Griglia",
|
||||
"HISTORY": "CRONOLOGIA",
|
||||
@@ -327,7 +325,6 @@
|
||||
"Search commands": "Cerca comandi",
|
||||
"Search this library": "Cerca in questa libreria",
|
||||
"Section": "Sezione",
|
||||
"Section position": "Posizione della sezione",
|
||||
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Seleziona altro: tieni premuto il pulsante sinistro senza muovere il puntatore sul modello (o fai clic destro › Seleziona altro…) per elencare gli oggetti sotto il cursore; fai di nuovo clic nello stesso punto per selezionare l'oggetto successivo. Fai doppio clic con il pulsante centrale per tornare alla vista iniziale (Preferenze › Navigazione).",
|
||||
"Select bodies (a face picks its body)": "Seleziona corpi (una faccia seleziona il corpo a cui appartiene)",
|
||||
"Select bodies or faces instead": "Seleziona invece corpi o facce",
|
||||
@@ -339,7 +336,6 @@
|
||||
"Show Agent Cursor": "Mostra cursore dell'agente",
|
||||
"Show in folder": "Mostra nella cartella",
|
||||
"Show the pointer an AI agent moves (#35)": "Mostra il puntatore mosso da un agente IA (#35)",
|
||||
"Show tool at cursor": "Mostra lo strumento accanto al cursore",
|
||||
"Signed axes": "Assi con segno",
|
||||
"Size": "Dimensioni",
|
||||
"Sketch constraints": "Vincoli dello schizzo",
|
||||
@@ -352,11 +348,9 @@
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft si è chiuso prima del salvataggio di questi progetti. È possibile aprire il loro ultimo salvataggio automatico.",
|
||||
"Specialise": "Specializza",
|
||||
"Speed": "Velocità",
|
||||
"Square": "Quadrato",
|
||||
"Stand it up": "Raddrizza",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Raddrizza: un quarto di giro attorno a X, come singola lavorazione Sposta annullabile.",
|
||||
"Stand this part up?": "Raddrizzare questa parte?",
|
||||
"Start angle": "Angolo iniziale",
|
||||
"Start bulge": "Rigonfiamento iniziale",
|
||||
"Start continuity": "Continuità iniziale",
|
||||
"Step": "Passo",
|
||||
@@ -383,7 +377,6 @@
|
||||
"This child is shared. Deleting it removes it from every copy of its parent.": "Questo elemento figlio è condiviso. Eliminandolo verrà rimosso da ogni copia del padre.",
|
||||
"This child is shared. Position changes apply to every copy of its parent.": "Questo elemento figlio è condiviso. Le modifiche alla posizione si applicano a ogni copia del padre.",
|
||||
"This face cannot provide a durable attachment. Choose another face or explicitly use a fixed plane.": "Questa faccia non consente un collegamento persistente. Scegli un'altra faccia o usa esplicitamente un piano fisso.",
|
||||
"Thread depth": "Profondità filetto",
|
||||
"To Offset": "Fino a una distanza",
|
||||
"To edges": "Spigoli di arrivo",
|
||||
"Tolerance": "Tolleranza",
|
||||
@@ -422,7 +415,6 @@
|
||||
"angle, height": "angolo, altezza",
|
||||
"appearance.dielectric_ior": "Indice di rifrazione dielettrico",
|
||||
"appearance.metallic": "Metallicità",
|
||||
"appearance.only_available": "Solo aspetti disponibili",
|
||||
"appearance.pbr_help": "Visualizza questa superficie in Rendering o in un PNG del prodotto. Senza rifrazione né trasmissione.",
|
||||
"appearance.pbr_opaque": "PBR opaco",
|
||||
"appearance.roughness": "Rugosità",
|
||||
@@ -462,7 +454,6 @@
|
||||
"cmd.canvas_delete": "Elimina immagine di riferimento",
|
||||
"cmd.canvas_edit": "Modifica immagine di riferimento",
|
||||
"cmd.canvas_insert": "Immagine di riferimento",
|
||||
"cmd.canvas_to_sketch": "Schizzo da immagine di riferimento",
|
||||
"cmd.component_activate": "Attiva componente",
|
||||
"cmd.component_break_link": "Interrompi collegamento",
|
||||
"cmd.component_capture_position": "Acquisisci posizione",
|
||||
@@ -540,7 +531,6 @@
|
||||
"cmd.file_export": "Esporta",
|
||||
"cmd.file_insert_mesh": "Inserisci mesh",
|
||||
"cmd.file_insert_step": "Inserisci STEP",
|
||||
"cmd.file_open_in_easyprint": "Apri con EasyPrint",
|
||||
"cmd.file_save_mesh": "Salva come mesh",
|
||||
"cmd.file_slicers": "Slicer installati",
|
||||
"cmd.frame_member": "Membro strutturale",
|
||||
@@ -586,7 +576,6 @@
|
||||
"cmd.material_create": "Nuovo materiale fisico",
|
||||
"cmd.material_delete": "Elimina materiale fisico",
|
||||
"cmd.material_edit": "Modifica materiale fisico",
|
||||
"cmd.material_inspect": "Ispeziona materiale fisico",
|
||||
"cmd.material_list": "Elenca materiali",
|
||||
"cmd.material_properties_directional": "Modulo elastico direzionale",
|
||||
"cmd.material_properties_validate": "Convalida dati del materiale fisico",
|
||||
@@ -875,26 +864,6 @@
|
||||
"help.mit": "Licenza MIT",
|
||||
"help.notice": "NOTICE",
|
||||
"help.third_party": "Licenze di terze parti",
|
||||
"hole_presets.confirm_delete": "Conferma eliminazione",
|
||||
"hole_presets.delete": "Elimina",
|
||||
"hole_presets.delete_prompt": "Eliminare questo preset personale? I fori esistenti restano invariati.",
|
||||
"hole_presets.full": "La raccolta personale contiene già 64 preset.",
|
||||
"hole_presets.invalid_name": "Inserisci un nome breve, senza a capo o caratteri di controllo.",
|
||||
"hole_presets.invalid_parameters": "Completa i parametri del foro con espressioni valide e di lunghezza ragionevole.",
|
||||
"hole_presets.missing": "Il preset selezionato non è più disponibile.",
|
||||
"hole_presets.name": "Nome preset",
|
||||
"hole_presets.name_exists": "Esiste già un preset con questo nome. Usa un nome diverso.",
|
||||
"hole_presets.name_hint": "Un nome univoco",
|
||||
"hole_presets.overwrite": "Salva le modifiche nel preset selezionato",
|
||||
"hole_presets.preset": "Preset",
|
||||
"hole_presets.recall": "Richiama",
|
||||
"hole_presets.rename": "Rinomina",
|
||||
"hole_presets.save": "Salva nuovo",
|
||||
"hole_presets.scope": "Salvati nel tuo profilo per tutti i progetti.",
|
||||
"hole_presets.select": "Seleziona un preset",
|
||||
"hole_presets.title": "Preset personali",
|
||||
"hole_presets.unsupported_options": "Questo preset usa opzioni del filetto che questa versione non può richiamare.",
|
||||
"hole_presets.unsupported_overwrite": "Questo preset contiene opzioni che non possono essere sovrascritte in sicurezza.",
|
||||
"home.back": "Torna a {name}",
|
||||
"home.crashes": "Rapporti di arresto anomalo",
|
||||
"home.design_workspace": "Progettazione",
|
||||
@@ -998,7 +967,6 @@
|
||||
"motion.key_here": "Inserisci fotogramma chiave qui",
|
||||
"motion.key_here_hint": "Aggiungi un fotogramma chiave in questo passo con il valore attuale del giunto",
|
||||
"motion.no_collisions": "Nessuna collisione durante lo studio",
|
||||
"motion.plot_quantity": "Quantità",
|
||||
"motion.step_back": "Passo indietro",
|
||||
"motion.step_forward": "Passo avanti",
|
||||
"motion.to_end": "Alla fine",
|
||||
@@ -1024,16 +992,6 @@
|
||||
"pattern.direction_1": "Direzione 1",
|
||||
"pattern.direction_2": "Direzione 2",
|
||||
"pattern.flip_direction": "Inverti direzione",
|
||||
"perf.click_for_details": "Fai clic per i dettagli: grafico dei tempi, istogramma e ciò che la GPU ha disegnato",
|
||||
"perf.draw_calls": "Chiamate di disegno",
|
||||
"perf.line_segments": "Segmenti",
|
||||
"perf.low_1": "1% più lento",
|
||||
"perf.renderer": "Renderer",
|
||||
"perf.scene_build": "Costruzione scena",
|
||||
"perf.triangles": "Triangoli",
|
||||
"perf.ui_time": "Tempo UI",
|
||||
"perf.viewport": "Vista",
|
||||
"perf.worst": "peggiore",
|
||||
"pick a hole to seat it, or OK places it at the origin": "seleziona un foro in cui inserirlo, oppure premi OK per posizionarlo nell'origine",
|
||||
"pick one or two vertices, edges or faces": "seleziona uno o due vertici, spigoli o facce",
|
||||
"prefs.cpu": "CPU (software)",
|
||||
@@ -1109,7 +1067,6 @@
|
||||
"search.try": "Digita il nome di un comando, una feature di questo progetto o un parametro. Per eseguire un comando con dei valori, digita il suo id e un oggetto JSON: {example}",
|
||||
"search.valid": "Valido",
|
||||
"settings.display_quality_note": "I livelli più fini rendono più uniformi le facce curve e gli spigoli e richiedono più tempo per la mesh. Cambia solo la vista, mai il progetto o le esportazioni.",
|
||||
"settings.frame_rate_details_note": "Aggiunge un grafico dei tempi degli ultimi fotogrammi con i triangoli disegnati sopra, un istogramma dei tempi dei fotogrammi e ciò che la GPU ha disegnato. Fai clic sulla frequenza nella vista per mostrarli o nasconderli.",
|
||||
"settings.frame_rate_note": "Mostra i fotogrammi al secondo nell'angolo inferiore destro della vista.",
|
||||
"settings.measure_axis_distances": "Mostra distanze X/Y/Z",
|
||||
"settings.measure_axis_distances_help": "Mostra le componenti lungo gli assi globali accanto alla distanza totale quando Misura è aperto.",
|
||||
@@ -1364,7 +1321,7 @@
|
||||
"ui.agent_chat_pop_out": "Apri in una finestra separata, che puoi spostare su un altro schermo",
|
||||
"ui.agent_chat_previous_picture": "Immagine precedente",
|
||||
"ui.agent_chat_privacy": "I dati vengono inviati solo all'agente che esegui, con il tuo account.",
|
||||
"ui.agent_chat_privacy_note": "La chat non invia da sola dati ad alcun servizio: l'agente che esegui comunica con il proprio servizio usando il tuo account. SolidCraft non vede né memorizza mai chiavi o password.",
|
||||
"ui.agent_chat_privacy_note": "SolidCraft non invia dati ad alcun servizio: l'agente che esegui comunica con il proprio servizio usando il tuo account. SolidCraft non vede né memorizza mai chiavi o password.",
|
||||
"ui.agent_chat_queue": "Metti in coda",
|
||||
"ui.agent_chat_queue_full": "Troppi messaggi in attesa: rimuovine uno o attendi l'agente.",
|
||||
"ui.agent_chat_queued": "In coda",
|
||||
@@ -1428,7 +1385,6 @@
|
||||
"ui.ai_http_busy": "l'endpoint è occupato o ha superato il limite; riprova più tardi",
|
||||
"ui.ai_http_key": "l'endpoint ha rifiutato la chiave: controllala in Preferenze > IA",
|
||||
"ui.ai_http_not_found": "endpoint o modello inesistente: controlla l'URL di base e il nome del modello",
|
||||
"ui.ai_http_too_large": "la conversazione è troppo grande per questo endpoint; inizia una nuova chat",
|
||||
"ui.ai_key": "Chiave API",
|
||||
"ui.ai_key_hint": "facoltativa; conservata nel portachiavi di sistema",
|
||||
"ui.ai_key_needs_https": "La chiave viene inviata solo tramite HTTPS o a questo computer.",
|
||||
@@ -1456,6 +1412,7 @@
|
||||
"ui.ai_permissions": "Autorizzazioni",
|
||||
"ui.ai_preset": "Preimpostazione",
|
||||
"ui.ai_remove": "Rimuovi",
|
||||
"ui.ai_request_limit": "arrestato dopo 24 richieste per un messaggio; invia un altro messaggio per proseguire",
|
||||
"ui.ai_reset": "Reimposta",
|
||||
"ui.ai_settings": "Impostazioni IA…",
|
||||
"ui.ai_show_features": "Mostra funzioni IA",
|
||||
@@ -1549,7 +1506,6 @@
|
||||
"ui.command_done": "{command} completato",
|
||||
"ui.command_palette_hint": "Digita un comando, ad es. {example} — oppure premi S per cercare",
|
||||
"ui.concentric": "Concentrico",
|
||||
"ui.cone": "Cono",
|
||||
"ui.conic_curve": "Curva conica",
|
||||
"ui.constant": "Costante",
|
||||
"ui.constraints_conflict": "Vincoli in conflitto",
|
||||
@@ -1608,11 +1564,6 @@
|
||||
"ui.draft": "Sformo",
|
||||
"ui.drawing": "Disegno",
|
||||
"ui.drill_point": "Punta del trapano",
|
||||
"ui.easyprint_consent": "EasyPrint è lo slicer di Printables nel browser. Per aprire lì il modello, SolidCraft lo carica sui server di Printables. Il link scade dopo 30 minuti e dovrai aver effettuato l'accesso a Printables. Continuare?",
|
||||
"ui.easyprint_failed": "Caricamento su EasyPrint non riuscito: {error}",
|
||||
"ui.easyprint_opened": "Aperto in EasyPrint",
|
||||
"ui.easyprint_row": "EasyPrint (Printables, nel browser)",
|
||||
"ui.easyprint_uploading": "Caricamento su Printables…",
|
||||
"ui.edit": "Modifica",
|
||||
"ui.editJointLimits": "Modifica limiti del giunto",
|
||||
"ui.edit_section_analysis": "Modifica analisi in sezione",
|
||||
@@ -1638,8 +1589,6 @@
|
||||
"ui.filter_commands_or_keys": "Filtra comandi o tasti",
|
||||
"ui.findInTimeline": "Trova nella sequenza temporale",
|
||||
"ui.findInWindow": "Trova nella finestra",
|
||||
"ui.fine": "Fine",
|
||||
"ui.finest": "Finissima",
|
||||
"ui.finish_sketch": "Termina schizzo",
|
||||
"ui.fit": "Adatta",
|
||||
"ui.fix_unfix": "Fissa/Libera",
|
||||
@@ -1647,7 +1596,6 @@
|
||||
"ui.flip": "Inverti",
|
||||
"ui.format": "Formato",
|
||||
"ui.frame_rate": "Frequenza dei fotogrammi",
|
||||
"ui.frame_rate_details": "Dettagli della frequenza dei fotogrammi",
|
||||
"ui.front": "Frontale",
|
||||
"ui.fully_constrained": "Completamente vincolato",
|
||||
"ui.function": "Funzione",
|
||||
@@ -1670,7 +1618,6 @@
|
||||
"ui.heading_circular_pattern": "RIPETIZIONE CIRCOLARE",
|
||||
"ui.heading_coil": "ELICA",
|
||||
"ui.heading_combine": "COMBINA",
|
||||
"ui.heading_cone": "CONO",
|
||||
"ui.heading_configure": "CONFIGURA",
|
||||
"ui.heading_constraints": "VINCOLI",
|
||||
"ui.heading_construct": "COSTRUISCI",
|
||||
@@ -1730,7 +1677,6 @@
|
||||
"ui.heading_torus": "TORO",
|
||||
"ui.heading_utilities": "UTILITÀ",
|
||||
"ui.heading_utility": "UTILITÀ",
|
||||
"ui.heading_weldments": "SALDATURE",
|
||||
"ui.help": "Aiuto",
|
||||
"ui.hide": "Nascondi",
|
||||
"ui.hideAxes": "Nascondi assi",
|
||||
@@ -1867,7 +1813,6 @@
|
||||
"ui.project_face_edges_sketch": "Proietta gli spigoli delle facce nei nuovi schizzi",
|
||||
"ui.properties": "Proprietà",
|
||||
"ui.pull_direction": "Direzione di estrazione",
|
||||
"ui.quality": "Qualità",
|
||||
"ui.quantity": "Quantità",
|
||||
"ui.quantity_2": "Quantità 2",
|
||||
"ui.quit": "Esci",
|
||||
@@ -1906,7 +1851,9 @@
|
||||
"ui.right": "Destra",
|
||||
"ui.save": "Salva",
|
||||
"ui.save_as": "Salva con nome",
|
||||
"ui.save_mesh_no_slicer": "Nessuno slicer trovato: scegli Personalizzato… e seleziona lo slicer, oppure disattiva Invia allo slicer",
|
||||
"ui.save_mesh_pick_bodies": "Seleziona i corpi da salvare",
|
||||
"ui.save_mesh_temp_failed": "Impossibile creare {folder}: {error}",
|
||||
"ui.saved_to": "Salvato {path}",
|
||||
"ui.sbox_pin": "Fissa in cima al riquadro S",
|
||||
"ui.sbox_unpin": "Sblocca",
|
||||
@@ -1921,6 +1868,7 @@
|
||||
"ui.selection_shift_add_only": "Maiusc+clic aggiunge senza rimuovere",
|
||||
"ui.selection_shift_add_only_tip": "Attivo: Maiusc aggiunge elementi, Ctrl/Cmd commuta. Disattivo (predefinito): Maiusc e Ctrl/Cmd commutano entrambi.",
|
||||
"ui.send": "Invia",
|
||||
"ui.send_to_slicer": "Invia allo slicer",
|
||||
"ui.sent_to_slicer": "Inviato a {slicer}",
|
||||
"ui.sheet_gap": "Gioco",
|
||||
"ui.shell": "Svuota",
|
||||
@@ -1959,7 +1907,6 @@
|
||||
"ui.spline": "Spline",
|
||||
"ui.split_body": "Dividi corpo",
|
||||
"ui.stand_up_tip": "Dopo l'apertura di un file STEP o IGES con asse Z verticale, proponi un quarto di giro attorno a X",
|
||||
"ui.standard": "Standard",
|
||||
"ui.start": "Inizio",
|
||||
"ui.start_over": "Ricomincia",
|
||||
"ui.start_page": "Pagina iniziale",
|
||||
@@ -2012,7 +1959,6 @@
|
||||
"ui.untitled": "Senza titolo",
|
||||
"ui.up_axis_note": "L'asse verticale nelle viste (viste degli assi, Vista iniziale, Vista normale). I progetti restano invariati.",
|
||||
"ui.updateToCurrentView": "Aggiorna alla vista corrente",
|
||||
"ui.upload": "Carica",
|
||||
"ui.view_centre": "Centro della vista",
|
||||
"ui.visual_shaded_hidden_edges": "Ombreggiato con spigoli nascosti",
|
||||
"ui.visual_shaded_with_edges": "Ombreggiato con spigoli",
|
||||
|
||||
+7
-905
File diff suppressed because it is too large
Load Diff
+7
-459
File diff suppressed because it is too large
Load Diff
+5
-25
@@ -72,7 +72,6 @@
|
||||
"Choose…": "Pilih…",
|
||||
"Circular": "Bulat",
|
||||
"Circumference": "Lilitan",
|
||||
"Clockwise": "Ikut arah jam",
|
||||
"Close draft": "Tutup draf",
|
||||
"Collisions": "Perlanggaran",
|
||||
"Colour": "Warna",
|
||||
@@ -323,7 +322,6 @@
|
||||
"Search commands": "Cari arahan",
|
||||
"Search this library": "Cari dalam koleksi ini",
|
||||
"Section": "Keratan",
|
||||
"Section position": "Kedudukan keratan",
|
||||
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Pilih Lain: tahan butang kiri pada model tanpa bergerak (atau klik kanan › Pilih Lain…) untuk senarai objek di bawah kursor. Klik tempat sama untuk objek seterusnya. Klik dua kali butang tengah untuk ke Utama (Tetapan › Navigasi).",
|
||||
"Select bodies (a face picks its body)": "Pilih jasad (klik muka untuk pilih jasadnya)",
|
||||
"Select bodies or faces instead": "Pilih jasad atau muka",
|
||||
@@ -335,7 +333,6 @@
|
||||
"Show Agent Cursor": "Papar Kursor Ejen",
|
||||
"Show in folder": "Papar dalam folder",
|
||||
"Show the pointer an AI agent moves (#35)": "Papar penuding yang digerakkan oleh ejen AI (#35)",
|
||||
"Show tool at cursor": "Tunjukkan alat di sebelah kursor",
|
||||
"Signed axes": "Paksi bertanda",
|
||||
"Size": "Saiz",
|
||||
"Sketch constraints": "Kekangan lakaran",
|
||||
@@ -348,11 +345,9 @@
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft ditutup sebelum reka bentuk disimpan. Buka simpanan automatik terakhirnya.",
|
||||
"Specialise": "Khususkan",
|
||||
"Speed": "Kelajuan",
|
||||
"Square": "Segi empat sama",
|
||||
"Stand it up": "Tegakkan",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Tegakkan: suku putaran pada X, satu ciri Alih yang boleh dibatalkan.",
|
||||
"Stand this part up?": "Tegakkan bahagian ini?",
|
||||
"Start angle": "Sudut mula",
|
||||
"Start bulge": "Bonjolan permulaan",
|
||||
"Start continuity": "Keselanjaran permulaan",
|
||||
"Step": "Langkah",
|
||||
@@ -417,7 +412,6 @@
|
||||
"angle, height": "sudut, ketinggian",
|
||||
"appearance.dielectric_ior": "IOR Dielektrik",
|
||||
"appearance.metallic": "Sifat Logam",
|
||||
"appearance.only_available": "Rupa yang tersedia sahaja",
|
||||
"appearance.pbr_help": "Lihat dalam Render atau PNG produk. Tiada pembiasan atau penembusan cahaya.",
|
||||
"appearance.pbr_opaque": "PBR Legap",
|
||||
"appearance.roughness": "Kekasaran",
|
||||
@@ -457,7 +451,6 @@
|
||||
"cmd.canvas_delete": "Padam Kanvas",
|
||||
"cmd.canvas_edit": "Sunting Kanvas",
|
||||
"cmd.canvas_insert": "Kanvas",
|
||||
"cmd.canvas_to_sketch": "Kanvas ke Lakaran",
|
||||
"cmd.component_activate": "Aktifkan Komponen",
|
||||
"cmd.component_break_link": "Putuskan Pautan",
|
||||
"cmd.component_capture_position": "Rakam Kedudukan",
|
||||
@@ -535,7 +528,6 @@
|
||||
"cmd.file_export": "Eksport",
|
||||
"cmd.file_insert_mesh": "Sisip Mesh",
|
||||
"cmd.file_insert_step": "Sisip STEP",
|
||||
"cmd.file_open_in_easyprint": "Buka dengan EasyPrint",
|
||||
"cmd.file_save_mesh": "Simpan sebagai Mesh",
|
||||
"cmd.file_slicers": "Penghiris Terpasang",
|
||||
"cmd.frame_member": "Anggota Rangka",
|
||||
@@ -581,7 +573,6 @@
|
||||
"cmd.material_create": "Bahan Fizikal Baharu",
|
||||
"cmd.material_delete": "Padam Bahan Fizikal",
|
||||
"cmd.material_edit": "Sunting Bahan Fizikal",
|
||||
"cmd.material_inspect": "Periksa Bahan Fizikal",
|
||||
"cmd.material_list": "Senarai Bahan",
|
||||
"cmd.material_properties_directional": "Modulus Keanjalan Berarah",
|
||||
"cmd.material_properties_validate": "Sahkan Data Bahan Fizikal",
|
||||
@@ -973,7 +964,6 @@
|
||||
"motion.key_here": "Kunci di sini",
|
||||
"motion.key_here_hint": "Tambah kekunci pada langkah ini dengan nilai sendi sekarang",
|
||||
"motion.no_collisions": "Tiada perlanggaran sepanjang kajian",
|
||||
"motion.plot_quantity": "Kuantiti",
|
||||
"motion.step_back": "Langkah ke belakang",
|
||||
"motion.step_forward": "Langkah ke hadapan",
|
||||
"motion.to_end": "Ke penghujung",
|
||||
@@ -1328,7 +1318,7 @@
|
||||
"ui.agent_chat_pop_out": "Buka dalam tetingkap sendiri, yang boleh dialihkan ke skrin lain",
|
||||
"ui.agent_chat_previous_picture": "Gambar sebelumnya",
|
||||
"ui.agent_chat_privacy": "Data hanya dihantar kepada ejen yang anda jalankan, melalui akaun anda sendiri.",
|
||||
"ui.agent_chat_privacy_note": "Sembang tidak menghantar data dengan sendirinya. Ejen berhubung dengan perkhidmatannya melalui akaun anda. SolidCraft tidak melihat atau menyimpan kunci atau kata laluan.",
|
||||
"ui.agent_chat_privacy_note": "SolidCraft tidak menghantar data. Ejen berhubung dengan perkhidmatannya melalui akaun anda. SolidCraft tidak melihat atau menyimpan kunci atau kata laluan.",
|
||||
"ui.agent_chat_queue": "Hantar Nanti",
|
||||
"ui.agent_chat_queue_full": "Terlalu banyak mesej menunggu: buang satu atau tunggu ejen selesai.",
|
||||
"ui.agent_chat_queued": "Menunggu",
|
||||
@@ -1392,7 +1382,6 @@
|
||||
"ui.ai_http_busy": "titik akhir sibuk atau telah melebihi had; cuba lagi kemudian",
|
||||
"ui.ai_http_key": "kunci ditolak oleh titik akhir: semak dalam Tetapan > AI",
|
||||
"ui.ai_http_not_found": "titik akhir atau model tidak ditemui: semak URL asas dan nama model",
|
||||
"ui.ai_http_too_large": "perbualan terlalu besar untuk titik akhir ini; mulakan sembang baharu",
|
||||
"ui.ai_key": "Kunci API",
|
||||
"ui.ai_key_hint": "tidak wajib; disimpan dalam rantai kunci sistem",
|
||||
"ui.ai_key_needs_https": "Kunci hanya dihantar melalui HTTPS, atau ke komputer ini.",
|
||||
@@ -1420,6 +1409,7 @@
|
||||
"ui.ai_permissions": "Kebenaran",
|
||||
"ui.ai_preset": "Pratetap",
|
||||
"ui.ai_remove": "Buang",
|
||||
"ui.ai_request_limit": "berhenti selepas 24 permintaan untuk satu mesej; hantar mesej lain untuk meneruskan",
|
||||
"ui.ai_reset": "Set Semula",
|
||||
"ui.ai_settings": "Tetapan AI…",
|
||||
"ui.ai_show_features": "Papar ciri AI",
|
||||
@@ -1513,7 +1503,6 @@
|
||||
"ui.command_done": "{command} selesai",
|
||||
"ui.command_palette_hint": "Taip arahan, cth. {example} — atau tekan S untuk mencari",
|
||||
"ui.concentric": "Sepusat",
|
||||
"ui.cone": "Kon",
|
||||
"ui.conic_curve": "Lengkung Kon",
|
||||
"ui.constant": "Malar",
|
||||
"ui.constraints_conflict": "Kekangan bercanggah",
|
||||
@@ -1572,11 +1561,6 @@
|
||||
"ui.draft": "Sudut Draf",
|
||||
"ui.drawing": "Lukisan",
|
||||
"ui.drill_point": "Hujung Gerudi",
|
||||
"ui.easyprint_consent": "EasyPrint ialah slicer Printables dalam pelayar. Untuk membuka model di sana, SolidCraft memuat naiknya ke pelayan Printables. Pautan tamat tempoh selepas 30 minit, dan anda perlu log masuk ke Printables. Teruskan?",
|
||||
"ui.easyprint_failed": "Muat naik ke EasyPrint gagal: {error}",
|
||||
"ui.easyprint_opened": "Dibuka dalam EasyPrint",
|
||||
"ui.easyprint_row": "EasyPrint (Printables, dalam pelayar)",
|
||||
"ui.easyprint_uploading": "Memuat naik ke Printables…",
|
||||
"ui.edit": "Sunting",
|
||||
"ui.editJointLimits": "Sunting Had Sendi",
|
||||
"ui.edit_section_analysis": "Sunting Analisis Keratan",
|
||||
@@ -1602,8 +1586,6 @@
|
||||
"ui.filter_commands_or_keys": "Tapis arahan atau kekunci",
|
||||
"ui.findInTimeline": "Cari dalam Garis Masa",
|
||||
"ui.findInWindow": "Cari dalam Tetingkap",
|
||||
"ui.fine": "Halus",
|
||||
"ui.finest": "Paling halus",
|
||||
"ui.finish_sketch": "Tamatkan Lakaran",
|
||||
"ui.fit": "Muatkan",
|
||||
"ui.fix_unfix": "Tetap/Lepas",
|
||||
@@ -1633,7 +1615,6 @@
|
||||
"ui.heading_circular_pattern": "CORAK MEMBULAT",
|
||||
"ui.heading_coil": "GEGELUNG",
|
||||
"ui.heading_combine": "GABUNG",
|
||||
"ui.heading_cone": "KON",
|
||||
"ui.heading_configure": "KONFIGURASI",
|
||||
"ui.heading_constraints": "KEKANGAN",
|
||||
"ui.heading_construct": "BINA",
|
||||
@@ -1693,7 +1674,6 @@
|
||||
"ui.heading_torus": "TORUS",
|
||||
"ui.heading_utilities": "UTILITI",
|
||||
"ui.heading_utility": "UTILITI",
|
||||
"ui.heading_weldments": "KIMPALAN",
|
||||
"ui.help": "Bantuan",
|
||||
"ui.hide": "Sembunyi",
|
||||
"ui.hideAxes": "Sembunyi Paksi",
|
||||
@@ -1830,7 +1810,6 @@
|
||||
"ui.project_face_edges_sketch": "Unjur tepi muka ke dalam lakaran baharu",
|
||||
"ui.properties": "Sifat",
|
||||
"ui.pull_direction": "Arah tarikan",
|
||||
"ui.quality": "Kualiti",
|
||||
"ui.quantity": "Kuantiti",
|
||||
"ui.quantity_2": "Kuantiti 2",
|
||||
"ui.quit": "Keluar",
|
||||
@@ -1869,7 +1848,9 @@
|
||||
"ui.right": "Kanan",
|
||||
"ui.save": "Simpan",
|
||||
"ui.save_as": "Simpan Sebagai",
|
||||
"ui.save_mesh_no_slicer": "Tiada penghiris: pilih program melalui Tersuai… atau matikan Hantar ke penghiris",
|
||||
"ui.save_mesh_pick_bodies": "Pilih jasad untuk disimpan",
|
||||
"ui.save_mesh_temp_failed": "Tidak dapat mencipta {folder}: {error}",
|
||||
"ui.saved_to": "{path} disimpan",
|
||||
"ui.sbox_pin": "Semat di atas kotak S",
|
||||
"ui.sbox_unpin": "Nyahsemat",
|
||||
@@ -1884,6 +1865,7 @@
|
||||
"ui.selection_shift_add_only": "Shift+klik menambah tanpa membuang",
|
||||
"ui.selection_shift_add_only_tip": "Hidup: Shift menambah item, Ctrl/Cmd menogol. Mati (lalai): Shift dan Ctrl/Cmd kedua-duanya menogol.",
|
||||
"ui.send": "Hantar",
|
||||
"ui.send_to_slicer": "Hantar ke penghiris",
|
||||
"ui.sent_to_slicer": "Dihantar ke {slicer}",
|
||||
"ui.sheet_gap": "Jarak",
|
||||
"ui.shell": "Ronggakan",
|
||||
@@ -1922,7 +1904,6 @@
|
||||
"ui.spline": "Splin",
|
||||
"ui.split_body": "Belah Jasad",
|
||||
"ui.stand_up_tip": "Selepas buka STEP atau IGES dengan Z ke atas, cadangkan suku putaran pada X",
|
||||
"ui.standard": "Standard",
|
||||
"ui.start": "Mula",
|
||||
"ui.start_over": "Mula semula",
|
||||
"ui.start_page": "Halaman mula",
|
||||
@@ -1975,7 +1956,6 @@
|
||||
"ui.untitled": "Tanpa Tajuk",
|
||||
"ui.up_axis_note": "Paksi yang menghala ke atas dalam pandangan paksi, Utama dan Pandang Tepat. Reka bentuk tidak berubah.",
|
||||
"ui.updateToCurrentView": "Kemas Kini Pandangan Semasa",
|
||||
"ui.upload": "Muat naik",
|
||||
"ui.view_centre": "Pusat pandangan",
|
||||
"ui.visual_shaded_hidden_edges": "Berlorek dengan tepi tersembunyi",
|
||||
"ui.visual_shaded_with_edges": "Berlorek dengan tepi",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+5
-25
@@ -72,7 +72,6 @@
|
||||
"Choose…": "Kiezen…",
|
||||
"Circular": "Cirkelvormig",
|
||||
"Circumference": "Omtrek",
|
||||
"Clockwise": "Met de klok mee",
|
||||
"Close draft": "Concept sluiten",
|
||||
"Collisions": "Botsingen",
|
||||
"Colour": "Kleur",
|
||||
@@ -322,7 +321,6 @@
|
||||
"Search commands": "Opdrachten zoeken",
|
||||
"Search this library": "Deze bibliotheek doorzoeken",
|
||||
"Section": "Doorsnede",
|
||||
"Section position": "Positie van de doorsnede",
|
||||
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Overige selecteren: houd de linkermuisknop stil op het model (of klik met rechts › Overige selecteren…) om alles onder de cursor weer te geven; klik opnieuw op dezelfde plek om het volgende item daar te selecteren. Dubbelklik met de middelste knop om naar Home te gaan (Voorkeuren › Navigatie).",
|
||||
"Select bodies (a face picks its body)": "Lichamen selecteren (een vlak kiest zijn lichaam)",
|
||||
"Select bodies or faces instead": "In plaats daarvan lichamen of vlakken selecteren",
|
||||
@@ -334,7 +332,6 @@
|
||||
"Show Agent Cursor": "Agentcursor tonen",
|
||||
"Show in folder": "Tonen in map",
|
||||
"Show the pointer an AI agent moves (#35)": "De aanwijzer tonen die een AI-agent verplaatst (#35)",
|
||||
"Show tool at cursor": "Gereedschap bij de cursor tonen",
|
||||
"Signed axes": "Assen met teken",
|
||||
"Size": "Grootte",
|
||||
"Sketch constraints": "Schetsbeperkingen",
|
||||
@@ -347,11 +344,9 @@
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft is gesloten voordat deze ontwerpen zijn opgeslagen. Hun laatste automatische opslag kan worden geopend.",
|
||||
"Specialise": "Specialiseren",
|
||||
"Speed": "Snelheid",
|
||||
"Square": "Vierkant",
|
||||
"Stand it up": "Rechtop zetten",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Rechtop zetten: een kwartslag om X, als één Verplaatsen-kenmerk dat u ongedaan kunt maken.",
|
||||
"Stand this part up?": "Dit onderdeel rechtop zetten?",
|
||||
"Start angle": "Beginhoek",
|
||||
"Start bulge": "Beginbolling",
|
||||
"Start continuity": "Begincontinuïteit",
|
||||
"Step": "Stap",
|
||||
@@ -416,7 +411,6 @@
|
||||
"angle, height": "hoek, hoogte",
|
||||
"appearance.dielectric_ior": "Diëlektrische IOR",
|
||||
"appearance.metallic": "Metaalachtig",
|
||||
"appearance.only_available": "Alleen beschikbare verschijningen",
|
||||
"appearance.pbr_help": "Bekijk dit oppervlak in Render of een product-PNG. Geen breking of transmissie.",
|
||||
"appearance.pbr_opaque": "Ondoorzichtige PBR",
|
||||
"appearance.roughness": "Ruwheid",
|
||||
@@ -456,7 +450,6 @@
|
||||
"cmd.canvas_delete": "Canvas verwijderen",
|
||||
"cmd.canvas_edit": "Canvas bewerken",
|
||||
"cmd.canvas_insert": "Canvas",
|
||||
"cmd.canvas_to_sketch": "Canvas naar schets",
|
||||
"cmd.component_activate": "Component activeren",
|
||||
"cmd.component_break_link": "Koppeling verbreken",
|
||||
"cmd.component_capture_position": "Positie vastleggen",
|
||||
@@ -534,7 +527,6 @@
|
||||
"cmd.file_export": "Exporteren",
|
||||
"cmd.file_insert_mesh": "Mesh invoegen",
|
||||
"cmd.file_insert_step": "STEP invoegen",
|
||||
"cmd.file_open_in_easyprint": "Openen met EasyPrint",
|
||||
"cmd.file_save_mesh": "Opslaan als mesh",
|
||||
"cmd.file_slicers": "Geïnstalleerde slicers",
|
||||
"cmd.frame_member": "Constructiedeel",
|
||||
@@ -580,7 +572,6 @@
|
||||
"cmd.material_create": "Nieuw fysiek materiaal",
|
||||
"cmd.material_delete": "Fysiek materiaal verwijderen",
|
||||
"cmd.material_edit": "Fysiek materiaal bewerken",
|
||||
"cmd.material_inspect": "Fysiek materiaal inspecteren",
|
||||
"cmd.material_list": "Materialen weergeven",
|
||||
"cmd.material_properties_directional": "Richtingsafhankelijke elasticiteitsmodulus",
|
||||
"cmd.material_properties_validate": "Gegevens fysiek materiaal valideren",
|
||||
@@ -972,7 +963,6 @@
|
||||
"motion.key_here": "Sleutel hier",
|
||||
"motion.key_here_hint": "Voeg een sleutel toe op deze stap met de huidige waarde van de verbinding",
|
||||
"motion.no_collisions": "Geen botsingen in de studie",
|
||||
"motion.plot_quantity": "Aantal",
|
||||
"motion.step_back": "Stap terug",
|
||||
"motion.step_forward": "Stap verder",
|
||||
"motion.to_end": "Naar het einde",
|
||||
@@ -1271,7 +1261,7 @@
|
||||
"ui.agent_chat_pop_out": "Openen in een eigen venster, dat u naar een ander scherm kunt verplaatsen",
|
||||
"ui.agent_chat_previous_picture": "Vorige afbeelding",
|
||||
"ui.agent_chat_privacy": "Er gaat niets naar buiten behalve naar de agent die u uitvoert, onder uw eigen account.",
|
||||
"ui.agent_chat_privacy_note": "De chat verstuurt zelf niets: de agent die u uitvoert praat met zijn eigen service onder uw eigen account. SolidCraft ziet of bewaart nooit een sleutel of wachtwoord.",
|
||||
"ui.agent_chat_privacy_note": "SolidCraft verstuurt niets: de agent die u uitvoert praat met zijn eigen service onder uw eigen account. SolidCraft ziet of bewaart nooit een sleutel of wachtwoord.",
|
||||
"ui.agent_chat_queue": "In wachtrij",
|
||||
"ui.agent_chat_queue_full": "Te veel berichten wachten: verwijder er een of wacht op de agent.",
|
||||
"ui.agent_chat_queued": "In wachtrij",
|
||||
@@ -1335,7 +1325,6 @@
|
||||
"ui.ai_http_busy": "het eindpunt is bezet of boven zijn limiet; probeer het later opnieuw",
|
||||
"ui.ai_http_key": "het eindpunt weigerde de sleutel: controleer deze in Voorkeuren > AI",
|
||||
"ui.ai_http_not_found": "geen eindpunt of model gevonden: controleer de basis-URL en de modelnaam",
|
||||
"ui.ai_http_too_large": "het gesprek is te groot voor dit endpoint; begin een nieuwe chat",
|
||||
"ui.ai_key": "API-sleutel",
|
||||
"ui.ai_key_hint": "optioneel; bewaard in de sleutelhanger van het systeem",
|
||||
"ui.ai_key_needs_https": "Een sleutel wordt alleen via HTTPS of naar deze computer verzonden.",
|
||||
@@ -1363,6 +1352,7 @@
|
||||
"ui.ai_permissions": "Machtigingen",
|
||||
"ui.ai_preset": "Voorinstelling",
|
||||
"ui.ai_remove": "Verwijderen",
|
||||
"ui.ai_request_limit": "gestopt na 24 verzoeken voor één bericht; stuur nog een bericht om verder te gaan",
|
||||
"ui.ai_reset": "Herstellen",
|
||||
"ui.ai_settings": "AI-instellingen…",
|
||||
"ui.ai_show_features": "AI-functies tonen",
|
||||
@@ -1455,7 +1445,6 @@
|
||||
"ui.command_done": "{command} gereed",
|
||||
"ui.command_palette_hint": "Typ een opdracht, bijv. {example} — of druk op S om te zoeken",
|
||||
"ui.concentric": "Concentrisch",
|
||||
"ui.cone": "Kegel",
|
||||
"ui.conic_curve": "Kegelsnede",
|
||||
"ui.constant": "Constant",
|
||||
"ui.constraints_conflict": "Beperkingen conflicteren",
|
||||
@@ -1514,11 +1503,6 @@
|
||||
"ui.draft": "Aanzet",
|
||||
"ui.drawing": "Tekening",
|
||||
"ui.drill_point": "Boorpunt",
|
||||
"ui.easyprint_consent": "EasyPrint is de slicer van Printables in de browser. Om het model daar te openen, uploadt SolidCraft het naar de servers van Printables. De link verloopt na 30 minuten en u moet bij Printables zijn ingelogd. Doorgaan?",
|
||||
"ui.easyprint_failed": "Uploaden naar EasyPrint mislukt: {error}",
|
||||
"ui.easyprint_opened": "Geopend in EasyPrint",
|
||||
"ui.easyprint_row": "EasyPrint (Printables, in de browser)",
|
||||
"ui.easyprint_uploading": "Uploaden naar Printables…",
|
||||
"ui.edit": "Bewerken",
|
||||
"ui.editJointLimits": "Verbindingslimieten bewerken",
|
||||
"ui.edit_section_analysis": "Doorsnedeanalyse bewerken",
|
||||
@@ -1544,8 +1528,6 @@
|
||||
"ui.filter_commands_or_keys": "Opdrachten of toetsen filteren",
|
||||
"ui.findInTimeline": "Zoeken in tijdlijn",
|
||||
"ui.findInWindow": "Zoeken in venster",
|
||||
"ui.fine": "Fijn",
|
||||
"ui.finest": "Zeer fijn",
|
||||
"ui.finish_sketch": "Schets voltooien",
|
||||
"ui.fit": "Passend",
|
||||
"ui.fix_unfix": "Vastzetten/losmaken",
|
||||
@@ -1575,7 +1557,6 @@
|
||||
"ui.heading_circular_pattern": "CIRKELVORMIG PATROON",
|
||||
"ui.heading_coil": "SPIRAAL",
|
||||
"ui.heading_combine": "COMBINEREN",
|
||||
"ui.heading_cone": "KEGEL",
|
||||
"ui.heading_configure": "CONFIGUREREN",
|
||||
"ui.heading_constraints": "BEPERKINGEN",
|
||||
"ui.heading_construct": "CONSTRUEREN",
|
||||
@@ -1635,7 +1616,6 @@
|
||||
"ui.heading_torus": "TORUS",
|
||||
"ui.heading_utilities": "HULPPROGRAMMA'S",
|
||||
"ui.heading_utility": "HULPPROGRAMMA",
|
||||
"ui.heading_weldments": "LASWERK",
|
||||
"ui.help": "Help",
|
||||
"ui.hide": "Verbergen",
|
||||
"ui.hideAxes": "Assen verbergen",
|
||||
@@ -1772,7 +1752,6 @@
|
||||
"ui.project_face_edges_sketch": "Vlakranden projecteren in nieuwe schetsen",
|
||||
"ui.properties": "Eigenschappen",
|
||||
"ui.pull_direction": "Trekrichting",
|
||||
"ui.quality": "Kwaliteit",
|
||||
"ui.quantity": "Aantal",
|
||||
"ui.quantity_2": "Aantal 2",
|
||||
"ui.quit": "Afsluiten",
|
||||
@@ -1811,7 +1790,9 @@
|
||||
"ui.right": "Rechts",
|
||||
"ui.save": "Opslaan",
|
||||
"ui.save_as": "Opslaan als",
|
||||
"ui.save_mesh_no_slicer": "Geen slicer gevonden: kies Aangepast… en selecteer uw slicerprogramma, of schakel Naar slicer verzenden uit",
|
||||
"ui.save_mesh_pick_bodies": "Selecteer de op te slaan lichamen",
|
||||
"ui.save_mesh_temp_failed": "{folder} kon niet worden gemaakt: {error}",
|
||||
"ui.saved_to": "{path} opgeslagen",
|
||||
"ui.sbox_pin": "Bovenaan het S-vak vastpinnen",
|
||||
"ui.sbox_unpin": "Losmaken",
|
||||
@@ -1826,6 +1807,7 @@
|
||||
"ui.selection_shift_add_only": "Shift+klik voegt toe zonder te verwijderen",
|
||||
"ui.selection_shift_add_only_tip": "Aan: Shift voegt items toe, Ctrl/Cmd schakelt om. Uit (standaard): Shift en Ctrl/Cmd schakelen beide om.",
|
||||
"ui.send": "Verzenden",
|
||||
"ui.send_to_slicer": "Naar slicer verzenden",
|
||||
"ui.sent_to_slicer": "Verzonden naar {slicer}",
|
||||
"ui.sheet_gap": "Opening",
|
||||
"ui.shell": "Schalen",
|
||||
@@ -1864,7 +1846,6 @@
|
||||
"ui.spline": "Spline",
|
||||
"ui.split_body": "Lichaam splitsen",
|
||||
"ui.stand_up_tip": "Zet na het openen van een STEP- of IGES-bestand met Z omhoog een kwartslag om X aan",
|
||||
"ui.standard": "Standaard",
|
||||
"ui.start": "Start",
|
||||
"ui.start_over": "Opnieuw beginnen",
|
||||
"ui.start_page": "Startpagina",
|
||||
@@ -1917,7 +1898,6 @@
|
||||
"ui.untitled": "Naamloos",
|
||||
"ui.up_axis_note": "Welke as in de weergaven omhoog wijst (aanzicht op as, Home, Kijken naar). Ontwerpen blijven ongewijzigd.",
|
||||
"ui.updateToCurrentView": "Bijwerken naar huidige weergave",
|
||||
"ui.upload": "Uploaden",
|
||||
"ui.view_centre": "Weergavemidden",
|
||||
"ui.visual_shaded_hidden_edges": "Gearceerd met verborgen randen",
|
||||
"ui.visual_shaded_with_edges": "Gearceerd met randen",
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
# Pending UI strings
|
||||
|
||||
A pull request that adds UI strings adds **one new file here** instead of editing
|
||||
`assets/locales/<locale>.json`. Two pull requests then never touch the same file, so they never
|
||||
conflict.
|
||||
|
||||
- Name it after the change, lowercase with dashes: `sketch-trim-hints.json`.
|
||||
- Contents: a map from locale code to `{"message id": "text"}`. Give English; add other locales
|
||||
you can translate (a locale you leave out falls back to English, as before):
|
||||
|
||||
```json
|
||||
{
|
||||
"en": { "sketch.trim.hint": "Click a segment to trim it" },
|
||||
"de": { "sketch.trim.hint": "Segment zum Kürzen anklicken" }
|
||||
}
|
||||
```
|
||||
|
||||
- The id must not already be in that locale's catalog, nor in another pending file; the build and
|
||||
`cargo xtask lists` fail otherwise. To change an existing message, edit the catalog itself.
|
||||
- Don't sort or fold anything. The app, `cargo xtask i18n` and the tests read these files as part of
|
||||
the catalogs, so your strings work in your own pull request.
|
||||
- After a merge, the Generated files workflow runs `cargo xtask fold-pending` on `main`: it adds the
|
||||
messages to the catalogs (sorted, one per line) and deletes the files. Never run it in a pull
|
||||
request.
|
||||
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"en": {
|
||||
"ui.continuity_smooth_curvature_change": "Smooth curvature change",
|
||||
"ui.continuity_smooth_curvature_change_tip": "Smooth curvature change (G4): even the way the curvature changes runs on smoothly across the seam.",
|
||||
"ui.fillet_corner_tangent_tip": "Tangent (G1): a rolling-ball corner, a piece of the rolling ball's sphere tangent to each round.",
|
||||
"ui.fillet_corner_curvature_tip": "Curvature (G2): a smooth patch. The corner is set back along each edge by the Setback ({factor} × the radius unless you type one) and filled with a patch, so reflections run through it without a kink."
|
||||
},
|
||||
"ar": {
|
||||
"ui.continuity_smooth_curvature_change": "تغيّر انحناء سلس",
|
||||
"ui.continuity_smooth_curvature_change_tip": "تغيّر انحناء سلس (G4): حتى طريقة تغيّر الانحناء تستمر بسلاسة عبر خط الالتقاء.",
|
||||
"ui.fillet_corner_tangent_tip": "مماس (G1): زاوية بالكرة المتدحرجة، جزء من سطح الكرة المتدحرجة مماسّ لكل تقويس.",
|
||||
"ui.fillet_corner_curvature_tip": "الانحناء (G2): رقعة ناعمة. تُرجَع الزاوية على طول كل حافة بمقدار الإرجاع ({factor} ضعف نصف القطر ما لم تكتب قيمة) وتُملأ برقعة، فتمر الانعكاسات عبرها دون انكسار."
|
||||
},
|
||||
"de": {
|
||||
"ui.continuity_smooth_curvature_change": "Glatte Krümmungsänderung",
|
||||
"ui.continuity_smooth_curvature_change_tip": "Glatte Krümmungsänderung (G4): Selbst die Art, wie sich die Krümmung ändert, läuft an der Naht glatt weiter.",
|
||||
"ui.fillet_corner_tangent_tip": "Tangential (G1): eine Kugelecke, ein Stück der Kugel des rollenden Balls, tangential zu jeder Rundung.",
|
||||
"ui.fillet_corner_curvature_tip": "Krümmung (G2): ein glattes Flächenstück. Die Ecke wird entlang jeder Kante um den Rücksprung zurückgesetzt ({factor} × der Radius, sofern Sie keinen eingeben) und mit einem Flächenstück gefüllt, sodass Spiegelungen ohne Knick hindurchlaufen."
|
||||
},
|
||||
"he": {
|
||||
"ui.continuity_smooth_curvature_change": "שינוי עקמומיות חלק",
|
||||
"ui.continuity_smooth_curvature_change_tip": "שינוי עקמומיות חלק (G4): גם האופן שבו העקמומיות משתנה ממשיך בצורה חלקה לאורך התפר.",
|
||||
"ui.fillet_corner_tangent_tip": "משיק (G1): פינת כדור מתגלגל, חלק מספירת הכדור המתגלגל, משיק לכל עיגול.",
|
||||
"ui.fillet_corner_curvature_tip": "עקמומיות (G2): טלאי חלק. הפינה נסוגה לאורך כל קצה במרחק הנסיגה (פי {factor} מהרדיוס אלא אם תקלידו ערך) ומתמלאת בטלאי, כך שההשתקפויות עוברות בה בלי שבר."
|
||||
},
|
||||
"ja": {
|
||||
"ui.continuity_smooth_curvature_change": "なめらかな曲率変化",
|
||||
"ui.continuity_smooth_curvature_change_tip": "なめらかな曲率変化 (G4): 境目で曲率の変わり方そのものもなめらかに続きます。",
|
||||
"ui.fillet_corner_tangent_tip": "接線 (G1): 転がるボールのコーナー。転がるボールの球面の一部で、各フィレットに接します。",
|
||||
"ui.fillet_corner_curvature_tip": "曲率 (G2): なめらかなパッチ。コーナーを各エッジに沿ってセットバック分 (入力しなければ半径の {factor} 倍) 後退させてパッチで埋め、映り込みが折れずに通ります。"
|
||||
},
|
||||
"ko": {
|
||||
"ui.continuity_smooth_curvature_change": "부드러운 곡률 변화",
|
||||
"ui.continuity_smooth_curvature_change_tip": "부드러운 곡률 변화(G4): 이음매에서 곡률이 변하는 방식까지 부드럽게 이어집니다.",
|
||||
"ui.fillet_corner_tangent_tip": "접선(G1): 굴러가는 볼 코너. 굴러가는 볼의 구면 일부로, 각 라운드에 접합니다.",
|
||||
"ui.fillet_corner_curvature_tip": "곡률(G2): 부드러운 패치. 코너를 각 모서리를 따라 셋백만큼(입력하지 않으면 반지름의 {factor}배) 물려 패치로 채우므로, 반사가 꺾임 없이 지나갑니다."
|
||||
},
|
||||
"zh": {
|
||||
"ui.continuity_smooth_curvature_change": "平滑曲率变化",
|
||||
"ui.continuity_smooth_curvature_change_tip": "平滑曲率变化 (G4):连曲率的变化方式在接缝处也平滑延续。",
|
||||
"ui.fillet_corner_tangent_tip": "相切 (G1):滚球角,滚球球面的一部分,与每个圆角相切。",
|
||||
"ui.fillet_corner_curvature_tip": "曲率 (G2):平滑曲面片。角点沿每条边后退一个后退距离(未输入时为半径的 {factor} 倍),并用曲面片填充,反射穿过时没有折痕。"
|
||||
}
|
||||
}
|
||||
@@ -1,219 +0,0 @@
|
||||
{
|
||||
"en": {
|
||||
"ui.continuity_position": "Position",
|
||||
"ui.continuity_tangent": "Tangent",
|
||||
"ui.continuity_curvature": "Curvature",
|
||||
"ui.continuity_curvature_change": "Curvature change",
|
||||
"ui.continuity_crease": "Crease",
|
||||
"ui.continuity_curvature_change_tip": "Curvature change (G3): the curvature also changes smoothly across the seam.",
|
||||
"ui.continuity_curvature_tip": "Curvature-continuous (G2): reflections run through without a kink.",
|
||||
"ui.continuity_position_tip": "Position (G0): the surfaces meet, with a crease where they do.",
|
||||
"ui.continuity_tangent_tip": "Tangent (G1): no crease, though the curvature jumps across the seam."
|
||||
},
|
||||
"ar": {
|
||||
"ui.continuity_position": "الموضع",
|
||||
"ui.continuity_tangent": "مماس",
|
||||
"ui.continuity_curvature": "الانحناء",
|
||||
"ui.continuity_curvature_change": "تغيّر الانحناء",
|
||||
"ui.continuity_crease": "تجعّد",
|
||||
"ui.continuity_curvature_change_tip": "تغيّر الانحناء (G3): يتغير الانحناء أيضًا بسلاسة عبر خط الالتقاء.",
|
||||
"ui.continuity_curvature_tip": "متصل الانحناء (G2): تمر الانعكاسات دون انكسار.",
|
||||
"ui.continuity_position_tip": "الموضع (G0): تلتقي الأسطح، مع تجعّد عند التقائها.",
|
||||
"ui.continuity_tangent_tip": "مماس (G1): لا تجعّد، لكن الانحناء يقفز عبر خط الالتقاء."
|
||||
},
|
||||
"cs": {
|
||||
"ui.continuity_position": "Poloha",
|
||||
"ui.continuity_tangent": "Tečnost",
|
||||
"ui.continuity_curvature": "Křivost",
|
||||
"ui.continuity_curvature_change": "Změna křivosti",
|
||||
"ui.continuity_crease": "Hrana"
|
||||
},
|
||||
"da": {
|
||||
"ui.continuity_position": "Positionskontinuitet",
|
||||
"ui.continuity_tangent": "Tangentkontinuitet",
|
||||
"ui.continuity_curvature": "Krumning",
|
||||
"ui.continuity_curvature_change": "Ændring i krumning",
|
||||
"ui.continuity_crease": "Knæk"
|
||||
},
|
||||
"de": {
|
||||
"ui.continuity_position": "Position",
|
||||
"ui.continuity_tangent": "Tangential",
|
||||
"ui.continuity_curvature": "Krümmung",
|
||||
"ui.continuity_curvature_change": "Krümmungsänderung",
|
||||
"ui.continuity_crease": "Knick",
|
||||
"ui.continuity_curvature_change_tip": "Krümmungsänderung (G3): Auch die Krümmung ändert sich an der Naht glatt.",
|
||||
"ui.continuity_curvature_tip": "Krümmungsstetig (G2): Spiegelungen laufen ohne Knick hindurch.",
|
||||
"ui.continuity_position_tip": "Position (G0): Die Flächen treffen sich, mit einem Knick an der Naht.",
|
||||
"ui.continuity_tangent_tip": "Tangential (G1): kein Knick, aber die Krümmung springt an der Naht."
|
||||
},
|
||||
"es": {
|
||||
"ui.continuity_position": "Posición",
|
||||
"ui.continuity_tangent": "Tangente",
|
||||
"ui.continuity_curvature": "Curvatura",
|
||||
"ui.continuity_curvature_change": "Cambio de curvatura",
|
||||
"ui.continuity_crease": "Arista"
|
||||
},
|
||||
"fr": {
|
||||
"ui.continuity_position": "Position",
|
||||
"ui.continuity_tangent": "Tangence",
|
||||
"ui.continuity_curvature": "Courbure",
|
||||
"ui.continuity_curvature_change": "Variation de courbure",
|
||||
"ui.continuity_crease": "Angle vif"
|
||||
},
|
||||
"gl": {
|
||||
"ui.continuity_position": "Posición",
|
||||
"ui.continuity_tangent": "Tanxente",
|
||||
"ui.continuity_curvature": "Curvatura",
|
||||
"ui.continuity_curvature_change": "Cambio de curvatura",
|
||||
"ui.continuity_crease": "Aresta"
|
||||
},
|
||||
"he": {
|
||||
"ui.continuity_position": "מיקום",
|
||||
"ui.continuity_tangent": "משיק",
|
||||
"ui.continuity_curvature": "עקמומיות",
|
||||
"ui.continuity_curvature_change": "שינוי עקמומיות",
|
||||
"ui.continuity_crease": "שבר",
|
||||
"ui.continuity_curvature_change_tip": "שינוי עקמומיות (G3): גם העקמומיות משתנה בצורה חלקה לאורך התפר.",
|
||||
"ui.continuity_curvature_tip": "רציף בעקמומיות (G2): ההשתקפויות עוברות בלי שבר.",
|
||||
"ui.continuity_position_tip": "מיקום (G0): המשטחים נפגשים, עם שבר במקום המפגש.",
|
||||
"ui.continuity_tangent_tip": "משיק (G1): אין שבר, אך העקמומיות קופצת לאורך התפר."
|
||||
},
|
||||
"hu": {
|
||||
"ui.continuity_position": "Pozíció",
|
||||
"ui.continuity_tangent": "Érintő",
|
||||
"ui.continuity_curvature": "Görbület",
|
||||
"ui.continuity_curvature_change": "Görbületváltozás",
|
||||
"ui.continuity_crease": "Törés"
|
||||
},
|
||||
"id": {
|
||||
"ui.continuity_position": "Posisi",
|
||||
"ui.continuity_tangent": "Tangen",
|
||||
"ui.continuity_curvature": "Kelengkungan",
|
||||
"ui.continuity_curvature_change": "Perubahan kelengkungan",
|
||||
"ui.continuity_crease": "Lipatan"
|
||||
},
|
||||
"it": {
|
||||
"ui.continuity_position": "Posizione",
|
||||
"ui.continuity_tangent": "Tangente",
|
||||
"ui.continuity_curvature": "Curvatura",
|
||||
"ui.continuity_curvature_change": "Variazione di curvatura",
|
||||
"ui.continuity_crease": "Spigolo vivo"
|
||||
},
|
||||
"ja": {
|
||||
"ui.continuity_position": "位置",
|
||||
"ui.continuity_tangent": "接線",
|
||||
"ui.continuity_curvature": "曲率",
|
||||
"ui.continuity_curvature_change": "曲率変化",
|
||||
"ui.continuity_crease": "折れ",
|
||||
"ui.continuity_curvature_change_tip": "曲率変化 (G3): 境目で曲率の変化もなめらかです。",
|
||||
"ui.continuity_curvature_tip": "曲率連続 (G2): 映り込みが折れずに通ります。",
|
||||
"ui.continuity_position_tip": "位置 (G0): 面はつながりますが、境目に折れがあります。",
|
||||
"ui.continuity_tangent_tip": "接線 (G1): 折れはありませんが、境目で曲率が飛びます。"
|
||||
},
|
||||
"ko": {
|
||||
"ui.continuity_position": "위치",
|
||||
"ui.continuity_tangent": "접선",
|
||||
"ui.continuity_curvature": "곡률",
|
||||
"ui.continuity_curvature_change": "곡률 변화",
|
||||
"ui.continuity_crease": "꺾임",
|
||||
"ui.continuity_curvature_change_tip": "곡률 변화(G3): 이음매에서 곡률의 변화도 부드럽습니다.",
|
||||
"ui.continuity_curvature_tip": "곡률 연속(G2): 반사가 꺾임 없이 이어집니다.",
|
||||
"ui.continuity_position_tip": "위치(G0): 면이 만나지만, 만나는 곳에 꺾임이 있습니다.",
|
||||
"ui.continuity_tangent_tip": "접선(G1): 꺾임은 없지만, 이음매에서 곡률이 급변합니다."
|
||||
},
|
||||
"ms": {
|
||||
"ui.continuity_position": "Kedudukan",
|
||||
"ui.continuity_tangent": "Tangen",
|
||||
"ui.continuity_curvature": "Kelengkungan",
|
||||
"ui.continuity_curvature_change": "Perubahan kelengkungan",
|
||||
"ui.continuity_crease": "Lipatan"
|
||||
},
|
||||
"nb": {
|
||||
"ui.continuity_position": "Posisjon",
|
||||
"ui.continuity_tangent": "Tangent",
|
||||
"ui.continuity_curvature": "Krumning",
|
||||
"ui.continuity_curvature_change": "Krumningsendring",
|
||||
"ui.continuity_crease": "Knekk"
|
||||
},
|
||||
"nl": {
|
||||
"ui.continuity_position": "Positie",
|
||||
"ui.continuity_tangent": "Tangentieel",
|
||||
"ui.continuity_curvature": "Kromming",
|
||||
"ui.continuity_curvature_change": "Krommingsverandering",
|
||||
"ui.continuity_crease": "Knik"
|
||||
},
|
||||
"pl": {
|
||||
"ui.continuity_position": "Położenie",
|
||||
"ui.continuity_tangent": "Styczność",
|
||||
"ui.continuity_curvature": "Krzywizna",
|
||||
"ui.continuity_curvature_change": "Zmiana krzywizny",
|
||||
"ui.continuity_crease": "Załamanie"
|
||||
},
|
||||
"pt-BR": {
|
||||
"ui.continuity_position": "Posição",
|
||||
"ui.continuity_tangent": "Tangente",
|
||||
"ui.continuity_curvature": "Curvatura",
|
||||
"ui.continuity_curvature_change": "Variação de curvatura",
|
||||
"ui.continuity_crease": "Vinco"
|
||||
},
|
||||
"ro": {
|
||||
"ui.continuity_position": "Poziție",
|
||||
"ui.continuity_tangent": "Tangent",
|
||||
"ui.continuity_curvature": "Curbură",
|
||||
"ui.continuity_curvature_change": "Variația curburii",
|
||||
"ui.continuity_crease": "Muchie"
|
||||
},
|
||||
"ru": {
|
||||
"ui.continuity_position": "Положение",
|
||||
"ui.continuity_tangent": "Касание",
|
||||
"ui.continuity_curvature": "Кривизна",
|
||||
"ui.continuity_curvature_change": "Изменение кривизны",
|
||||
"ui.continuity_crease": "Излом"
|
||||
},
|
||||
"sv": {
|
||||
"ui.continuity_position": "Läge",
|
||||
"ui.continuity_tangent": "Tangent",
|
||||
"ui.continuity_curvature": "Krökning",
|
||||
"ui.continuity_curvature_change": "Krökningsändring",
|
||||
"ui.continuity_crease": "Veck"
|
||||
},
|
||||
"tr": {
|
||||
"ui.continuity_position": "Konum",
|
||||
"ui.continuity_tangent": "Teğet",
|
||||
"ui.continuity_curvature": "Eğrilik",
|
||||
"ui.continuity_curvature_change": "Eğrilik değişimi",
|
||||
"ui.continuity_crease": "Kırık"
|
||||
},
|
||||
"uk": {
|
||||
"ui.continuity_position": "Положення",
|
||||
"ui.continuity_tangent": "Дотична",
|
||||
"ui.continuity_curvature": "Кривизна",
|
||||
"ui.continuity_curvature_change": "Зміна кривини",
|
||||
"ui.continuity_crease": "Злам"
|
||||
},
|
||||
"vi": {
|
||||
"ui.continuity_position": "Vị trí liên tục",
|
||||
"ui.continuity_tangent": "Tiếp tuyến",
|
||||
"ui.continuity_curvature": "Độ cong",
|
||||
"ui.continuity_curvature_change": "Thay đổi độ cong",
|
||||
"ui.continuity_crease": "Nếp gập"
|
||||
},
|
||||
"zh": {
|
||||
"ui.continuity_position": "位置",
|
||||
"ui.continuity_tangent": "相切",
|
||||
"ui.continuity_curvature": "曲率",
|
||||
"ui.continuity_curvature_change": "曲率变化",
|
||||
"ui.continuity_crease": "折痕",
|
||||
"ui.continuity_curvature_change_tip": "曲率变化 (G3):曲率在接缝处的变化也是平滑的。",
|
||||
"ui.continuity_curvature_tip": "曲率连续 (G2):反射平滑穿过,没有折痕。",
|
||||
"ui.continuity_position_tip": "位置 (G0):曲面相接,但接缝处有折痕。",
|
||||
"ui.continuity_tangent_tip": "相切 (G1):没有折痕,但曲率在接缝处突变。"
|
||||
},
|
||||
"zh-TW": {
|
||||
"ui.continuity_position": "位置",
|
||||
"ui.continuity_tangent": "相切",
|
||||
"ui.continuity_curvature": "曲率",
|
||||
"ui.continuity_curvature_change": "曲率變化",
|
||||
"ui.continuity_crease": "摺痕"
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"en": {
|
||||
"ui.comb_length": "Comb length",
|
||||
"ui.comb_length_tip": "How long the teeth are: a multiple of the automatic length, which makes the longest tooth a fifth of the size of the curves shown.",
|
||||
"ui.comb_teeth": "Teeth",
|
||||
"ui.comb_teeth_tip": "How many teeth each comb has, evenly spaced along the curve. More teeth show more detail of how the curvature changes."
|
||||
},
|
||||
"ar": {
|
||||
"ui.comb_length": "طول المشط",
|
||||
"ui.comb_length_tip": "طول الأسنان: مضاعف للطول التلقائي الذي يجعل أطول سنّ خُمس حجم المنحنيات المعروضة.",
|
||||
"ui.comb_teeth": "الأسنان",
|
||||
"ui.comb_teeth_tip": "عدد أسنان كل مشط، موزّعة بالتساوي على طول المنحنى. كلما زادت الأسنان ظهرت تفاصيل أكثر عن تغيّر الانحناء."
|
||||
},
|
||||
"de": {
|
||||
"ui.comb_length": "Kammlänge",
|
||||
"ui.comb_length_tip": "Wie lang die Zähne sind: ein Vielfaches der automatischen Länge, bei der der längste Zahn ein Fünftel der Größe der gezeigten Kurven ist.",
|
||||
"ui.comb_teeth": "Zähne",
|
||||
"ui.comb_teeth_tip": "Wie viele Zähne jeder Kamm hat, gleichmäßig entlang der Kurve verteilt. Mehr Zähne zeigen genauer, wie sich die Krümmung ändert."
|
||||
},
|
||||
"he": {
|
||||
"ui.comb_length": "אורך המסרק",
|
||||
"ui.comb_length_tip": "אורך השיניים: כפולה של האורך האוטומטי, שבו השן הארוכה ביותר היא חמישית מגודל העקומות המוצגות.",
|
||||
"ui.comb_teeth": "שיניים",
|
||||
"ui.comb_teeth_tip": "כמה שיניים יש לכל מסרק, במרווחים שווים לאורך העקומה. יותר שיניים מראות בפירוט רב יותר איך העקמומיות משתנה."
|
||||
},
|
||||
"ja": {
|
||||
"ui.comb_length": "コームの長さ",
|
||||
"ui.comb_length_tip": "歯の長さ: 自動の長さの倍率です。自動の長さでは、いちばん長い歯が表示中の曲線の大きさの 5 分の 1 になります。",
|
||||
"ui.comb_teeth": "歯の数",
|
||||
"ui.comb_teeth_tip": "各コームの歯の数です。曲線に沿って等間隔に並びます。歯を増やすと、曲率の変わり方がより細かく見えます。"
|
||||
},
|
||||
"ko": {
|
||||
"ui.comb_length": "빗 길이",
|
||||
"ui.comb_length_tip": "빗살의 길이: 자동 길이의 배수입니다. 자동 길이에서는 가장 긴 빗살이 표시된 곡선 크기의 5분의 1이 됩니다.",
|
||||
"ui.comb_teeth": "빗살 수",
|
||||
"ui.comb_teeth_tip": "각 빗의 빗살 수이며, 곡선을 따라 같은 간격으로 놓입니다. 빗살이 많을수록 곡률이 어떻게 바뀌는지 더 자세히 보입니다."
|
||||
},
|
||||
"zh": {
|
||||
"ui.comb_length": "梳长",
|
||||
"ui.comb_length_tip": "梳齿的长度:自动长度的倍数。自动长度使最长的梳齿为所示曲线尺寸的五分之一。",
|
||||
"ui.comb_teeth": "梳齿数",
|
||||
"ui.comb_teeth_tip": "每个梳的梳齿数,沿曲线均匀分布。梳齿越多,越能看清曲率如何变化。"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user