Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 492e2d1c17 | |||
| a09ecc2ec6 | |||
| a077906234 | |||
| b2733d607a |
@@ -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",
|
||||
]
|
||||
|
||||
@@ -12,6 +12,7 @@ not (
|
||||
(package(solidcraft-engine) & (
|
||||
test(=tests::hostile_params_never_panic)
|
||||
| test(/^tests::robustness::/)
|
||||
| test(/^tests::robustness_goldens::seed_/)
|
||||
| test(=pattern_suppress_tests::enclosure_arc_extinction_keeps_a_real_sharp_inner_lip_after_resize_and_reopen)
|
||||
| test(=pattern_suppress_tests::enclosure_datums_follow_wall_height_and_board_depth_with_material_proofs)
|
||||
| test(=cmd::plastic::tests::enclosure_snap_fit_follows_resized_front_wall)
|
||||
@@ -43,12 +44,13 @@ threads-required = 12
|
||||
priority = 100
|
||||
|
||||
# The slowest tests start first, so a shard does not end with one of them running alone on a
|
||||
# core while the others sit idle. Medians from the CI test timings (the junit artifacts), slowest
|
||||
# first; a test over ~100 s belongs here.
|
||||
# core while the others sit idle (seed_81 alone takes about 20 minutes under CI load). Medians
|
||||
# from the CI test timings (the junit artifacts), slowest first; a test over ~100 s belongs here.
|
||||
[[profile.ci.overrides]]
|
||||
filter = """
|
||||
(package(solidcraft-engine) & (
|
||||
test(/^tests::robustness::/)
|
||||
test(/^tests::robustness_goldens::seed_(81|45|20|24|95|02|57|40|18|48|32|46|85|61)$/)
|
||||
| test(/^tests::robustness::/)
|
||||
| test(=tests::hostile_params_never_panic)
|
||||
| test(=cmd::clearance::tests::distances_are_exact_on_edges_and_curved_faces)
|
||||
| test(=cmd::clipboard::tests::a_pasted_coil_joins_the_pasted_body)
|
||||
@@ -90,13 +92,14 @@ threads-required = "num-cpus"
|
||||
priority = 100
|
||||
|
||||
[[profile.local-ci.overrides]]
|
||||
filter = "package(solidcraft-kernel) & test(=tspline::tests::hostile_tsplines_never_panic)"
|
||||
filter = "(package(solidcraft-engine) & test(=tests::robustness_goldens::seed_81)) | (package(solidcraft-kernel) & test(=tspline::tests::hostile_tsplines_never_panic))"
|
||||
priority = 99
|
||||
|
||||
[[profile.local-ci.overrides]]
|
||||
filter = """
|
||||
(package(solidcraft-engine) & (
|
||||
test(/^tests::robustness::/)
|
||||
test(/^tests::robustness_goldens::seed_(81|45|20|24|95|02|57|40|18|48|32|46|85|61)$/)
|
||||
| test(/^tests::robustness::/)
|
||||
| test(=tests::hostile_params_never_panic)
|
||||
| test(=cmd::clearance::tests::distances_are_exact_on_edges_and_curved_faces)
|
||||
| test(=cmd::clipboard::tests::a_pasted_coil_joins_the_pasted_body)
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<!-- Title: which part of the app changes and what a user now sees or can do, clear to someone who hasn't seen your chat, the issue or the code. -->
|
||||
|
||||
## Change
|
||||
|
||||
Start with what this is, in plain words someone new to the project would understand: what it does and who it's for. Don't assume anyone knows the name of your tool, project or feature. Then describe the problem and resulting behavior. Link the issue when applicable.
|
||||
Describe the problem and resulting behavior. Link the issue when applicable.
|
||||
|
||||
Changes a default: no
|
||||
|
||||
@@ -16,8 +14,6 @@ What did you run, and what were the results? Include expected and measured value
|
||||
|
||||
## Screenshots of the behavior (when observable)
|
||||
|
||||
Put the picture or video here in the description. A file in the diff doesn't count: a maintainer has to be able to see what this is without opening the changed files.
|
||||
|
||||
For observable CAD, UI or model behavior, include actual descriptive screenshots or a short recording from a small reproduction. This includes kernel, geometry, assembly and backend effects that can be shown in the app. Coding agents should run the app and capture the evidence themselves, rather than asking a human for evidence they can produce. Show the actual failure/result, preferably before and after, with brief steps, expected/actual behavior and annotations where helpful.
|
||||
|
||||
Flowcharts and Mermaid diagrams cannot substitute for evidence of visible behavior. Use diagrams for architectural reasoning when actual screenshots cannot illustrate the point. Changes with no useful observable behavior may omit screenshot/video evidence. Exclude private designs and sensitive details; attach media or link to an accessible recording rather than committing generated review artifacts.
|
||||
|
||||
@@ -33,8 +33,6 @@
|
||||
# `ci` job still runs and reports the commit's real result (see the `ci` job): the merge queue
|
||||
# reads `ci` from the newest run of a commit, so a run without it would block a PR that passed.
|
||||
# Cross-compilation is opt-in through Run workflow; release builds remain in release.yml.
|
||||
# The release gate (release-gate.yml) calls this workflow (workflow_call) as a Run workflow with
|
||||
# cross-compilation on.
|
||||
# Every Linux job runs on Blacksmith's Ubuntu 24.04 runners (blacksmith-<N>vcpu-ubuntu-2404), sized
|
||||
# per job: 32 vCPU for the tests, 16 for Clippy, the GPU path tests, the archive build (nightly) and the merge queue's, 8 for compiles, doc and reference tests, 4 for the light checks, 2 for Select checks and ci.
|
||||
# The Windows shader job runs on Blacksmith's Windows Server 2025 runners (blacksmith-8vcpu-windows-2025);
|
||||
@@ -52,14 +50,10 @@ on:
|
||||
# only (other labels are skipped, see the top of this file).
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review, labeled]
|
||||
# The kernel-smlib branch (ADR 0002) runs no CI for now (owner, 2026-10-21). Drop this
|
||||
# line before the branch merges back into main.
|
||||
branches-ignore: [kernel-smlib]
|
||||
# The merge queue: each queued group runs the full suite before it lands on main.
|
||||
merge_group:
|
||||
push:
|
||||
# develop: the SMLib kernel's trunk until it merges into main (its PRs land there).
|
||||
branches: [main, develop]
|
||||
branches: [main]
|
||||
schedule:
|
||||
- cron: "17 6 * * *"
|
||||
workflow_dispatch:
|
||||
@@ -68,14 +62,6 @@ on:
|
||||
description: "Also check WebAssembly and RISC-V (before a release)"
|
||||
type: boolean
|
||||
default: false
|
||||
# The release gate (release-gate.yml): the event stays the caller's (workflow_dispatch), so a
|
||||
# call runs exactly what Run workflow runs.
|
||||
workflow_call:
|
||||
inputs:
|
||||
cross_compile:
|
||||
description: "Also check WebAssembly and RISC-V (before a release)"
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -131,6 +117,7 @@ jobs:
|
||||
outputs:
|
||||
native: ${{ steps.paths.outputs.native }}
|
||||
kernel: ${{ steps.paths.outputs.kernel }}
|
||||
goldens: ${{ steps.paths.outputs.goldens }}
|
||||
windows: ${{ steps.paths.outputs.windows }}
|
||||
# The GPU path tests run (see the rule where it is computed). The job's `if` and the `ci` job's check both read this, so they cannot disagree.
|
||||
gpu: ${{ steps.paths.outputs.gpu }}
|
||||
@@ -169,6 +156,7 @@ jobs:
|
||||
run: |
|
||||
native=false
|
||||
kernel=false
|
||||
goldens=false
|
||||
windows=false
|
||||
gpu_paths=false
|
||||
deps=false
|
||||
@@ -250,12 +238,12 @@ jobs:
|
||||
if [ "${tested:-0}" -gt 0 ]; then
|
||||
echo "The merge queue already tested $PUSH_SHA: repository checks only."
|
||||
elif tested=$(curl -fsS -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" \
|
||||
"https://api.github.com/repos/$REPO/actions/workflows/ci.yml/runs?event=push&branch=$GITHUB_REF_NAME&status=success&per_page=1" \
|
||||
"https://api.github.com/repos/$REPO/actions/workflows/ci.yml/runs?event=push&branch=main&status=success&per_page=1" \
|
||||
| grep -o '"head_sha": *"[0-9a-f]*"' | head -n 1 | grep -o '[0-9a-f]\{40\}') \
|
||||
&& [ -n "$tested" ] && git merge-base --is-ancestor "$tested" "$PUSH_SHA" \
|
||||
&& git diff --no-renames --name-only -z "$tested" "$PUSH_SHA" > "$RUNNER_TEMP/ci-paths" \
|
||||
&& select_packages "$RUNNER_TEMP/ci-paths" && [ "$full" != true ] && [ -z "$packages" ]; then
|
||||
# Nothing a build reads changed since the last push to this branch whose run passed (the
|
||||
# Nothing a build reads changed since the last push to main whose run passed (the
|
||||
# push before may never have run: a newer push cancels a waiting one).
|
||||
echo "$tested..$PUSH_SHA changes no code: repository checks only."
|
||||
else
|
||||
@@ -294,6 +282,13 @@ jobs:
|
||||
git diff --no-renames --name-only -z "$BASE_SHA...$HEAD_SHA" > "$RUNNER_TEMP/ci-paths"
|
||||
while IFS= read -r -d '' path; do
|
||||
case "$path" in crates/kernel/*|vendor/truck-*) kernel=true ;; esac
|
||||
# What a pull request re-checks the goldens for: the kernel, truck and the goldens
|
||||
# themselves (a re-record is checked before it is queued). The goldens are frozen, so
|
||||
# on any other change they would only re-check unchanged results; the merge queue,
|
||||
# main and the nightly run every seed before anything lands.
|
||||
case "$path" in
|
||||
crates/kernel/*|vendor/truck-*|tests/robustness/*) goldens=true ;;
|
||||
esac
|
||||
done < "$RUNNER_TEMP/ci-paths"
|
||||
# Computed as for a merge group, then turned off below (the fast lane).
|
||||
windows_paths "$RUNNER_TEMP/ci-paths" "$BASE_SHA...$HEAD_SHA"
|
||||
@@ -305,6 +300,10 @@ jobs:
|
||||
# selected means no code changed.
|
||||
select_packages "$RUNNER_TEMP/ci-paths"
|
||||
if [ "$full" = true ] || [ -n "$packages" ]; then native=true; fi
|
||||
# The goldens are solidcraft-engine tests: its tests must be in the archive.
|
||||
if [ "$goldens" = true ] && [ "$full" != true ]; then
|
||||
case " $packages " in *" solidcraft-engine "*) ;; *) packages="$packages solidcraft-engine" ;; esac
|
||||
fi
|
||||
fi
|
||||
# The reference tests replay recipes through solidcraft-cli: needed when it, or anything
|
||||
# it depends on, is selected.
|
||||
@@ -319,12 +318,13 @@ jobs:
|
||||
doctests=$native
|
||||
if [ "$EVENT_NAME" = pull_request ]; then windows=false; oracle=false; doctests=false; fi
|
||||
# Until a maintainer adds approved-for-ci, a pull request gets a quick compile instead of
|
||||
# Clippy, the tests and the doc tests.
|
||||
# Clippy, the tests, the doc tests and the goldens.
|
||||
quick=false
|
||||
if [ "$EVENT_NAME" = pull_request ] && [ "$APPROVED" != true ]; then
|
||||
quick=$native
|
||||
native=false
|
||||
oracle=false
|
||||
goldens=false
|
||||
fi
|
||||
# A push that only merges main into the change (Merge branch 'main', Update branch) when
|
||||
# the change itself already passed: nothing new to test here, and main tests the merged
|
||||
@@ -337,6 +337,7 @@ jobs:
|
||||
if [ -n "$carried" ]; then
|
||||
echo "Only a merge of main into a change that passed in $carried: repository checks only."
|
||||
native=false
|
||||
goldens=false
|
||||
fi
|
||||
fi
|
||||
# The GPU path tests (their own job, about 56 runner-minutes) are for render changes only:
|
||||
@@ -362,6 +363,7 @@ jobs:
|
||||
echo "native=$native"
|
||||
echo "gpu=$gpu"
|
||||
echo "kernel=$kernel"
|
||||
echo "goldens=$goldens"
|
||||
echo "windows=$windows"
|
||||
echo "oracle=$oracle"
|
||||
echo "doctests=$doctests"
|
||||
@@ -387,7 +389,7 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: repo-checks
|
||||
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: Format
|
||||
run: cargo fmt --all -- --check
|
||||
- name: Asset attribution
|
||||
@@ -407,10 +409,6 @@ jobs:
|
||||
# New UI text must reach every complete catalog; report it here, before review (#8542).
|
||||
- name: Translations in the complete catalogs
|
||||
run: cargo xtask i18n --check
|
||||
- name: Model decisions use exact geometry, not meshes
|
||||
run: |
|
||||
cargo xtask mesh-decisions
|
||||
cargo test -p xtask mesh_decisions --locked
|
||||
- name: Agent router and skills cite real files
|
||||
run: |
|
||||
cargo xtask skills
|
||||
@@ -441,7 +439,7 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: quick-compile
|
||||
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: Cargo check
|
||||
run: cargo check --workspace --all-targets --locked
|
||||
|
||||
@@ -460,7 +458,7 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: native-clippy
|
||||
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: Clippy
|
||||
env:
|
||||
FULL: ${{ needs.changes.outputs.full }}
|
||||
@@ -503,7 +501,7 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: native-tests
|
||||
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: Set up compiler cache
|
||||
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
|
||||
with:
|
||||
@@ -574,7 +572,7 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: native-tests
|
||||
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: Set up compiler cache
|
||||
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
|
||||
with:
|
||||
@@ -585,8 +583,13 @@ jobs:
|
||||
- name: Run tests
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
GOLDENS: ${{ needs.changes.outputs.goldens }}
|
||||
FULL: ${{ needs.changes.outputs.full }}
|
||||
PACKAGES: ${{ needs.changes.outputs.packages }}
|
||||
# The robustness golden seeds a pull request skips: each takes one to twenty minutes under
|
||||
# CI load, together about four fifths of the goldens' time. Pushes to main, the merge
|
||||
# queue and the nightly run every seed.
|
||||
SLOW_SEEDS: 81|45|20|24|95|02|57|40|18|48|32|46|85|61|77|34|14
|
||||
run: |
|
||||
# Pull requests skip the heavy tests (default-filter in .config/nextest.toml); `default()`
|
||||
# below is that filter, so a pull request still runs only what it ran before.
|
||||
@@ -600,23 +603,29 @@ jobs:
|
||||
# The boolean property tests draw new random inputs every run; they run only in the
|
||||
# nightly property-tests job, so a known kernel bug cannot turn main red at random.
|
||||
base='not (package(solidcraft-kernel) & binary(boolean_props))'
|
||||
elif [ "$GOLDENS" = true ]; then
|
||||
# A pull request that can change a robustness golden (tests/robustness/goldens.json)
|
||||
# also checks every seed but the slow ones, in the same run so they share its cores.
|
||||
base="(default() & not (binary(action_matrix) & test(=action_matrix_sample))) | (package(solidcraft-engine) & test(/^tests::robustness_goldens::/) & not test(/^tests::robustness_goldens::seed_($SLOW_SEEDS)\$/))"
|
||||
fi
|
||||
junit=${CARGO_TARGET_DIR:-target}/nextest/ci/junit.xml
|
||||
rm -f "$junit"
|
||||
# Pull requests test the affected packages (scripts/ci/affected.py).
|
||||
scope=(--workspace)
|
||||
if [ "$FULL" != true ]; then scope=(); for p in $PACKAGES; do scope+=(-p "$p"); done; fi
|
||||
status=0
|
||||
# The GPU path tracer's tests run in their own job (GPU path tests): one at a time, they
|
||||
# took about 36 of this job's 45 minutes.
|
||||
# A failed test does not fail the step by itself: the tests known to fail today
|
||||
# (tests/KNOWN_FAILING_TESTS) may fail, any other fails it, and so does a listed test
|
||||
# that passes. nextest's exit code 100 is "tests failed"; any other is its own error
|
||||
# (a build error, a bad filter) and fails the step at once.
|
||||
status=0
|
||||
cargo nextest run "${scope[@]}" --cargo-profile ci --locked --profile ci --no-tests=pass \
|
||||
--ignore-default-filter -E "($base) & not (package(=solidcraft-gpu) & test(/^path::/))" || status=$?
|
||||
if [ "$status" -ne 0 ] && [ "$status" -ne 100 ]; then exit "$status"; fi
|
||||
cargo xtask known-failing --junit "$junit"
|
||||
--ignore-default-filter -E "($base) & not (package(=solidcraft-gpu) & test(/^path::/))" || status=1
|
||||
if [ "$status" != 0 ]; then
|
||||
# A failed golden gets the hint to re-record it (junit.xml lists the failures).
|
||||
if [ -f "$junit" ] && tr '\n' ' ' < "$junit" | grep -q '<testcase name="tests::robustness_goldens::[^"]*"[^>]*>[[:space:]]*<failure'; then
|
||||
echo "::error::A robustness golden changed. If the new result is intended, re-record it in this pull request and say why:"
|
||||
echo "::error::ROBUSTNESS_GOLDENS_BLESS=\"why it changed\" cargo test --profile ci -p solidcraft-engine --lib robustness_goldens:: (see docs/robustness.md)"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
- name: Compiler cache statistics
|
||||
if: always()
|
||||
run: sccache --show-stats
|
||||
@@ -690,7 +699,7 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: native-gpu
|
||||
save-if: ${{ (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') && matrix.shard == 1 }}
|
||||
save-if: ${{ github.ref == 'refs/heads/main' && matrix.shard == 1 }}
|
||||
- name: Set up compiler cache
|
||||
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
|
||||
with:
|
||||
@@ -705,14 +714,8 @@ jobs:
|
||||
# None of these is in the default filter (the heavy tests), so pull requests, the merge
|
||||
# queue, main and nightly all run every one of them, as before. `count` deals the tests
|
||||
# out in turn (27 each); `hash` split them 41/40/27 across three shards.
|
||||
# Graded against tests/KNOWN_FAILING_TESTS, as in Native Linux tests.
|
||||
junit=${CARGO_TARGET_DIR:-target}/nextest/ci/junit.xml
|
||||
rm -f "$junit"
|
||||
status=0
|
||||
cargo nextest run -p solidcraft-gpu --cargo-profile ci --locked --profile ci --no-tests=pass \
|
||||
--ignore-default-filter -E 'test(/^path::/)' --partition "count:$SHARD/4" || status=$?
|
||||
if [ "$status" -ne 0 ] && [ "$status" -ne 100 ]; then exit "$status"; fi
|
||||
cargo xtask known-failing --junit "$junit"
|
||||
--ignore-default-filter -E 'test(/^path::/)' --partition "count:$SHARD/4"
|
||||
- name: Compiler cache statistics
|
||||
if: always()
|
||||
run: sccache --show-stats
|
||||
@@ -846,7 +849,7 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: doc-tests
|
||||
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: Set up compiler cache
|
||||
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
|
||||
with:
|
||||
@@ -904,7 +907,7 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: oracle
|
||||
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: Set up compiler cache
|
||||
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
|
||||
with:
|
||||
@@ -943,7 +946,7 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: windows-fxc
|
||||
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
# One test at a time: WARP's shader JIT crashes now and then (a null read in
|
||||
# d3d10warp.dll's JIT optimizer, about 1 run in 60) when several devices compile compute
|
||||
# pipelines at once, which the parallel tests do. Minidumps showed it in WARP itself, not
|
||||
@@ -967,7 +970,7 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: windows-startup
|
||||
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: Backend order and probing on Windows
|
||||
run: cargo test -p solidcraft --profile ci --locked -- startup::tests native_crash::tests native_frame::tests
|
||||
- name: Build the app
|
||||
@@ -1104,7 +1107,7 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: wasm
|
||||
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- run: cargo xtask wasm
|
||||
|
||||
riscv64-gates:
|
||||
@@ -1126,7 +1129,7 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: riscv64
|
||||
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: Install cross-compilation dependencies
|
||||
run: |
|
||||
# Ubuntu's regular archive serves amd64; RISC-V packages live in ports.
|
||||
@@ -1142,7 +1145,7 @@ jobs:
|
||||
EOF
|
||||
sudo dpkg --add-architecture riscv64
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gcc-riscv64-linux-gnu g++-riscv64-linux-gnu libasound2-dev:riscv64 libxkbcommon-dev:riscv64 \
|
||||
sudo apt-get install -y gcc-riscv64-linux-gnu libasound2-dev:riscv64 libxkbcommon-dev:riscv64 \
|
||||
libwayland-dev:riscv64 libx11-dev:riscv64 libxrandr-dev:riscv64 libxi-dev:riscv64 libgl1-mesa-dev:riscv64
|
||||
- name: Cargo check on RISC-V
|
||||
env:
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
# The fillet rig as a regression gate: a pull request that touches fillet code (the paths below)
|
||||
# runs every case in tests/kernel-cases/fillets/ through one release build of
|
||||
# solidcraft-cli, and fails on a regression (a case that fails and
|
||||
# tests/kernel-cases/fillets/KNOWN_FAILING does not list) or on a listed case that now passes
|
||||
# (it is fixed: remove its line, so the list only shrinks). See tests/kernel-cases/README.md.
|
||||
#
|
||||
# GitHub filters a workflow by whole files, so the fillet parts of crates/doc/src/eval*.rs and
|
||||
# crates/engine/src/cmd/features.rs select it whenever those files change. Pushes to main and
|
||||
# develop that touch the same paths run it too and save the compiler cache (a Blacksmith sticky
|
||||
# disk) that pull requests then mount read-only.
|
||||
name: Fillet rig
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
paths:
|
||||
- "crates/kernel/src/blend*.rs"
|
||||
- "crates/kernel/src/fillet*.rs"
|
||||
- "crates/kernel/src/*corner*.rs"
|
||||
- "crates/kernel/src/continuity*.rs"
|
||||
- "crates/smlib/**"
|
||||
- "crates/doc/src/eval*.rs"
|
||||
- "crates/engine/src/cmd/features.rs"
|
||||
- "tests/kernel-cases/fillets/**"
|
||||
# The rig itself: the grading, the runner and this workflow.
|
||||
- "apps/solidcraft-cli/src/kernel_cases.rs"
|
||||
- "xtask/src/kernel_cases.rs"
|
||||
- ".github/workflows/fillet-rig.yml"
|
||||
push:
|
||||
branches: [main, develop]
|
||||
# The same paths as pull_request.
|
||||
paths:
|
||||
- "crates/kernel/src/blend*.rs"
|
||||
- "crates/kernel/src/fillet*.rs"
|
||||
- "crates/kernel/src/*corner*.rs"
|
||||
- "crates/kernel/src/continuity*.rs"
|
||||
- "crates/smlib/**"
|
||||
- "crates/doc/src/eval*.rs"
|
||||
- "crates/engine/src/cmd/features.rs"
|
||||
- "tests/kernel-cases/fillets/**"
|
||||
# The rig itself: the grading, the runner and this workflow.
|
||||
- "apps/solidcraft-cli/src/kernel_cases.rs"
|
||||
- "xtask/src/kernel_cases.rs"
|
||||
- ".github/workflows/fillet-rig.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: fillet-rig-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
fillets:
|
||||
name: Fillet rig
|
||||
# Draft pull requests run no CI, as in ci.yml.
|
||||
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
CARGO_INCREMENTAL: "0"
|
||||
RUSTC_WRAPPER: sccache
|
||||
SCCACHE_DIR: /home/runner/.cache/sccache
|
||||
SCCACHE_CACHE_SIZE: 10G
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Size parallel compiles to the job's memory
|
||||
run: scripts/ci/build-jobs.sh
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
# Only main and develop commit the cache; pull requests and forks mount a clone and never
|
||||
# write back.
|
||||
- name: Mount the compiler cache
|
||||
uses: useblacksmith/stickydisk@3f6be1451e3cad893b778bc93495c80452a5da26 # v1.7.2
|
||||
continue-on-error: true
|
||||
with:
|
||||
key: ${{ github.repository }}-sccache-fillet-rig
|
||||
path: /home/runner/.cache/sccache
|
||||
commit: ${{ (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') && 'on-change' || 'false' }}
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: fillet-rig
|
||||
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
|
||||
- name: Set up compiler cache
|
||||
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
|
||||
with:
|
||||
version: v0.16.0
|
||||
- name: Build the CLI (release)
|
||||
run: cargo build --release --locked -p solidcraft-cli
|
||||
- name: Run the fillet cases against the known-failing list
|
||||
run: >-
|
||||
cargo run -q --locked -p xtask -- kernel-cases --area fillets
|
||||
--cli target/release/solidcraft-cli --out "$RUNNER_TEMP/fillet-rig"
|
||||
--known-failing tests/kernel-cases/fillets/KNOWN_FAILING
|
||||
- name: Keep the results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: fillet-rig-results
|
||||
path: ${{ runner.temp }}/fillet-rig/results.json
|
||||
if-no-files-found: ignore
|
||||
@@ -1,7 +1,6 @@
|
||||
# While a new geometry kernel is evaluated, new issues and discussions about kernel work, and new
|
||||
# pull requests that change the kernel directories, are labelled `kernel-paused` for a maintainer to
|
||||
# answer (CONTRIBUTING.md, "Kernel work is paused"). The logic is in scripts/kernel-paused/ (tested
|
||||
# in ci.yml).
|
||||
# While a new geometry kernel is evaluated, new issues, pull requests and discussions about kernel
|
||||
# work are labelled `kernel-paused` for a maintainer to answer (CONTRIBUTING.md, "Kernel work is
|
||||
# paused"). The logic is in scripts/kernel-paused/ (tested in ci.yml).
|
||||
name: Kernel paused
|
||||
|
||||
on:
|
||||
|
||||
@@ -1,238 +0,0 @@
|
||||
# The kernel rig and the complexity ladder as a regression gate. A plan job maps a pull request's
|
||||
# changed files to the rig areas they can affect (scripts/ci/kernel_rig_plan.py): an area runs
|
||||
# when a file its tests/kernel-cases/<area>/PATHS lists changes, or one of its cases; every area
|
||||
# runs when the shared core in tests/kernel-cases/PATHS-ALL changes, and when the solidcore pin in
|
||||
# the root Cargo.toml and Cargo.lock moves (the plan reads their diff); the ladder runs whenever any
|
||||
# area does. One job then builds solidcraft-cli once (release), checks that every
|
||||
# registry command has a case or an exemption (tests/kernel-cases/EXEMPT), and hands the CLI to a
|
||||
# matrix of shards as an artifact, one per selected area plus the ladder, in parallel, so the
|
||||
# wall time is the build plus the slowest shard. See tests/kernel-cases/README.md.
|
||||
#
|
||||
# - An area fails on a regression (a case that fails and its tests/kernel-cases/<area>/KNOWN_FAILING
|
||||
# does not list) or on a listed case that passes (it is fixed: remove its line in the same pull
|
||||
# request, so the list only shrinks).
|
||||
# - The ladder fails on any case whose status or counts differ from
|
||||
# tests/kernel-cases/ladder/golden.json; a right change is re-recorded (`--record "why"`).
|
||||
#
|
||||
# The trigger paths are whole folders that hold every area's PATHS (a self-test,
|
||||
# scripts/ci/test_kernel_rig_plan.py, checks they do), so a pull request that touches none of
|
||||
# them (the UI, docs, packaging) starts no runner at all, and a new area needs no edit here: the
|
||||
# plan finds the areas from the folders. Pushes to main and develop on the same paths, merge groups
|
||||
# (once the queue is on), the release gate (release-gate.yml, workflow_call) and a run by hand run
|
||||
# every area; those pushes also commit the compiler cache (a Blacksmith sticky disk) that every
|
||||
# other run mounts read-only.
|
||||
name: Kernel rig
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
paths:
|
||||
- "crates/kernel/**"
|
||||
- "crates/geom/**"
|
||||
- "crates/smlib/**"
|
||||
- "crates/step/**"
|
||||
- "crates/io/src/**"
|
||||
- "crates/doc/src/**"
|
||||
- "crates/engine/src/**"
|
||||
- "tests/kernel-cases/**"
|
||||
# The solidcore pin (the plan runs every area only when its lines change).
|
||||
- "Cargo.toml"
|
||||
- "Cargo.lock"
|
||||
# The rig itself: the grading, the runner, the selection and this workflow.
|
||||
- "apps/solidcraft-cli/src/kernel_cases.rs"
|
||||
- "xtask/src/kernel_cases.rs"
|
||||
- "xtask/src/kernel_ladder.rs"
|
||||
- "scripts/ci/kernel_rig_plan.py"
|
||||
- "scripts/ci/test_kernel_rig_plan.py"
|
||||
- ".github/workflows/kernel-rig.yml"
|
||||
push:
|
||||
branches: [main, develop]
|
||||
# The same paths as pull_request.
|
||||
paths:
|
||||
- "crates/kernel/**"
|
||||
- "crates/geom/**"
|
||||
- "crates/smlib/**"
|
||||
- "crates/step/**"
|
||||
- "crates/io/src/**"
|
||||
- "crates/doc/src/**"
|
||||
- "crates/engine/src/**"
|
||||
- "tests/kernel-cases/**"
|
||||
# The solidcore pin (the plan runs every area only when its lines change).
|
||||
- "Cargo.toml"
|
||||
- "Cargo.lock"
|
||||
# The rig itself: the grading, the runner, the selection and this workflow.
|
||||
- "apps/solidcraft-cli/src/kernel_cases.rs"
|
||||
- "xtask/src/kernel_cases.rs"
|
||||
- "xtask/src/kernel_ladder.rs"
|
||||
- "scripts/ci/kernel_rig_plan.py"
|
||||
- "scripts/ci/test_kernel_rig_plan.py"
|
||||
- ".github/workflows/kernel-rig.yml"
|
||||
merge_group:
|
||||
workflow_call:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
# A new push to a pull request replaces its old run.
|
||||
group: kernel-rig-${{ github.event_name }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
plan:
|
||||
name: Select the rig areas
|
||||
# Draft pull requests run no CI, as in ci.yml.
|
||||
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
# The shard matrix (JSON list of areas and the ladder; [] when nothing is selected).
|
||||
shards: ${{ steps.plan.outputs.shards }}
|
||||
# Whether the build job, and with it the coverage check, runs.
|
||||
build: ${{ steps.plan.outputs.build }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
# A pull request's diff needs its base's history.
|
||||
fetch-depth: ${{ github.event_name == 'pull_request' && '0' || '1' }}
|
||||
- name: Selection self-test
|
||||
run: python3 -I scripts/ci/test_kernel_rig_plan.py
|
||||
- id: plan
|
||||
name: Map the changed files to areas
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
run: |
|
||||
# Pull requests run the areas their diff reaches; pushes, merge groups, the release
|
||||
# gate and a run by hand run every area.
|
||||
if [ "$EVENT_NAME" = pull_request ]; then
|
||||
git diff --no-renames --name-only -z "$BASE_SHA...$HEAD_SHA" > "$RUNNER_TEMP/rig-paths"
|
||||
git diff --no-renames "$BASE_SHA...$HEAD_SHA" -- Cargo.toml Cargo.lock > "$RUNNER_TEMP/rig-pin.diff"
|
||||
python3 -I scripts/ci/kernel_rig_plan.py --paths "$RUNNER_TEMP/rig-paths" --pin-diff "$RUNNER_TEMP/rig-pin.diff" \
|
||||
--github-output "$GITHUB_OUTPUT"
|
||||
else
|
||||
python3 -I scripts/ci/kernel_rig_plan.py --all --github-output "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
build:
|
||||
name: Build the CLI
|
||||
needs: plan
|
||||
if: needs.plan.outputs.build == 'true'
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
CARGO_INCREMENTAL: "0"
|
||||
RUSTC_WRAPPER: sccache
|
||||
SCCACHE_DIR: /home/runner/.cache/sccache
|
||||
SCCACHE_CACHE_SIZE: 10G
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Size parallel compiles to the job's memory
|
||||
run: scripts/ci/build-jobs.sh
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
# Only pushes to main and develop commit the cache; pull requests, merge groups and forks
|
||||
# mount a clone and never write back.
|
||||
- name: Mount the compiler cache
|
||||
uses: useblacksmith/stickydisk@3f6be1451e3cad893b778bc93495c80452a5da26 # v1.7.2
|
||||
continue-on-error: true
|
||||
with:
|
||||
key: ${{ github.repository }}-sccache-kernel-rig
|
||||
path: /home/runner/.cache/sccache
|
||||
commit: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') && 'on-change' || 'false' }}
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: kernel-rig
|
||||
save-if: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') }}
|
||||
- name: Set up compiler cache
|
||||
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
|
||||
with:
|
||||
version: v0.16.0
|
||||
- name: Build the CLI (release) and the rig's runner
|
||||
run: |
|
||||
cargo build --release --locked -p solidcraft-cli
|
||||
cargo build --locked -p xtask
|
||||
# A tar keeps the executable bits, which an artifact drops. xtask finds the repository from
|
||||
# the path it was built in (CARGO_MANIFEST_DIR), which is the same checkout path in every
|
||||
# job of this workflow.
|
||||
# Every registry command is used by a case or listed in tests/kernel-cases/EXEMPT.
|
||||
- name: Every command has a case
|
||||
run: target/debug/xtask kernel-cases --coverage --cli target/release/solidcraft-cli
|
||||
# A coverage-only run (the registry or the cases changed, no area selected) runs no shard.
|
||||
- name: Pack the binaries
|
||||
if: needs.plan.outputs.shards != '[]'
|
||||
run: tar -cf "$RUNNER_TEMP/kernel-rig-bin.tar" -C target release/solidcraft-cli debug/xtask
|
||||
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
if: needs.plan.outputs.shards != '[]'
|
||||
with:
|
||||
name: kernel-rig-bin
|
||||
path: ${{ runner.temp }}/kernel-rig-bin.tar
|
||||
retention-days: 1
|
||||
- name: Compiler cache statistics
|
||||
if: always()
|
||||
run: sccache --show-stats
|
||||
|
||||
shard:
|
||||
name: Kernel rig (${{ matrix.shard }})
|
||||
needs: [plan, build]
|
||||
if: needs.plan.outputs.shards != '[]'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# The areas of tests/kernel-cases/ the plan selected, and the ladder. On the build box
|
||||
# (aarch64, 4 cases at a time) the slowest areas took about 1 minute (booleans, 212 s of
|
||||
# cases with one of 84 s; threads, 161 s) and the ladder, one case at a time, 92 s: no area
|
||||
# needs splitting.
|
||||
shard: ${{ fromJSON(needs.plan.outputs.shards) }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
name: kernel-rig-bin
|
||||
path: ${{ runner.temp }}
|
||||
- name: Unpack the binaries
|
||||
run: mkdir -p target && tar -xf "$RUNNER_TEMP/kernel-rig-bin.tar" -C target
|
||||
- name: Run the cases against the known-failing list (the ladder against its golden)
|
||||
env:
|
||||
SHARD: ${{ matrix.shard }}
|
||||
run: |
|
||||
if [ "$SHARD" = ladder ]; then
|
||||
set -- --ladder --gate
|
||||
else
|
||||
set -- --area "$SHARD" --known-failing "tests/kernel-cases/$SHARD/KNOWN_FAILING"
|
||||
fi
|
||||
target/debug/xtask kernel-cases "$@" --cli target/release/solidcraft-cli --out "$RUNNER_TEMP/kernel-rig"
|
||||
- name: Keep the results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: kernel-rig-results-${{ matrix.shard }}
|
||||
path: ${{ runner.temp }}/kernel-rig/**/results.json
|
||||
if-no-files-found: ignore
|
||||
|
||||
# One check for the whole gate, for branch protection and the merge queue.
|
||||
kernel-rig:
|
||||
name: kernel-rig
|
||||
needs: [plan, build, shard]
|
||||
if: always() && (github.event_name != 'pull_request' || !github.event.pull_request.draft)
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
steps:
|
||||
# A skipped build or shard counts as passed only when the plan selected none.
|
||||
- name: Every selected job passed
|
||||
env:
|
||||
PLAN: ${{ needs.plan.result }}
|
||||
WANT_BUILD: ${{ needs.plan.outputs.build }}
|
||||
WANT_SHARDS: ${{ needs.plan.outputs.shards }}
|
||||
BUILD: ${{ needs.build.result }}
|
||||
SHARDS: ${{ needs.shard.result }}
|
||||
run: |
|
||||
echo "plan=$PLAN build=$BUILD shards=$SHARDS selected=$WANT_SHARDS"
|
||||
test "$PLAN" = success
|
||||
if [ "$WANT_BUILD" = true ]; then test "$BUILD" = success; else test "$BUILD" = skipped; fi
|
||||
if [ "$WANT_SHARDS" != '[]' ]; then test "$SHARDS" = success; else test "$SHARDS" = skipped; fi
|
||||
@@ -1,6 +1,6 @@
|
||||
# Three quick checks on every pull request (CONTRIBUTING.md, "Sending a change"): no second open PR
|
||||
# for an issue a lower-numbered one already closes, the "Changes a default:" line is filled in, and
|
||||
# a PR adding to the showcase shows a picture. None is a required status check. The logic is in scripts/pr-checks/pr-checks.js (tested in ci.yml).
|
||||
# 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:
|
||||
@@ -48,21 +48,6 @@ jobs:
|
||||
const r = await checks.checkDefaults({ github, context, core });
|
||||
core.info(JSON.stringify(r));
|
||||
|
||||
picture:
|
||||
name: Picture 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.checkPicture({ 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:
|
||||
|
||||
@@ -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
|
||||
@@ -21,8 +21,6 @@
|
||||
# releases/latest/download/<name> links in the README keep working across releases.
|
||||
#
|
||||
# A dry run: dispatch with draft=false (the default). Only the release branch may create drafts.
|
||||
# The release gate (release-gate.yml) calls it as such a dry run, without secrets (unsigned) and
|
||||
# without the release job (packages_only).
|
||||
# A dry run from any branch also test-signs the Windows files when SIGNPATH_API_TOKEN exists:
|
||||
# gh workflow run release.yml --ref <branch> -f draft=false [-f signpath_signing_policy=test-signing]
|
||||
name: Release
|
||||
@@ -43,25 +41,6 @@ on:
|
||||
description: "SignPath signing policy for the Windows files (empty: the repository variable, else test-signing)"
|
||||
type: string
|
||||
default: ""
|
||||
# The release gate (release-gate.yml): a dry run (draft false) of every package and its checks.
|
||||
# The event stays the caller's (workflow_dispatch), so a call runs as a dispatched dry run does.
|
||||
# The gate passes no secrets, so nothing is signed (the signing steps skip without their
|
||||
# secrets, as on a fork), and packages_only skips the release job (checksums, update signing,
|
||||
# publishing).
|
||||
workflow_call:
|
||||
inputs:
|
||||
packages_only:
|
||||
description: "Build and check every package only: no release job"
|
||||
type: boolean
|
||||
default: false
|
||||
draft:
|
||||
description: "Create/update a draft (release branch only; off: build artifacts only)"
|
||||
type: boolean
|
||||
default: false
|
||||
signpath_signing_policy:
|
||||
description: "SignPath signing policy for the Windows files (empty: the repository variable, else test-signing)"
|
||||
type: string
|
||||
default: ""
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -234,8 +213,6 @@ jobs:
|
||||
if: needs.version.outputs.build == 'true'
|
||||
# makepkg only repackages the tarball's prebuilt binaries, so both architectures build here.
|
||||
runs-on: ubuntu-24.04
|
||||
# A pre-release or nightly without an Arch package is still worth having; a stable release isn't.
|
||||
continue-on-error: ${{ contains(needs.version.outputs.version, '-') }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -252,29 +229,10 @@ jobs:
|
||||
# makepkg in an archlinux:base-devel container. The package is named after the tarball's
|
||||
# .desktop file (io.tangencylabs.solidcraft) and written as
|
||||
# dist/release/solidcraft-<version>-linux-<arch>.pkg.tar.zst.
|
||||
# An Arch version can't hold a hyphen, and the action reads the version from the tarball's name
|
||||
# (a hyphenated one comes out as 0.0.0): 0.40.0-rc.1 goes in as 0.40.0rc.1, which pacman sorts
|
||||
# before 0.40.0, and the package keeps the release's name.
|
||||
- id: pkgver
|
||||
name: Arch version
|
||||
run: |
|
||||
pkgver="${VERSION//-/}"
|
||||
echo "pkgver=$pkgver" >>"$GITHUB_OUTPUT"
|
||||
if [ "$pkgver" != "$VERSION" ]; then
|
||||
cp "dist/linux/solidcraft-$VERSION-linux-$ARCH.tar.gz" "dist/linux/solidcraft-$pkgver-linux-$ARCH.tar.gz"
|
||||
fi
|
||||
env:
|
||||
ARCH: ${{ matrix.arch }}
|
||||
- uses: storytold/craft-common-actions/actions/arch-package@fa5ef18780cf5e7d5d3b8de068de56cffc83cac5 # main
|
||||
with:
|
||||
tarball: dist/linux/solidcraft-${{ steps.pkgver.outputs.pkgver }}-linux-${{ matrix.arch }}.tar.gz
|
||||
tarball: dist/linux/solidcraft-${{ env.VERSION }}-linux-${{ matrix.arch }}.tar.gz
|
||||
arch: ${{ matrix.arch }}
|
||||
- name: The package under the release's version
|
||||
if: steps.pkgver.outputs.pkgver != env.VERSION
|
||||
env:
|
||||
ARCH: ${{ matrix.arch }}
|
||||
PKGVER: ${{ steps.pkgver.outputs.pkgver }}
|
||||
run: sudo mv "dist/release/solidcraft-$PKGVER-linux-$ARCH.pkg.tar.zst" "dist/release/solidcraft-$VERSION-linux-$ARCH.pkg.tar.zst"
|
||||
- name: Check the package
|
||||
env:
|
||||
PKG: dist/release/solidcraft-${{ env.VERSION }}-linux-${{ matrix.arch }}.pkg.tar.zst
|
||||
@@ -392,7 +350,7 @@ jobs:
|
||||
name: macOS build ${{ matrix.arch }}
|
||||
needs: version
|
||||
if: needs.version.outputs.build == 'true'
|
||||
runs-on: macos-15
|
||||
runs-on: macos-14
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -426,7 +384,7 @@ jobs:
|
||||
name: macOS universal
|
||||
needs: [version, macos-build]
|
||||
if: needs.version.outputs.build == 'true'
|
||||
runs-on: macos-15
|
||||
runs-on: macos-14
|
||||
env:
|
||||
SOLIDCRAFT_VERSION: ${{ needs.version.outputs.version }}
|
||||
SOLIDCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
|
||||
@@ -654,8 +612,7 @@ jobs:
|
||||
release:
|
||||
name: GitHub Release or dry-run artifacts
|
||||
needs: [version, linux, flatpak, arch, snap, freebsd, macos, windows, web]
|
||||
# Not for the release gate (packages_only): it builds and checks the packages, nothing more.
|
||||
if: needs.version.outputs.build == 'true' && !inputs.packages_only
|
||||
if: needs.version.outputs.build == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
@@ -10,7 +10,6 @@ Work: ROADMAP.md, "Where help is wanted most" in CONTRIBUTING.md, issues labelle
|
||||
- No GPL/LGPL/AGPL code, not even reading it (OpenCASCADE, FreeCAD, SolveSpace, CadQuery, LibreCAD, QCAD): `cargo xtask licences --check`.
|
||||
- Every asset is original or openly licensed with a row in ATTRIBUTION.md; read `assets/AGENTS.md` before icon work: `cargo xtask assets`, `cargo xtask icons`.
|
||||
- Never crash: no unwrap/expect/panic/unsafe in non-test code (unsafe allowlist in crash-safety), errors are `Result`, input is hostile: clippy `-D warnings`, `hostile_params_never_panic`.
|
||||
- Model decisions (what an operation does, which bodies it touches, which face or edge a point or name means, depths, nearness) use exact geometry; a mesh is for display, export, previews and mesh bodies (`xtask/mesh-decisions.txt` only shrinks): `cargo xtask mesh-decisions`.
|
||||
- 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`.
|
||||
@@ -23,7 +22,6 @@ Work: ROADMAP.md, "Where help is wanted most" in CONTRIBUTING.md, issues labelle
|
||||
- A changed default keeps the old behaviour behind a preference and fills the PR's `Changes a default:` line, and no second PR for an issue a lower-numbered one closes: `pr-checks.yml`.
|
||||
- A regression test goes in the area's existing test file, never a per-bug file (#3226).
|
||||
- Look at open PRs first, claim with a draft PR (lowest number wins), one topic per PR, never force-push `main`, no pushes just to catch up with `main`: `skills/solidcraft-contributing/SKILL.md`.
|
||||
- A PR's title and first sentence make sense to someone who never saw your chat or the issue: `skills/solidcraft-contributing/SKILL.md`.
|
||||
- Show observable changes with real screenshots or a recording, captured yourself.
|
||||
- Run `cargo xtask ci` before every commit, without switching branches while it runs. `cargo xtask skills` keeps this file and `skills/` honest.
|
||||
|
||||
|
||||
@@ -84,8 +84,6 @@ Editable drawing code, native projects and shipped exports are collected under `
|
||||
| docs/book/src/generated/tool-preview.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/tool-preview.json`; gitignored) | MIT OR Apache-2.0 |
|
||||
| assets/fonts/liberation-sans/LiberationSans-Regular.ttf | Red Hat, Inc. (Liberation Fonts 2.x; digitized data Google) | https://github.com/liberationfonts (Debian fonts-liberation), unmodified; licence text in assets/fonts/liberation-sans/OFL.txt; used for sketch Text outlines | SIL OFL 1.1 |
|
||||
| assets/fonts/biz-udp-gothic/BIZUDPGothic-Regular.ttf | The BIZ UDGothic Project Authors | https://github.com/googlefonts/morisawa-biz-ud-gothic (Google Fonts), unmodified; licence text in assets/fonts/biz-udp-gothic/OFL.txt; used for web UI Japanese font | SIL OFL 1.1 |
|
||||
| assets/fonts/craft-ui-sc/CraftUI-SC.otf | Adobe / Google Inc. (Noto Sans SC); SolidCraft subset | https://github.com/notofonts/noto-cjk Sans/SubsetOTF/SC Regular, glyph-subset for assets/locales/zh.json and renamed Craft UI SC (OFL reserved-name); licence text in assets/fonts/craft-ui-sc/OFL.txt; web UI Simplified Chinese | SIL OFL 1.1 |
|
||||
| assets/fonts/craft-ui-tc/CraftUI-TC.otf | Adobe / Google Inc. (Noto Sans TC); SolidCraft subset | https://github.com/notofonts/noto-cjk Sans/SubsetOTF/TC Regular, glyph-subset for assets/locales/zh-TW.json and renamed Craft UI TC (OFL reserved-name); licence text in assets/fonts/craft-ui-tc/OFL.txt; web UI Traditional Chinese | SIL OFL 1.1 |
|
||||
| tests/step-conformance/cone_point.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/cone_point_vertex.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/cylinder.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
|
||||
@@ -103,22 +101,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 |
|
||||
@@ -143,8 +125,6 @@ Editable drawing code, native projects and shipped exports are collected under `
|
||||
| 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/fonts/biz-udp-gothic/OFL.txt | The BIZ UDGothic Project Authors | Unmodified accompanying font licence | SIL OFL 1.1 |
|
||||
| assets/fonts/craft-ui-sc/OFL.txt | Adobe / Google Inc. (Noto CJK); SolidCraft header | Accompanying OFL for CraftUI-SC.otf | SIL OFL 1.1 |
|
||||
| assets/fonts/craft-ui-tc/OFL.txt | Adobe / Google Inc. (Noto CJK); SolidCraft header | Accompanying OFL for CraftUI-TC.otf | 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 |
|
||||
| assets/fonts/liberation-sans/OFL.txt | Red Hat, Inc.; Liberation Fonts contributors | Unmodified accompanying font licence | SIL OFL 1.1 |
|
||||
@@ -205,7 +185,11 @@ Editable drawing code, native projects and shipped exports are collected under `
|
||||
| assets/shaders/viewport.wgsl | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
| assets/styles/ui/palettes.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
| assets/styles/ui/themes/gruvbox-dark.json | SolidCraft contributors | original SolidCraft theme file mapping SolidCraft's UI colours to the Gruvbox palette's colour values (Gruvbox by Pavel Pertsev, https://github.com/morhetz/gruvbox, MIT) | MIT OR Apache-2.0 |
|
||||
| assets/styles/ui/themes/gruvbox-dark-hard.json | SolidCraft contributors | original SolidCraft theme file mapping SolidCraft's UI colours to the Gruvbox palette's colour values (Gruvbox by Pavel Pertsev, https://github.com/morhetz/gruvbox, MIT) | MIT OR Apache-2.0 |
|
||||
| assets/styles/ui/themes/gruvbox-dark-soft.json | SolidCraft contributors | original SolidCraft theme file mapping SolidCraft's UI colours to the Gruvbox palette's colour values (Gruvbox by Pavel Pertsev, https://github.com/morhetz/gruvbox, MIT) | MIT OR Apache-2.0 |
|
||||
| assets/styles/ui/themes/gruvbox-light.json | SolidCraft contributors | original SolidCraft theme file mapping SolidCraft's UI colours to the Gruvbox palette's colour values (Gruvbox by Pavel Pertsev, https://github.com/morhetz/gruvbox, MIT) | MIT OR Apache-2.0 |
|
||||
| assets/styles/ui/themes/gruvbox-light-hard.json | SolidCraft contributors | original SolidCraft theme file mapping SolidCraft's UI colours to the Gruvbox palette's colour values (Gruvbox by Pavel Pertsev, https://github.com/morhetz/gruvbox, MIT) | MIT OR Apache-2.0 |
|
||||
| assets/styles/ui/themes/gruvbox-light-soft.json | SolidCraft contributors | original SolidCraft theme file mapping SolidCraft's UI colours to the Gruvbox palette's colour values (Gruvbox by Pavel Pertsev, https://github.com/morhetz/gruvbox, MIT) | MIT OR Apache-2.0 |
|
||||
| assets/styles/ui/themes/solidcraft-95.json | SolidCraft contributors | original SolidCraft theme file | MIT OR Apache-2.0 |
|
||||
| assets/styles/ui/themes/platinum-9.json | SolidCraft contributors | original SolidCraft theme file | MIT OR Apache-2.0 |
|
||||
| assets/styles/ui/themes/cupertino.json | SolidCraft contributors | original SolidCraft theme file | MIT OR Apache-2.0 |
|
||||
@@ -625,153 +609,6 @@ Original SolidCraft drawings, Open for owner review; no runtime assignments.
|
||||
| assets/icons/ui/design/svg/model/timeline_stop.svg | SolidCraft contributors | Original history playback icon; canonical grey_cube base and original action cue | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/flat/status_pin.svg | SolidCraft contributors | Original analytic pin icon for persistent viewport results | MIT OR Apache-2.0 |
|
||||
| assets/locales/pending/render-wear.json | SolidCraft contributors | Original UI labels and translations | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/equation_curve.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
| assets/locales/pending/sketch-recent-snap.json | SolidCraft contributors | Original preference messages for the recent centre and midpoint snap | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/construction_line.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/construction_line_horizontal.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/construction_line_vertical.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/construction_line_angle.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/construction_line_parallel.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/construction_line_perpendicular.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/construction_line_tangent_external.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/construction_line_tangent_at.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/construction_line_tangent_two.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/construction_line_bisector.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| docs/book/src/generated/equation-circle.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/equation-circle.json`; gitignored) | MIT OR Apache-2.0 |
|
||||
| docs/book/src/generated/equation-flower.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/equation-flower.json`; gitignored) | MIT OR Apache-2.0 |
|
||||
| docs/book/src/generated/equation-involute.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/equation-involute.json`; gitignored) | MIT OR Apache-2.0 |
|
||||
| docs/book/src/generated/equation-line.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/equation-line.json`; gitignored) | MIT OR Apache-2.0 |
|
||||
| docs/book/src/generated/equation-parabola.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/equation-parabola.json`; gitignored) | MIT OR Apache-2.0 |
|
||||
| docs/book/src/generated/equation-spiral.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/equation-spiral.json`; gitignored) | MIT OR Apache-2.0 |
|
||||
| docs/book/src/generated/equation-wave.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/equation-wave.json`; gitignored) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/print_face.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
| assets/locales/fi.json | SolidCraft contributors | Original Finnish translation of SolidCraft's English catalog, AI-assisted (Claude Code) clean-room from the English and not yet reviewed line by line by a native speaker; no commercial CAD text used | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/plane_axis_point.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/aa-2x.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/aa-2x.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/aa-4x.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/aa-4x.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/aa-8x.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/aa-8x.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/aa-off.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/aa-off.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/agent-pointer-speed.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/agent-pointer-speed.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/ao-on.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/ao-on.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/assembly-move.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/assembly-move.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/browser-card.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/browser-card.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/browser-column.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/browser-column.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/colour-vision-axes.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/colour-vision-axes.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/colour-vision-bends.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/colour-vision-bends.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/colour-vision-draft.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/colour-vision-draft.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/colour-vision-failed.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/colour-vision-failed.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/colour-vision-fixed.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/colour-vision-fixed.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/colour-vision-projected.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/colour-vision-projected.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/direct-interaction.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/direct-interaction.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/frame-rate.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/frame-rate.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/history-bottom.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/history-bottom.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/history-in-browser.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/history-in-browser.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/history-left.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/history-left.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/history-under-browser.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/history-under-browser.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/layout-schematic.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/layout-schematic.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/measure-axis-distances.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/measure-axis-distances.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/mouse-mapping.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/mouse-mapping.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/navigator-axes.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/navigator-axes.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/navigator-cube.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/navigator-cube.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/orbit-centre.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/orbit-centre.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/orbit-cursor.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/orbit-cursor.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/orbit-origin.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/orbit-origin.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/orientation-y-up.dark.svg | SolidCraft contributors | original SolidCraft artwork: the Orientation figure copied from the app and extracted from the Preferences redesign board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/orientation-y-up.light.svg | SolidCraft contributors | original SolidCraft artwork: the Orientation figure copied from the app and extracted from the Preferences redesign board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/orientation-z-up.dark.svg | SolidCraft contributors | original SolidCraft artwork: the Orientation figure copied from the app and extracted from the Preferences redesign board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/orientation-z-up.light.svg | SolidCraft contributors | original SolidCraft artwork: the Orientation figure copied from the app and extracted from the Preferences redesign board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/quick-measure.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/quick-measure.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/select-bodies.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/select-bodies.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/select-faces-edges.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/select-faces-edges.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/sketch-conflict.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/sketch-conflict.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/sketch-exit-keep.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/sketch-exit-keep.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/sketch-exit-previous.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/sketch-exit-previous.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/sketch-exit-unless-turned.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/sketch-exit-unless-turned.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/sketch-guides.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/sketch-guides.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/sketch-hide-glyphs.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/sketch-hide-glyphs.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/sketch-state-icons.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/sketch-state-icons.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/style-shaded-edges.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/style-shaded-edges.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/tess-coarse.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/tess-coarse.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/tess-fine.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/tess-fine.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/tess-normal.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/tess-normal.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/tess-ultra.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/tess-ultra.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/theme-dark.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/theme-dark.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/theme-gruvbox.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/theme-gruvbox.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/theme-imported.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/theme-imported.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/theme-light.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/theme-light.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/theme-system-dark-half.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/theme-system-dark-half.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/theme-system-light-half.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/theme-system-light-half.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/tool-cursor.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/tool-cursor.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/tool-extrude.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/tool-extrude.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/tool-hole.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/tool-hole.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/tool-placement.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/tool-placement.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/vertex-markers.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/vertex-markers.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/view-angles.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/view-angles.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/view-grid.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/view-grid.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/view-perspective.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/view-perspective.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/view-shadow.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/view-shadow.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| `assets/locales/pending/sketch-offset-instances.json` | SolidCraft contributors | Original offset instance-count label and translations | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/flat/settings_profile.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original analytic head and shoulder geometry for settings profiles | MIT OR Apache-2.0 |
|
||||
| tests/kernel-cases/import/4561-convert-box-mesh-with-a-zero-area-triangle/box-degenerate.stl | SolidCraft contributors | synthetic ASCII STL written by hand: a box with one zero-area triangle, for the kernel regression case of tangency-labs/solidcraft#4561 | MIT OR Apache-2.0 |
|
||||
|
||||
## Code adapted from sibling projects
|
||||
|
||||
@@ -811,15 +648,6 @@ in THIRD-PARTY-LICENSES.txt; no images or artwork from that project are copied.
|
||||
The glTF test files are built by the tests themselves (crates/io/src/gltf_in_tests.rs,
|
||||
crates/engine/src/tests/gltf_import.rs); no third-party models are committed.
|
||||
|
||||
## Geometry kernel crates
|
||||
|
||||
| Crate | Used for | Licence |
|
||||
|---|---|---|
|
||||
| solidcore (https://github.com/tangency-labs/solidcore, pinned by rev in Cargo.toml) | the Rust port of NVIDIA's SMLib: the B-rep kernel behind crates/smlib | Apache-2.0 (its NOTICE credits NVIDIA's USD BRep) |
|
||||
|
||||
Linked as a crate; no code from it is copied into SolidCraft. Its licence text and NOTICE go
|
||||
into THIRD-PARTY-LICENSES.txt when `main` regenerates it.
|
||||
|
||||
## FreeCAD and Rhino import library
|
||||
|
||||
FreeCAD `.FCStd` and Rhino `.3dm` files are read with [cadmpeg](https://github.com/cadmpeg/cadmpeg)
|
||||
@@ -835,13 +663,16 @@ copied into SolidCraft.
|
||||
| SolidCraft file | Original contribution | Licence |
|
||||
|---|---|---|
|
||||
| crates/ui-egui/src/viewport.rs (`nearest_quarter_turn`, `axis_view_orbit`) | N3MI-DG's [PR #101](https://github.com/bherbruck/solidcraft/pull/101): [minimal TOP/BOTTOM turns](https://github.com/bherbruck/solidcraft/commit/c28e780f52490784ef5886f094b9d3f6d10c3e3f) and [drag-to-orbit](https://github.com/bherbruck/solidcraft/commit/f30b30be6ecc4e2e6544a851243137c01a354934), adapted to the axis gizmo | MIT OR Apache-2.0 |
|
||||
| crates/smlib/src/core/offset.rs (`draft`: the tilted planes and `SmBrep::LocalOperation`, the tail of the capi's `draft_faces`) | ablasik's `draft` in `crates/smlib/src/ffi/rs_ops2.rs` on [ablasik/solidcraft `app/solidcore-backend-on-develop`](https://github.com/ablasik/solidcraft/blob/9c7e8e31565e1823f0591b58e2967351c32af13f/crates/smlib/src/ffi/rs_ops2.rs), adapted to `core::offset`'s errors and checks | MIT OR Apache-2.0 |
|
||||
|
||||
## Vendored third-party code
|
||||
|
||||
| Directory | Source | Licence | Changes |
|
||||
|---|---|---|---|
|
||||
| vendor/accesskit_macos | accesskit_macos 0.26.3, https://github.com/AccessKit/accesskit (The AccessKit contributors) | MIT OR Apache-2.0 | the subclassing adapter and its tests from 0.27.2: dropping the adapter keeps AppKit's key-value observers (vendor/README.md) |
|
||||
| vendor/truck-meshalgo | truck-meshalgo 0.4.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | meshing: bounded parameter searches, collapsed-boundary projection, subdivision and provisional fixed-grid previews (vendor/README.md) |
|
||||
| vendor/truck-stepio | truck-stepio 0.3.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | STEP writer: intersection-curve surfaces written at unique ids (vendor/README.md) |
|
||||
| vendor/truck-shapeops | truck-shapeops 0.4.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | robust meshing and nearest-parameter fallback in booleans; degenerate intersection-curve rejection; face and edge history of booleans; trace macro; corner choice at vertices several intersection curves end on; reuse of an operand's meshes (vendor/README.md) |
|
||||
| vendor/truck-geometry | truck-geometry 0.5.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | intersection curves divided from their neighbours' surface parameters; surface second derivatives (vendor/README.md) |
|
||||
| vendor/matext4cgmath | matext4cgmath 0.1.0, https://github.com/ricosjp/matext4cgmath (RICOS Co. Ltd.) | Apache-2.0 | root refinement stops after 100 Newton steps (vendor/README.md) |
|
||||
|
||||
## Vendored renderer compatibility fix
|
||||
|
||||
|
||||
@@ -19,13 +19,12 @@ Questions, ideas or want to say what you're working on? Join the [Discord](https
|
||||
|
||||
## Kernel work is paused
|
||||
|
||||
We're moving to a new geometry kernel. Until that's done, please don't open pull requests that
|
||||
change `crates/kernel/`, `crates/geom/` or `vendor/truck*`, or issues and discussions about kernel
|
||||
work: booleans, fillets, chamfers, shell, offsets, sweeps and lofts, threads and holes, STEP/IGES
|
||||
import and export, or tessellation. The pinned issue on the tracker explains it. New ones get the
|
||||
`kernel-paused` label (`.github/workflows/kernel-paused.yml`) and a maintainer answers them. Crashes
|
||||
and data loss are still taken as issues. Pull requests outside those directories carry on as normal,
|
||||
including the dialogs and tools for these features.
|
||||
We're evaluating a new geometry kernel. Until that's decided, please don't open issues, pull
|
||||
requests or discussions about kernel work: booleans, fillets, chamfers, shell, offsets, sweeps and
|
||||
lofts, threads and holes, STEP/IGES import and export, tessellation, or anything else that changes
|
||||
`crates/kernel/`. The pinned issue on the tracker explains it. New ones get the `kernel-paused`
|
||||
label (`.github/workflows/kernel-paused.yml`) and a maintainer answers them. Crashes and data loss
|
||||
are still taken as issues, and everything outside the kernel carries on as normal.
|
||||
|
||||
## Get it running (about 10 minutes on a fast machine)
|
||||
|
||||
@@ -161,11 +160,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.
|
||||
|
||||
@@ -224,7 +221,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
|
||||
@@ -244,8 +242,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.
|
||||
|
||||
@@ -341,15 +338,6 @@ and a fresh CI run:
|
||||
PRs in groups by area, so PRs that touch the same code meet a conflict at most once.
|
||||
4. Reference the issue (`Fixes #12`). In the PR, say what you tested and how. For geometry work, include the measured numbers (and
|
||||
the expected ones).
|
||||
5. Write the PR title and first sentence for someone who hasn't seen your chat, the issue or the
|
||||
code. Maintainers pick what to review from the title, and it becomes the commit on `main`. Say
|
||||
which part of the app changes and what a user now sees or can do, and name things rather than
|
||||
leaning on "there", "it" or "this".
|
||||
- Unclear: `Send to Slicer: pick one print face per body there, and show print faces only while it is open`
|
||||
- Clear: `3D printing: choose the face each body rests on in the Send to Slicer window, not the face menu`
|
||||
|
||||
Use plain, literal English in PR text: one idea per sentence, active voice, and one word for one
|
||||
thing. This helps everyone, including readers whose first language isn't English.
|
||||
|
||||
Commenting `/claim` assigns you, adds the `in progress` label, and replies with its automatic cleanup
|
||||
deadline. It frees a claim with no linked PR after 72 hours, or a claim with a PR after 14 days
|
||||
@@ -358,13 +346,6 @@ without pushes. These automatic cleanup deadlines are separate from the daily pr
|
||||
`/unclaim` releases the bot claim early (only you or a maintainer can). Issues a maintainer has
|
||||
taken are never freed automatically.
|
||||
|
||||
**A PR has to explain itself.** Start the description with what it is, in plain words someone new
|
||||
to the project would understand: what it does and who it's for. Don't assume anyone knows the name
|
||||
of your tool, project or feature. If there's anything to look at, show it in the description: a
|
||||
picture or video has to be visible there without opening the changed files. "The GIF is the change"
|
||||
doesn't count. A PR that doesn't do this waits until it does. For a PR that adds to
|
||||
`docs/showcase.md` or `docs/screenshots/`, the Picture check fails until the description shows one.
|
||||
|
||||
Help reviewers see what the words mean. For observable CAD, UI or model behavior, please
|
||||
include actual descriptive screenshots or a short recording in the issue or PR. This evidence
|
||||
is required for changes or bugs that can be demonstrated in the app, including kernel,
|
||||
|
||||
@@ -60,6 +60,8 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "accesskit_macos"
|
||||
version = "0.26.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce02dc63b43f0c9296af9ac946312a2dc8814427d7a64d2d600971dac55b6076"
|
||||
dependencies = [
|
||||
"accesskit",
|
||||
"accesskit_consumer 0.38.0",
|
||||
@@ -186,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"
|
||||
@@ -206,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"
|
||||
@@ -518,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"
|
||||
@@ -765,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"
|
||||
@@ -787,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"
|
||||
@@ -857,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"
|
||||
@@ -1034,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"
|
||||
@@ -1180,7 +1249,7 @@ dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"emath",
|
||||
"epaint",
|
||||
"itertools",
|
||||
"itertools 0.15.0",
|
||||
"log",
|
||||
"nohash-hasher",
|
||||
"profiling",
|
||||
@@ -1598,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 +1985,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 +2168,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 +2271,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 +2432,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 +3316,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 +3512,15 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rclite"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e09665c494a9de2bd230e1aedc0c2fb20481a51f28f7db84d0c9bcfe2fe537b2"
|
||||
dependencies = [
|
||||
"branches",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "read-fonts"
|
||||
version = "0.41.0"
|
||||
@@ -4051,17 +4208,9 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcore"
|
||||
version = "1.0.1"
|
||||
source = "git+https://github.com/tangency-labs/solidcore?rev=8ddce901671f1cebd9214a31d21895d725e886c8#8ddce901671f1cebd9214a31d21895d725e886c8"
|
||||
dependencies = [
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft"
|
||||
version = "0.40.0-rc.1"
|
||||
version = "0.26.1"
|
||||
dependencies = [
|
||||
"block2 0.6.2",
|
||||
"eframe",
|
||||
@@ -4093,29 +4242,27 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
"winit",
|
||||
"x11rb",
|
||||
"zbus 5.19.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-3dx-client"
|
||||
version = "0.40.0-rc.1"
|
||||
version = "0.26.1"
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-cli"
|
||||
version = "0.40.0-rc.1"
|
||||
version = "0.26.1"
|
||||
dependencies = [
|
||||
"image",
|
||||
"log",
|
||||
"serde_json",
|
||||
"solidcraft-engine",
|
||||
"solidcraft-gpu",
|
||||
"solidcraft-kernel",
|
||||
"solidcraft-mcp",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-derive"
|
||||
version = "0.40.0-rc.1"
|
||||
version = "0.26.1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -4124,7 +4271,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-doc"
|
||||
version = "0.40.0-rc.1"
|
||||
version = "0.26.1"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
@@ -4133,7 +4280,6 @@ dependencies = [
|
||||
"solidcraft-derive",
|
||||
"solidcraft-geom",
|
||||
"solidcraft-kernel",
|
||||
"solidcraft-profile",
|
||||
"solidcraft-render",
|
||||
"solidcraft-sketch",
|
||||
"thiserror 2.0.21",
|
||||
@@ -4142,7 +4288,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-engine"
|
||||
version = "0.40.0-rc.1"
|
||||
version = "0.26.1"
|
||||
dependencies = [
|
||||
"image",
|
||||
"log",
|
||||
@@ -4154,7 +4300,6 @@ dependencies = [
|
||||
"solidcraft-geom",
|
||||
"solidcraft-io",
|
||||
"solidcraft-kernel",
|
||||
"solidcraft-profile",
|
||||
"solidcraft-render",
|
||||
"solidcraft-sketch",
|
||||
"thiserror 2.0.21",
|
||||
@@ -4164,7 +4309,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-geom"
|
||||
version = "0.40.0-rc.1"
|
||||
version = "0.26.1"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -4173,7 +4318,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-gpu"
|
||||
version = "0.40.0-rc.1"
|
||||
version = "0.26.1"
|
||||
dependencies = [
|
||||
"image",
|
||||
"log",
|
||||
@@ -4193,7 +4338,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-io"
|
||||
version = "0.40.0-rc.1"
|
||||
version = "0.26.1"
|
||||
dependencies = [
|
||||
"cadmpeg-core",
|
||||
"cadmpeg-ir",
|
||||
@@ -4211,15 +4356,13 @@ dependencies = [
|
||||
"solidcraft-doc",
|
||||
"solidcraft-geom",
|
||||
"solidcraft-kernel",
|
||||
"solidcraft-profile",
|
||||
"solidcraft-testkit",
|
||||
"thiserror 2.0.21",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-kernel"
|
||||
version = "0.40.0-rc.1"
|
||||
version = "0.26.1"
|
||||
dependencies = [
|
||||
"log",
|
||||
"proptest",
|
||||
@@ -4227,35 +4370,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.40.0-rc.1"
|
||||
version = "0.26.1"
|
||||
dependencies = [
|
||||
"image",
|
||||
"serde_json",
|
||||
"solidcraft-engine",
|
||||
"solidcraft-gpu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-profile"
|
||||
version = "0.40.0-rc.1"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-render"
|
||||
version = "0.40.0-rc.1"
|
||||
version = "0.26.1"
|
||||
dependencies = [
|
||||
"image",
|
||||
"moxcms",
|
||||
@@ -4264,13 +4400,12 @@ dependencies = [
|
||||
"serde_json",
|
||||
"sha2 0.10.9",
|
||||
"solidcraft-geom",
|
||||
"web-time",
|
||||
"weezl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-sketch"
|
||||
version = "0.40.0-rc.1"
|
||||
version = "0.26.1"
|
||||
dependencies = [
|
||||
"harfrust",
|
||||
"serde",
|
||||
@@ -4281,40 +4416,16 @@ dependencies = [
|
||||
"unicode-bidi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-smlib"
|
||||
version = "0.40.0-rc.1"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"log",
|
||||
"serde_json",
|
||||
"solidcore",
|
||||
"solidcraft-geom",
|
||||
"solidcraft-profile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-space-mouse"
|
||||
version = "0.40.0-rc.1"
|
||||
version = "0.26.1"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-step"
|
||||
version = "0.40.0-rc.1"
|
||||
dependencies = [
|
||||
"solidcraft-geom",
|
||||
"solidcraft-profile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-testkit"
|
||||
version = "0.40.0-rc.1"
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-ui-egui"
|
||||
version = "0.40.0-rc.1"
|
||||
version = "0.26.1"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"egui",
|
||||
@@ -4333,7 +4444,6 @@ dependencies = [
|
||||
"solidcraft-gpu",
|
||||
"solidcraft-space-mouse",
|
||||
"syn 2.0.119",
|
||||
"sys-locale",
|
||||
"tiny-skia",
|
||||
"toml",
|
||||
"ttf-parser",
|
||||
@@ -4345,7 +4455,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-web"
|
||||
version = "0.40.0-rc.1"
|
||||
version = "0.26.1"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"egui",
|
||||
@@ -4406,6 +4516,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"
|
||||
@@ -4439,18 +4559,6 @@ dependencies = [
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sys-locale"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.27.0"
|
||||
@@ -4458,7 +4566,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.4.3",
|
||||
"getrandom 0.3.4",
|
||||
"once_cell",
|
||||
"rustix 1.1.5",
|
||||
"windows-sys 0.61.2",
|
||||
@@ -4655,6 +4763,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"
|
||||
@@ -5752,10 +5998,9 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.40.0-rc.1"
|
||||
version = "0.26.1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quick-xml",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.10.9",
|
||||
|
||||
@@ -5,7 +5,7 @@ members = ["crates/*", "apps/*", "xtask"]
|
||||
exclude = ["vendor"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.40.0-rc.1"
|
||||
version = "0.26.1"
|
||||
edition = "2024"
|
||||
license = "MIT OR Apache-2.0"
|
||||
rust-version = "1.95"
|
||||
@@ -16,11 +16,8 @@ repository = "https://github.com/bherbruck/solidcraft"
|
||||
robust = "1.2"
|
||||
# internal
|
||||
solidcraft-geom = { path = "crates/geom" }
|
||||
solidcraft-profile = { path = "crates/profile" }
|
||||
solidcraft-sketch = { path = "crates/sketch" }
|
||||
solidcraft-kernel = { path = "crates/kernel" }
|
||||
solidcraft-smlib = { path = "crates/smlib" }
|
||||
solidcraft-step = { path = "crates/step" }
|
||||
solidcraft-render = { path = "crates/render" }
|
||||
solidcraft-gpu = { path = "crates/gpu" }
|
||||
solidcraft-doc = { path = "crates/doc" }
|
||||
@@ -31,13 +28,6 @@ solidcraft-space-mouse = { path = "crates/space-mouse" }
|
||||
solidcraft-mcp = { path = "crates/mcp" }
|
||||
solidcraft-derive = { path = "crates/derive" }
|
||||
solidcraft-3dx-client = { path = "crates/threedx-client" }
|
||||
# The Rust port of NVIDIA's SMLib (Apache-2.0), the B-rep kernel behind crates/smlib. Pinned;
|
||||
# move the rev on its own commit. For a
|
||||
# local checkout, add `[patch."https://github.com/tangency-labs/solidcore"] solidcore = { path =
|
||||
# "../solidcore/crates/solidcore" }` to a `.cargo/config.toml` outside the repository.
|
||||
solidcore = { git = "https://github.com/tangency-labs/solidcore", rev = "8ddce901671f1cebd9214a31d21895d725e886c8" }
|
||||
# test tooling: a dev-dependency only (xtask/src/layers.rs)
|
||||
solidcraft-testkit = { path = "crates/testkit" }
|
||||
|
||||
# foundation
|
||||
thiserror = "2"
|
||||
@@ -55,7 +45,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"
|
||||
|
||||
@@ -128,7 +125,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
|
||||
@@ -151,9 +150,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" }
|
||||
|
||||
@@ -161,9 +165,6 @@ egui-wgpu = { path = "vendor/egui-wgpu" }
|
||||
# winit 0.30 never binds and eframe 0.36 still requires winit 0.30.
|
||||
winit = { path = "vendor/winit" }
|
||||
|
||||
# macOS: dropping the accessibility adapter leaves AppKit's key-value observers intact (#7535).
|
||||
accesskit_macos = { path = "vendor/accesskit_macos" }
|
||||
|
||||
# Mutation testing (cargo-mutants, docs/mutation-testing.md): the ci profile with incremental
|
||||
# builds, since every mutant rebuilds the crates above the one it changes.
|
||||
[profile.mutants]
|
||||
|
||||
@@ -42,8 +42,8 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<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" alt="tangency-labs%2Fsolidcraft | Trendshift" width="250" height="55"/></a>
|
||||
<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>
|
||||
<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" alt="tangency-labs%2Fsolidcraft | Trendshift" width="250" height="55"/></a>
|
||||
<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>
|
||||
|
||||

|
||||
@@ -98,7 +98,7 @@ More commands, the Windows cross-build and the browser build are in [docs/buildi
|
||||
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:
|
||||
`"C:\Program Files\SolidCraft\solidcraft-cli.exe"`). Tools, access rules and an example session:
|
||||
[docs/mcp.md](docs/mcp.md).
|
||||
|
||||
## Scripts
|
||||
@@ -150,4 +150,3 @@ Tinkercad are trademarks of their respective owners, named only to describe comp
|
||||
[linux-appimage]: https://github.com/tangency-labs/solidcraft/releases/latest/download/solidcraft-linux-x86_64.AppImage
|
||||
[snap-store]: https://snapcraft.io/solidcraft
|
||||
[web-app]: https://solidcraft.io/app
|
||||
|
||||
|
||||
@@ -662,9 +662,7 @@ completed drawings publish atomically. The browser still exports synchronously.
|
||||
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. A fold crosses the sheet's own outer edges the same way, slanted or on any curve,
|
||||
with the outline exact in the flat pattern and its DXF, and flanges and other folds still go
|
||||
on the panels.
|
||||
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
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
## Supported versions
|
||||
|
||||
SolidCraft is young and moves quickly, so only the latest release gets security fixes. Please
|
||||
update to the [latest release](https://github.com/tangency-labs/solidcraft/releases/latest) before
|
||||
update to the [latest release](https://github.com/bherbruck/solidcraft/releases/latest) before
|
||||
reporting, and check whether the problem is still on `main`.
|
||||
|
||||
| Version | Supported |
|
||||
@@ -15,46 +15,18 @@ reporting, and check whether the problem is still on `main`.
|
||||
## Reporting a vulnerability
|
||||
|
||||
Please **don't open a public issue** for a security problem. Report it privately through GitHub:
|
||||
open the repository's [Security tab](https://github.com/tangency-labs/solidcraft/security) and choose
|
||||
open the repository's [Security tab](https://github.com/bherbruck/solidcraft/security) and choose
|
||||
**Report a vulnerability**
|
||||
([direct link](https://github.com/tangency-labs/solidcraft/security/advisories/new)).
|
||||
([direct link](https://github.com/bherbruck/solidcraft/security/advisories/new)).
|
||||
|
||||
Please include what you found, the steps you ran to reproduce it (a design file, command or MCP
|
||||
request), the version and platform, and what an attacker gains. I'll acknowledge the report as soon
|
||||
as I can, keep you updated in the advisory, and credit you when the fix is published unless you'd
|
||||
rather stay anonymous.
|
||||
Please include what you found, how to reproduce it (a design file, command or MCP request), the
|
||||
version and platform, and the impact you expect. I'll acknowledge the report as soon as I can,
|
||||
keep you updated in the advisory, and credit you when the fix is published unless you'd rather
|
||||
stay anonymous.
|
||||
|
||||
## What counts as a security issue
|
||||
Areas where reports are especially useful:
|
||||
|
||||
Advisories are for problems where someone gets something they shouldn't, without already having
|
||||
access to your computer or your account. For example:
|
||||
|
||||
- opening a design, STEP, STL or 3MF file from someone else runs code, or reads or overwrites files
|
||||
it shouldn't
|
||||
- a website or another computer can reach or control SolidCraft
|
||||
- an agent gets past a prompt that promised to ask you first
|
||||
- the web app reaches beyond the page's own storage
|
||||
- a release package or an update can be tampered with
|
||||
- an API key or token gets sent somewhere it shouldn't go
|
||||
|
||||
## What to report as a normal bug
|
||||
|
||||
Please [open an issue](https://github.com/tangency-labs/solidcraft/issues/new) for these. They're
|
||||
still worth fixing, but they aren't security issues:
|
||||
|
||||
- anything that needs a program already running on your computer, or the control token. A program
|
||||
running as you can already do what you can.
|
||||
- MCP's allowed folders. They guard against accidents, they aren't a security boundary, and an agent
|
||||
with a shell of its own can already reach those files. Getting around them isn't a vulnerability.
|
||||
- crashes, hangs, or heavy memory or CPU use, unless they also lead to something in the list above
|
||||
- anything that only happens in an unusual setup you'd have to choose on purpose
|
||||
|
||||
## Before you send it
|
||||
|
||||
- **Run it.** Reproduce the problem on the latest release. If you only read the code, say so, and
|
||||
expect it to wait.
|
||||
- **Check it yourself.** If a scanner or an AI agent found it, read the code and confirm it first.
|
||||
These tools tend to overrate what they find.
|
||||
- **Say what's gained.** Name what the attacker needs and what they get. I set the final severity.
|
||||
|
||||
Reports that don't fit will be closed with a short note, or moved to the issue tracker.
|
||||
- opening untrusted design, STEP, STL or 3MF files (crashes, excessive memory or CPU, path tricks)
|
||||
- the MCP server and the control API (file access outside the allowed folders, command injection)
|
||||
- the web app (anything reaching beyond the page's own storage)
|
||||
- release packages and the update paths (AppImage, Flatpak, MSI, deb, rpm, snap)
|
||||
|
||||
@@ -20,12 +20,9 @@ name = "solvecraft-cli"
|
||||
path = "src/alias.rs"
|
||||
|
||||
[features]
|
||||
default = ["raytrace-cpu", "profile"]
|
||||
default = ["raytrace-cpu"]
|
||||
bundled-render-library = ["solidcraft-engine/bundled-render-library"]
|
||||
raytrace-cpu = ["solidcraft-engine/raytrace-cpu"]
|
||||
# The profiler, compiled in so any build can say where its time goes (`--profile`,
|
||||
# SOLIDCRAFT_PROFILE=1); it records nothing until asked.
|
||||
profile = ["solidcraft-engine/profile"]
|
||||
|
||||
[dependencies]
|
||||
solidcraft-engine = { workspace = true }
|
||||
@@ -37,5 +34,3 @@ serde_json = { workspace = true }
|
||||
[dev-dependencies]
|
||||
# Snapshot tests decode the pictures to compare them with a tolerance.
|
||||
image = { workspace = true }
|
||||
# Tests measure meshes at their own tolerances.
|
||||
solidcraft-kernel = { workspace = true }
|
||||
|
||||
@@ -66,7 +66,7 @@ fn run() -> Result<(), String> {
|
||||
let mut session = Session::default();
|
||||
eprintln!("Generating original CAD scene from {recipe_path}");
|
||||
let generation_start = Instant::now();
|
||||
let (commands, command_timings) = session.run_script_timed(&recipe, false, &mut |_, _| {}).map_err(|e| e.to_string())?;
|
||||
let (commands, command_timings) = session.run_script_timed(&recipe, false).map_err(|e| e.to_string())?;
|
||||
let generation_seconds = generation_start.elapsed().as_secs_f64();
|
||||
let design_path = output.with_extension(solidcraft_engine::io::DESIGN_EXT);
|
||||
session.execute("file.save_as", &json!({"path":design_path})).map_err(|e| e.to_string())?;
|
||||
|
||||
@@ -184,7 +184,7 @@ pub fn run(args: &[String]) -> Result<(), String> {
|
||||
s.execute(id, &c["params"]).map_err(|e| format!("command {i} ({id}): {e}"))?;
|
||||
run.push(t.elapsed().as_secs_f64() * 1000.0);
|
||||
let t = std::time::Instant::now();
|
||||
s.world_state().map_err(|e| e.to_string())?.warm_meshes(|| true);
|
||||
s.world_state().warm_meshes(|| true);
|
||||
shown.push(t.elapsed().as_secs_f64() * 1000.0);
|
||||
if run.len() == 100 || i + 1 == commands.len() {
|
||||
let mean = |v: &[f64]| v.iter().sum::<f64>() / v.len().max(1) as f64;
|
||||
@@ -198,7 +198,7 @@ pub fn run(args: &[String]) -> Result<(), String> {
|
||||
shown.clear();
|
||||
}
|
||||
}
|
||||
let world = s.world_state().map_err(|e| e.to_string())?;
|
||||
let world = s.world_state();
|
||||
println!(
|
||||
"{}",
|
||||
json!({"step": "built", "ms": began.elapsed().as_secs_f64() * 1000.0, "commands": commands.len(), "features": s.doc.features.len(), "bodies": world.bodies.len()})
|
||||
@@ -236,7 +236,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
assert!(s.model.results.iter().all(|r| r.error.is_none()));
|
||||
let world = s.world_state().unwrap();
|
||||
let world = s.world_state();
|
||||
assert!(world.bodies.len() > 40, "{} bodies", world.bodies.len());
|
||||
assert!(world.bodies.iter().any(|b| b.name.contains("spoke")));
|
||||
}
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
//!
|
||||
//! ```text
|
||||
//! solidcraft-cli run <script.json|design.sc|part.step|mesh.3mf> [--in design|part.step|mesh.3mf] [--out FILE]... [--save FILE] [--quiet] [--results] [--timings] [--profile] [--json-out FILE] [--allow-errors]
|
||||
//! [--param-check off|warn|strict]
|
||||
//! solidcraft-cli eval <script.json|design.sc|part.step> [--in design.sc|part.step] [--json-out FILE] (alias: inspect) measurements as JSON
|
||||
//! solidcraft-cli script <design.sc> [--out script.json] the design's history as a command script
|
||||
//! solidcraft-cli bench-open <design.sc | part.step> [--parse-only] [--save OUT.sc] [--display LEVEL [--edit NAME EXPR]] [--ops script.json] loading timings as JSON lines
|
||||
@@ -10,13 +9,12 @@
|
||||
//! solidcraft-cli bench-assistant --connect 127.0.0.1:PORT [--token T] --base-url URL --model NAME [--tasks 1,2,5] [--repeat N]
|
||||
//! [--timeout-s 300] [--json out.json] [--markdown out.md]
|
||||
//! the standard Assistant model test (#4851) on a running app
|
||||
//! solidcraft-cli snapshot <script|design> [--in design|part.step] --out shot.png [--width W] [--height H] [--view iso|front|top|…|NAME | --camera JSON] [--no-grid] [--no-axes] [--quality LEVEL]
|
||||
//! solidcraft-cli snapshot <script|design> [--in design|part.step] --out shot.png [--width W] [--height H] [--view iso|front|top|…|NAME | --camera JSON] [--no-grid] [--no-axes]
|
||||
//! [--body NAME:opacity=0.3,color=#rrggbb]... [--tool NAME]... [--section z=27]
|
||||
//! solidcraft-cli exec <command> [json-params] run one command on an empty design
|
||||
//! solidcraft-cli commands the command registry as JSON
|
||||
//! solidcraft-cli recipe <recipe.json> translate an oracle recipe to a script
|
||||
//! solidcraft-cli oracle <case-dir>... replay recipes and compare with expected.json
|
||||
//! solidcraft-cli kernel-cases [--json] [--dumps=DIR] [--validation=MODE] <case-dir>... the kernel regression rig (tests/kernel-cases)
|
||||
//! solidcraft-cli mcp [--headless] [--in design|script] [--connect HOST:PORT [--token T]] [--allow-path DIR]... [--allow-any-path]
|
||||
//! [--param-check off|warn|strict]
|
||||
//! MCP server on stdio (docs/mcp.md)
|
||||
@@ -34,11 +32,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;
|
||||
@@ -61,19 +57,19 @@ struct Command {
|
||||
}
|
||||
|
||||
/// `run`'s lines in the usage.
|
||||
const USAGE_RUN: &str = " solidcraft-cli run <script.json|design.sc|part.step> [--in design.sc|part.step] [--out FILE]... [--save FILE] [--quiet] [--results] [--timings] [--profile] [--profile-out trace.json] [--json-out FILE] [--allow-errors] [--validation each_step|each_feature|off]\n [--param-check off|warn|strict]\n solidcraft-cli run --in <design.sc|part.step> [--out FILE]...\n (a timeline feature that fails fails the run and nothing is written; --allow-errors writes anyway)\n (--validation: when SMLib's full check runs on the features computed (diagnostics.validation), default each_feature)\n (--param-check checks each step against its command's contract: warn reports on stderr, strict stops the run)\n (--timings prints {timings: [{index, command, ms}], total_ms} for a script, with --results its results too;\n --profile adds each command's profile: {boolean_ms, tessellate_ms, other_ms}, the kernel's share)\n (--profile also records where the time goes, SMLib's calls, kernel operations, features, commands and import stages,\n and prints the table to stderr: count, total, self time and longest per span; --profile-out FILE writes the spans\n as a Chrome trace for chrome://tracing or ui.perfetto.dev; SOLIDCRAFT_PROFILE=1 does the same for any command)\n";
|
||||
const USAGE_RUN: &str = " solidcraft-cli run <script.json|design.sc|part.step> [--in design.sc|part.step] [--out FILE]... [--save FILE] [--quiet] [--results] [--timings] [--profile] [--json-out FILE] [--allow-errors]\n solidcraft-cli run --in <design.sc|part.step> [--out FILE]...\n (a timeline feature that fails fails the run and nothing is written; --allow-errors writes anyway)\n (--timings prints {timings: [{index, command, ms}], total_ms} for a script, with --results its results too;\n --profile adds each command's profile: {boolean_ms, tessellate_ms, other_ms}, the kernel's share)\n";
|
||||
/// `eval`'s lines in the usage.
|
||||
const USAGE_EVAL: &str = " solidcraft-cli eval <script.json|design.sc|part.step> [--in design.sc|part.step] [--json-out FILE] [--profile] [--profile-out trace.json] (alias: inspect)\n solidcraft-cli eval --in <script.json|design.sc|part.step> [--json-out FILE]\n (with both inputs, runs the positional script on --in before measuring; does not automatically save the modified design)\n (--json-out FILE writes the JSON that run (with --results, else the measurement) and eval print to FILE instead of stdout,\n as plain UTF-8: a shell's > can re-encode it, e.g. Windows PowerShell adds a byte-order mark)\n (--profile and --profile-out as for run)\n";
|
||||
const USAGE_EVAL: &str = " solidcraft-cli eval <script.json|design.sc|part.step> [--in design.sc|part.step] [--json-out FILE] (alias: inspect)\n solidcraft-cli eval --in <script.json|design.sc|part.step> [--json-out FILE]\n (with both inputs, runs the positional script on --in before measuring; does not automatically save the modified design)\n (--json-out FILE writes the JSON that run (with --results, else the measurement) and eval print to FILE instead of stdout,\n as plain UTF-8: a shell's > can re-encode it, e.g. Windows PowerShell adds a byte-order mark)\n";
|
||||
/// `script`'s lines in the usage.
|
||||
const USAGE_SCRIPT: &str = " solidcraft-cli script <design.sc|script.json> [--out script.json]\n (the design's history as a command script that eval and run replay)\n";
|
||||
/// `bench-open`'s lines in the usage.
|
||||
const USAGE_BENCH_OPEN: &str = " solidcraft-cli bench-open <design.sc | part.step> [--parse-only] [--save OUT.sc] [--display LEVEL [--edit NAME EXPR]] [--ops script.json] [--profile] [--profile-out trace.json]\n (--profile and --profile-out as for run)\n (--ops: after opening, run each command of the script and undo it, timing the command, the\n evaluations it caused and the view it leaves: placing and meshing what changed)\n";
|
||||
const USAGE_BENCH_OPEN: &str = " solidcraft-cli bench-open <design.sc | part.step> [--parse-only] [--save OUT.sc] [--display LEVEL [--edit NAME EXPR]] [--ops script.json]\n (--ops: after opening, run each command of the script and undo it, timing the command, the\n evaluations it caused and the view it leaves: placing and meshing what changed)\n";
|
||||
/// `bench-agent`'s lines in the usage.
|
||||
const USAGE_BENCH_AGENT: &str = " solidcraft-cli bench-agent [--scale F] [--save design.sc] [--script-out script.json]\n";
|
||||
/// `bench-assistant`'s lines in the usage.
|
||||
const USAGE_BENCH_ASSISTANT: &str = " solidcraft-cli bench-assistant --connect 127.0.0.1:PORT [--token T] --base-url URL --model NAME [--tasks 1,2,5] [--repeat N] [--timeout-s 300] [--json out.json] [--markdown out.md]\n";
|
||||
/// `snapshot`'s lines in the usage.
|
||||
const USAGE_SNAPSHOT: &str = " solidcraft-cli snapshot <script|design> [--in design.sc|part.step] --out shot.png [--width W] [--height H] [--view NAME | --camera JSON] [--no-grid] [--no-axes]\n [--body NAME:opacity=0.3,color=#rrggbb]... [--tool NAME]... [--section z=27] [--quality coarse|normal|fine|ultra]\n solidcraft-cli snapshot --in <design.sc|part.step> --out shot.png ...\n (as with run, --in starts from a design and the script runs on top of it; an option snapshot doesn't take is an error;\n --view: iso, front, back, top, bottom, left, right, or the name of a view saved in the design with view.save;\n --camera: {target: [x,y,z], yaw, pitch, distance, fov?} in mm and radians, as the MCP screenshot takes it (docs/mcp.md);\n --no-grid and --no-axes leave the ground grid (on Z 0) and the origin's axes out of the picture;\n --body: how one body is drawn, as appearance.assign takes it: opacity 0-1 (below 1 is see-through, so what is inside\n it shows) and color #rrggbb, either or both, once per body;\n --tool: draw this body solid in the tool colour and every other body see-through, so a cut's or a join's tool body\n is visible inside the material it meets; a later --body overrides it;\n --section: cut the model as inspect.section does, so the inside shows: z=27 (the plane through that world\n coordinate, keeping the smaller side), add ,flip to keep the other side, or XY, XZ, YZ, or its own JSON;\n --quality: the display quality the bodies are meshed at, as the app's setting (default normal))\n";
|
||||
const USAGE_SNAPSHOT: &str = " solidcraft-cli snapshot <script|design> [--in design.sc|part.step] --out shot.png [--width W] [--height H] [--view NAME | --camera JSON] [--no-grid] [--no-axes]\n [--body NAME:opacity=0.3,color=#rrggbb]... [--tool NAME]... [--section z=27]\n solidcraft-cli snapshot --in <design.sc|part.step> --out shot.png ...\n (as with run, --in starts from a design and the script runs on top of it; an option snapshot doesn't take is an error;\n --view: iso, front, back, top, bottom, left, right, or the name of a view saved in the design with view.save;\n --camera: {target: [x,y,z], yaw, pitch, distance, fov?} in mm and radians, as the MCP screenshot takes it (docs/mcp.md);\n --no-grid and --no-axes leave the ground grid (on Z 0) and the origin's axes out of the picture;\n --body: how one body is drawn, as appearance.assign takes it: opacity 0-1 (below 1 is see-through, so what is inside\n it shows) and color #rrggbb, either or both, once per body;\n --tool: draw this body solid in the tool colour and every other body see-through, so a cut's or a join's tool body\n is visible inside the material it meets; a later --body overrides it;\n --section: cut the model as inspect.section does, so the inside shows: z=27 (the plane through that world\n coordinate, keeping the smaller side), add ,flip to keep the other side, or XY, XZ, YZ, or its own JSON)\n";
|
||||
/// `render`'s lines in the usage.
|
||||
const USAGE_RENDER: &str = " solidcraft-cli render <script|design> --out image.png [--setup NAME] [--width W] [--height H] [--transparent] [--overwrite] [--timeout SECONDS]\n [--progressive] [--samples N] [--max-depth N] [--seed N] [--exposure EV] [--lighting FILE.json] [--environment FILE.json] [--tone-map reinhard|aces_fitted]\n";
|
||||
/// `exec`'s lines in the usage.
|
||||
@@ -84,8 +80,6 @@ const USAGE_COMMANDS: &str = " solidcraft-cli commands\n";
|
||||
const USAGE_RECIPE: &str = " solidcraft-cli recipe <recipe.json>\n";
|
||||
/// `oracle`'s lines in the usage.
|
||||
const USAGE_ORACLE: &str = " solidcraft-cli oracle <case-dir>... [--json]\n";
|
||||
/// `kernel-cases`'s lines in the usage.
|
||||
const USAGE_KERNEL_CASES: &str = " solidcraft-cli kernel-cases <case-dir>... [--json] [--dumps=DIR] [--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";
|
||||
@@ -123,7 +117,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 },
|
||||
@@ -139,19 +132,6 @@ const COMMANDS: &[Command] = &[
|
||||
fn main() -> ExitCode {
|
||||
let mut args: Vec<String> = std::env::args().skip(1).collect();
|
||||
logger::init(&mut args);
|
||||
// `--profile` / `--profile-out` (run, eval, bench-open) or SOLIDCRAFT_PROFILE (any command):
|
||||
// record from here, and print the table when the command ends.
|
||||
let profiling = match args.first().map(String::as_str) {
|
||||
Some("run" | "eval" | "inspect" | "bench-open") => profiling::start(&args[1..]),
|
||||
_ => profiling::start(&[]),
|
||||
};
|
||||
let profiling = match profiling {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("error: {e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
if let Err(e) = max_file_size(&mut args) {
|
||||
eprintln!("error: {e}");
|
||||
return ExitCode::FAILURE;
|
||||
@@ -189,14 +169,9 @@ 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") => {
|
||||
let result = cmd_mcp(&args[1..]);
|
||||
solidcraft_mcp::cleanup_scratch_dir();
|
||||
result
|
||||
}
|
||||
Some("mcp") => cmd_mcp(&args[1..]),
|
||||
Some("crashes") => crashes::run(&args[1..]),
|
||||
Some("icon") => cmd_icon(&args[1..]),
|
||||
Some("licences" | "licenses") => {
|
||||
@@ -226,11 +201,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) => {
|
||||
@@ -311,15 +281,8 @@ pub fn load(path: &str) -> Result<Session, String> {
|
||||
/// `load`, with scripts and recipes run at the parameter-check level `check`, which the
|
||||
/// session keeps.
|
||||
fn load_checked(path: &str, check: solidcraft_engine::schema::ParamCheck) -> Result<Session, String> {
|
||||
load_with(path, check, solidcraft_engine::kernel::Validation::default())
|
||||
}
|
||||
|
||||
/// `load_checked`, the features computed under `validation` (`diagnostics.validation`), which
|
||||
/// the session keeps.
|
||||
fn load_with(path: &str, check: solidcraft_engine::schema::ParamCheck, validation: solidcraft_engine::kernel::Validation) -> Result<Session, String> {
|
||||
let mut s = Session::default();
|
||||
s.param_check = check;
|
||||
s.set_validation(validation);
|
||||
if solidcraft_engine::io::is_step_path(path) || solidcraft_engine::io::is_mesh_path(path) || solidcraft_engine::io::is_point_cloud_path(path) {
|
||||
s.execute("doc.open", &json!({"path": path})).map_err(|e| e.to_string())?;
|
||||
return Ok(s);
|
||||
@@ -327,7 +290,7 @@ fn load_with(path: &str, check: solidcraft_engine::schema::ParamCheck, validatio
|
||||
let bytes = solidcraft_engine::io::limits::read(path, solidcraft_engine::io::limits::Kind::Design).map_err(|e| format!("{path}: {e}"))?;
|
||||
if solidcraft_engine::io::is_design_container(&bytes) {
|
||||
let doc = solidcraft_engine::io::read_design(&bytes).map_err(|e| format!("{path}: {e}"))?;
|
||||
let mut session = Session::opened_at_validating(doc, None, Some(path), validation);
|
||||
let mut session = Session::opened_at(doc, None, Some(path));
|
||||
session.param_check = check;
|
||||
return Ok(session);
|
||||
}
|
||||
@@ -336,36 +299,18 @@ fn load_with(path: &str, check: solidcraft_engine::schema::ParamCheck, validatio
|
||||
s.execute("doc.open", &json!({"path": path})).map_err(|e| e.to_string())?;
|
||||
} else {
|
||||
let script = if v.get("features").is_some() && v.get("commands").is_none() { recipe::to_script(&v)? } else { v };
|
||||
run_script_warning(&mut s, path, &script)?;
|
||||
// Warn level: each step's `param_warnings` as it completes, even when a later step fails
|
||||
// (stderr, so stdout stays the caller's).
|
||||
let mut warn = |step: usize, r: &Value| {
|
||||
for w in r.get("param_warnings").and_then(Value::as_array).into_iter().flatten() {
|
||||
eprintln!("{path}: step {}: {}", step + 1, w.as_str().unwrap_or_default());
|
||||
}
|
||||
};
|
||||
s.run_script_each(&script, &mut warn).map_err(|e| e.to_string())?;
|
||||
}
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
/// Run `script`, read from `path`, on `s`. At the warn level each step's `param_warnings` go to
|
||||
/// stderr as the step completes, even when a later step fails (stdout stays the caller's).
|
||||
fn run_script_warning(s: &mut Session, path: &str, script: &Value) -> Result<Vec<Value>, String> {
|
||||
s.run_script_each(script, &mut |step, r| print_param_warnings(path, step, r)).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Step `step`'s `param_warnings` on stderr, one per line, named after the script at `path`.
|
||||
fn print_param_warnings(path: &str, step: usize, r: &Value) {
|
||||
for w in r.get("param_warnings").and_then(Value::as_array).into_iter().flatten() {
|
||||
eprintln!("{path}: step {}: {}", step + 1, w.as_str().unwrap_or_default());
|
||||
}
|
||||
}
|
||||
|
||||
/// `--param-check off|warn|strict`: how calls that break a command's parameter contract are
|
||||
/// treated (docs/mcp.md); off when the option isn't given.
|
||||
fn parse_param_check(cmd: &str, args: &[String]) -> Result<solidcraft_engine::schema::ParamCheck, String> {
|
||||
match flag(args, "--param-check") {
|
||||
None if args.iter().any(|a| a == "--param-check") => Err(format!("{cmd}: --param-check wants off, warn or strict")),
|
||||
None => Ok(solidcraft_engine::schema::ParamCheck::Off),
|
||||
Some(v) => {
|
||||
solidcraft_engine::schema::ParamCheck::parse(v).ok_or_else(|| format!("{cmd}: --param-check wants off, warn or strict, not `{v}`"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn flag<'a>(args: &'a [String], name: &str) -> Option<&'a str> {
|
||||
args.iter().position(|a| a == name).and_then(|i| args.get(i + 1)).map(String::as_str)
|
||||
}
|
||||
@@ -391,16 +336,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") {
|
||||
@@ -432,53 +367,36 @@ 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)?;
|
||||
// Before the option check, so a bare `--param-check` gets the words `mcp` uses.
|
||||
let check = parse_param_check("run", args)?;
|
||||
known_args(
|
||||
"run",
|
||||
args,
|
||||
&["--in", "--out", "--save", "--json-out", "--param-check", "--profile-out", "--validation"],
|
||||
&["--quiet", "--results", "--timings", "--profile", "--allow-errors"],
|
||||
true,
|
||||
)?;
|
||||
let validation = validation_flag("run", args)?;
|
||||
known_args("run", args, &["--in", "--out", "--save", "--json-out"], &["--quiet", "--results", "--timings", "--profile", "--allow-errors"], true)?;
|
||||
let base = flag(args, "--in");
|
||||
let path = positional(args);
|
||||
if path.is_none() && base.is_none() {
|
||||
return Err(usage());
|
||||
}
|
||||
let want_results = args.iter().any(|a| a == "--results");
|
||||
// `--profile` records the profiler's spans (the table goes to stderr, `profiling.rs`); for a
|
||||
// script it also times each command and splits out its kernel time, implying `--timings`.
|
||||
let script = path.is_some_and(|p| is_script(p));
|
||||
let profile = script && args.iter().any(|a| a == "--profile" || a == "--profile-out");
|
||||
// `--profile` times each command and splits out its kernel time; it implies `--timings`.
|
||||
let profile = args.iter().any(|a| a == "--profile");
|
||||
let timings = profile || args.iter().any(|a| a == "--timings");
|
||||
let (mut s, results) = if want_results || timings {
|
||||
// Collect every command's result or time (scripts only), then process requested files too.
|
||||
let mut s = match base {
|
||||
Some(b) => load_with(b, check, validation)?,
|
||||
None => {
|
||||
let mut s = Session::default();
|
||||
s.set_validation(validation);
|
||||
s
|
||||
}
|
||||
Some(b) => load(b)?,
|
||||
None => Session::default(),
|
||||
};
|
||||
s.param_check = check;
|
||||
let needs = if want_results { "--results needs a script" } else { "--timings needs a script" };
|
||||
let script = path.ok_or(needs)?;
|
||||
let v = read_json(script)?;
|
||||
let v = read_json(path.ok_or(needs)?)?;
|
||||
let out = if timings {
|
||||
let (r, mut t) = s.run_script_timed(&v, profile, &mut |step, r| print_param_warnings(script, step, r)).map_err(|e| e.to_string())?;
|
||||
let (r, mut t) = s.run_script_timed(&v, profile).map_err(|e| e.to_string())?;
|
||||
if want_results && let Some(o) = t.as_object_mut() {
|
||||
o.insert("results".into(), Value::Array(r));
|
||||
}
|
||||
t
|
||||
} else {
|
||||
Value::Array(run_script_warning(&mut s, script, &v)?)
|
||||
Value::Array(s.run_script(&v).map_err(|e| e.to_string())?)
|
||||
};
|
||||
(s, Some(out))
|
||||
} else {
|
||||
(open_validating(base, path, check, 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
|
||||
@@ -503,23 +421,12 @@ fn cmd_run(args: &[String]) -> Result<(), String> {
|
||||
}
|
||||
|
||||
/// The session `run`, `eval` and `snapshot` start from: `--in` (a design or STEP file) with the
|
||||
/// positional script run on top of it, or either one alone, with scripts run at `check`.
|
||||
fn open(base: Option<&str>, path: Option<&String>, check: solidcraft_engine::schema::ParamCheck) -> Result<Session, String> {
|
||||
open_validating(base, path, check, solidcraft_engine::kernel::Validation::default())
|
||||
}
|
||||
|
||||
/// [`open`], the features computed under `validation`.
|
||||
fn open_validating(
|
||||
base: Option<&str>,
|
||||
path: Option<&String>,
|
||||
check: solidcraft_engine::schema::ParamCheck,
|
||||
validation: solidcraft_engine::kernel::Validation,
|
||||
) -> Result<Session, String> {
|
||||
let load = |p: &str| load_with(p, check, validation);
|
||||
/// positional script run on top of it, or either one alone.
|
||||
fn open(base: Option<&str>, path: Option<&String>) -> Result<Session, String> {
|
||||
match (base, path) {
|
||||
(Some(b), Some(p)) => {
|
||||
let mut s = load(b)?;
|
||||
run_script_warning(&mut s, p, &read_json(p)?)?;
|
||||
s.run_script(&read_json(p)?).map_err(|e| e.to_string())?;
|
||||
Ok(s)
|
||||
}
|
||||
(Some(b), None) => load(b),
|
||||
@@ -528,15 +435,6 @@ fn open_validating(
|
||||
}
|
||||
}
|
||||
|
||||
/// `--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.
|
||||
@@ -574,8 +472,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), solidcraft_engine::schema::ParamCheck::Off)?;
|
||||
known_args("eval", args, &["--in", "--json-out"], &[], true)?;
|
||||
let mut s = open(flag(args, "--in"), positional(args))?;
|
||||
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!({
|
||||
@@ -656,7 +554,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,
|
||||
)?;
|
||||
@@ -669,17 +567,7 @@ fn cmd_snapshot(args: &[String]) -> Result<(), String> {
|
||||
Some(x) => x.parse().ok().filter(|n| (16..=8192).contains(n)).ok_or_else(|| format!("{name} must be 16 to 8192 pixels, not `{x}`")),
|
||||
};
|
||||
let (w, h) = (size("--width", 1280)?, size("--height", 800)?);
|
||||
// The display quality the bodies are meshed at, as the app's Display Quality setting: set
|
||||
// before the design opens, so every display plan is made at it.
|
||||
if let Some(q) = flag(args, "--quality") {
|
||||
use solidcraft_engine::doc::display_quality::{DisplayQuality, set_current};
|
||||
let level = DisplayQuality::parse(q).ok_or_else(|| {
|
||||
let ids: Vec<&str> = DisplayQuality::ALL.iter().map(|q| q.id()).collect();
|
||||
format!("--quality must be one of {}, not `{q}`", ids.join(", "))
|
||||
})?;
|
||||
set_current(level);
|
||||
}
|
||||
let mut s = open(flag(args, "--in"), positional(args), solidcraft_engine::schema::ParamCheck::Off)?;
|
||||
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`).
|
||||
preview::apply(&mut s, &flags(args, "--body"), &flags(args, "--tool"), flag(args, "--section"))?;
|
||||
@@ -698,12 +586,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});
|
||||
@@ -806,8 +691,17 @@ fn cmd_mcp(args: &[String]) -> Result<(), String> {
|
||||
eprintln!("solidcraft mcp: the design's folder is left out because {why}; add it with --allow-path if you mean it");
|
||||
}
|
||||
}
|
||||
// How calls that break a command's parameter contract are treated, headless or bridged.
|
||||
let param_check = parse_param_check("mcp", args)?;
|
||||
// How a headless session treats calls that break a command's parameter contract (docs/mcp.md).
|
||||
let param_check = match flag(args, "--param-check") {
|
||||
None if args.iter().any(|a| a == "--param-check") => return Err("mcp: --param-check wants off, warn or strict".into()),
|
||||
None => solidcraft_engine::schema::ParamCheck::Off,
|
||||
Some(v) => {
|
||||
solidcraft_engine::schema::ParamCheck::parse(v).ok_or_else(|| format!("mcp: --param-check wants off, warn or strict, not `{v}`"))?
|
||||
}
|
||||
};
|
||||
if param_check != solidcraft_engine::schema::ParamCheck::Off && flag(args, "--connect").is_some() {
|
||||
eprintln!("solidcraft mcp: --param-check applies to headless sessions; calls to the app are not checked yet");
|
||||
}
|
||||
let backend: Box<dyn solidcraft_mcp::Backend> = match (flag(args, "--connect"), flag(args, "--in")) {
|
||||
(Some(_), Some(_)) => return Err("mcp: use either --connect or --in, not both".into()),
|
||||
(Some(addr), None) => {
|
||||
@@ -815,7 +709,7 @@ fn cmd_mcp(args: &[String]) -> Result<(), String> {
|
||||
return Err(format!("mcp: --connect wants HOST:PORT, such as 127.0.0.1:7878, not `{addr}`"));
|
||||
};
|
||||
// Serve without the app too: the tools connect once it runs, and say it isn't until then.
|
||||
let mut remote = solidcraft_mcp::Remote::new(addr, flag(args, "--token")).with_param_check(param_check);
|
||||
let mut remote = solidcraft_mcp::Remote::new(addr, flag(args, "--token"));
|
||||
if let Err(e) = remote.try_connect() {
|
||||
eprintln!(
|
||||
"solidcraft mcp: no SolidCraft app at {addr} yet ({e}); start it with `solidcraft --control {port}` \
|
||||
@@ -835,8 +729,7 @@ fn cmd_mcp(args: &[String]) -> Result<(), String> {
|
||||
let auto = !args.iter().any(|a| a == "--headless");
|
||||
if auto && let Some(port) = discover_app() {
|
||||
eprintln!("solidcraft mcp: bridged to the running app at 127.0.0.1:{port} (--headless starts a session of its own)");
|
||||
let remote = solidcraft_mcp::Remote::new(&format!("127.0.0.1:{port}"), flag(args, "--token")).with_param_check(param_check);
|
||||
return serve_mcp(Box::new(remote), scope);
|
||||
return serve_mcp(Box::new(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;
|
||||
@@ -932,7 +825,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)
|
||||
@@ -988,7 +881,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);
|
||||
@@ -1058,7 +951,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, the kernel's calls to the
|
||||
//! commands, print the table to stderr when the command ends (stdout stays the command's JSON)
|
||||
//! and write the spans as a Chrome trace for chrome://tracing or Perfetto.
|
||||
|
||||
use solidcraft_engine::profile;
|
||||
|
||||
/// Rows the table prints.
|
||||
const TOP: usize = 30;
|
||||
|
||||
/// A recording that prints its table when finished.
|
||||
pub struct Profiling {
|
||||
trace: Option<String>,
|
||||
}
|
||||
|
||||
/// Start recording when `args` (after the command) ask for it, or `SOLIDCRAFT_PROFILE` does.
|
||||
pub fn start(args: &[String]) -> Result<Option<Profiling>, String> {
|
||||
let trace = match args.iter().position(|a| a == "--profile-out") {
|
||||
Some(i) => match args.get(i + 1) {
|
||||
Some(f) if !f.starts_with("--") => Some(f.clone()),
|
||||
_ => return Err("--profile-out needs a FILE".into()),
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
let asked = trace.is_some() || args.iter().any(|a| a == "--profile");
|
||||
if !asked && !solidcraft_engine::timing::profile_from_env() {
|
||||
return Ok(None);
|
||||
}
|
||||
if !profile::COMPILED {
|
||||
return Err("this solidcraft-cli was built without the profiler (its `profile` feature)".into());
|
||||
}
|
||||
profile::reset();
|
||||
profile::set_enabled(true);
|
||||
Ok(Some(Profiling { trace }))
|
||||
}
|
||||
|
||||
impl Profiling {
|
||||
/// Stop recording, print the table on stderr and write the trace.
|
||||
pub fn finish(self) -> Result<(), String> {
|
||||
profile::set_enabled(false);
|
||||
eprint!("{}", profile::report().table(TOP));
|
||||
if let Some(path) = self.trace {
|
||||
let mut bytes = Vec::new();
|
||||
profile::write_chrome_trace(&mut bytes).map_err(|e| format!("{path}: {e}"))?;
|
||||
std::fs::write(&path, bytes).map_err(|e| format!("{path}: {e}"))?;
|
||||
eprintln!("profile: trace written to {path} (open it in chrome://tracing or ui.perfetto.dev)");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
@@ -1338,9 +1338,7 @@ fn a_fillet_or_chamfer_on_one_edge_ending_at_a_cylinder() {
|
||||
fn a_round_on_a_knuckle_cap_circle() {
|
||||
for (cmd, size, removed) in [("solid.fillet", "radius", (2.4, 2.8)), ("solid.chamfer", "distance", (5.6, 6.3))] {
|
||||
let mut s = load(&root().join("tests/oracle/63-hinge-knuckle/recipe.json")).unwrap();
|
||||
let volume = |s: &Session| {
|
||||
s.model.state().bodies.iter().map(|b| b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.001)).unwrap().measure().volume).sum::<f64>()
|
||||
};
|
||||
let volume = |s: &Session| s.model.state().bodies.iter().map(|b| b.body.tessellate(0.001).unwrap().measure().volume).sum::<f64>();
|
||||
let before = volume(&s);
|
||||
s.execute(cmd, &json!({"edges": [[40.0, 29.837_722, 6.449_490]], size: 0.728})).unwrap_or_else(|e| panic!("{cmd}: {e}"));
|
||||
check(&s, 0, &BTreeMap::new()).unwrap_or_else(|(kind, e)| panic!("{cmd}: {kind}: {e}"));
|
||||
@@ -1360,9 +1358,7 @@ fn a_round_on_a_knuckle_cap_circle() {
|
||||
#[test]
|
||||
fn a_hair_thin_hole_on_a_ledge_beside_a_loft_cut() {
|
||||
let mut s = load(&root().join("examples/regressions/loft_cut_beside_round_hole.json")).unwrap();
|
||||
let volume = |s: &Session| {
|
||||
s.model.state().bodies.iter().map(|b| b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.005)).unwrap().measure().volume).sum::<f64>()
|
||||
};
|
||||
let volume = |s: &Session| s.model.state().bodies.iter().map(|b| b.body.tessellate_with(0.005, true).unwrap().measure().volume).sum::<f64>();
|
||||
let before = volume(&s);
|
||||
s.execute("solid.hole", &json!({"diameter": 0.133, "position": [357.22, -150.993_333, 730.0]})).unwrap();
|
||||
check(&s, 0, &BTreeMap::new()).unwrap_or_else(|(kind, e)| panic!("{kind}: {e}"));
|
||||
@@ -1379,9 +1375,7 @@ fn a_hair_thin_hole_on_a_ledge_beside_a_loft_cut() {
|
||||
#[test]
|
||||
fn a_small_hole_through_the_wall_of_a_plastic_rest() {
|
||||
let mut s = load(&root().join("apps/solidcraft-cli/tests/corpus/modern-plastic.solidcraft")).unwrap();
|
||||
let volume = |s: &Session| {
|
||||
s.model.state().bodies.iter().map(|b| b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.002)).unwrap().measure().volume).sum::<f64>()
|
||||
};
|
||||
let volume = |s: &Session| s.model.state().bodies.iter().map(|b| b.body.tessellate_with(0.002, true).unwrap().measure().volume).sum::<f64>();
|
||||
let before = volume(&s);
|
||||
s.execute("solid.hole", &json!({"diameter": 0.8, "position": [63.069_841_5, 28.333_333, 4.0]})).unwrap();
|
||||
check(&s, 0, &BTreeMap::new()).unwrap_or_else(|(kind, e)| panic!("{kind}: {e}"));
|
||||
@@ -1396,9 +1390,7 @@ fn a_small_hole_through_the_wall_of_a_plastic_rest() {
|
||||
#[test]
|
||||
fn a_hole_square_to_a_45_degree_flange() {
|
||||
let mut s = load(&root().join("tests/oracle/70-sm-flange-45/recipe.json")).unwrap();
|
||||
let volume = |s: &Session| {
|
||||
s.model.state().bodies.iter().map(|b| b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.002)).unwrap().measure().volume).sum::<f64>()
|
||||
};
|
||||
let volume = |s: &Session| s.model.state().bodies.iter().map(|b| b.body.tessellate_with(0.002, true).unwrap().measure().volume).sum::<f64>();
|
||||
let before = volume(&s);
|
||||
s.execute("solid.hole", &json!({"diameter": 3.448, "position": [66.666_667, -4.511_845, 4.511_845]})).unwrap();
|
||||
check(&s, 0, &BTreeMap::new()).unwrap_or_else(|(kind, e)| panic!("{kind}: {e}"));
|
||||
@@ -1736,7 +1728,7 @@ fn the_ribbed_lid_builds_the_same_body_every_time() {
|
||||
st.bodies
|
||||
.iter()
|
||||
.map(|b| {
|
||||
let mesh = b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.1)).unwrap();
|
||||
let mesh = b.body.tessellate(0.1).unwrap();
|
||||
let bits = mesh.positions.iter().fold(0u64, |acc, p| {
|
||||
acc.rotate_left(7).wrapping_add(p.x.to_bits() ^ p.y.to_bits().rotate_left(21) ^ p.z.to_bits().rotate_left(42))
|
||||
});
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -263,110 +263,3 @@ mod setup {
|
||||
assert!(json_of(&pkg.join("claude_desktop_config.json"))["mcpServers"]["solidcraft"].is_object());
|
||||
}
|
||||
}
|
||||
|
||||
/// Scratch files belong to the CLI process; exercise real startup, requests and shutdown.
|
||||
mod scratch_cleanup {
|
||||
use super::*;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
fn command(case: &str) -> (PathBuf, Command) {
|
||||
let dir = std::env::temp_dir().join(format!("solidcraft-mcp-lifecycle-{case}-{}", std::process::id()));
|
||||
std::fs::create_dir(&dir).unwrap();
|
||||
let mut command = Command::new(env!("CARGO_BIN_EXE_solidcraft-cli"));
|
||||
command
|
||||
.args(["mcp", "--headless"])
|
||||
.env("TMPDIR", &dir)
|
||||
.env("TMP", &dir)
|
||||
.env("TEMP", &dir)
|
||||
.env("SOLIDCRAFT_RECOVERY_DIR", dir.join("recovery"))
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
(dir, command)
|
||||
}
|
||||
|
||||
fn scratch_folders(dir: &Path) -> Vec<PathBuf> {
|
||||
std::fs::read_dir(dir)
|
||||
.unwrap()
|
||||
.map(|entry| entry.unwrap().path())
|
||||
.filter(|path| path.file_name().unwrap().to_string_lossy().starts_with("solidcraft-mcp-"))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn initialize() -> Value {
|
||||
json!({"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2025-06-18"}})
|
||||
}
|
||||
|
||||
fn run(mut command: Command, request: Value) -> std::process::Output {
|
||||
let mut child = command.spawn().unwrap();
|
||||
writeln!(child.stdin.take().unwrap(), "{request}").unwrap();
|
||||
child.wait_with_output().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eof_removes_only_the_process_scratch_folder() {
|
||||
let (dir, command) = command("eof");
|
||||
let unrelated = dir.join("solidcraft-mcp-unrelated");
|
||||
std::fs::create_dir(&unrelated).unwrap();
|
||||
let out = run(command, initialize());
|
||||
assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr));
|
||||
let reply: Value = serde_json::from_slice(&out.stdout).unwrap();
|
||||
assert!(reply.get("result").is_some(), "{reply}");
|
||||
assert_eq!(scratch_folders(&dir), vec![unrelated]);
|
||||
assert!(dir.join("recovery").is_dir(), "cleanup must leave the recovery folder alone");
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_error_removes_the_process_scratch_folder() {
|
||||
let (dir, mut command) = command("startup-error");
|
||||
let out = command.args(["--param-check", "invalid"]).stdin(Stdio::null()).output().unwrap();
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(!out.status.success());
|
||||
assert!(stderr.contains("--param-check wants off, warn or strict"), "{stderr}");
|
||||
assert!(scratch_folders(&dir).is_empty(), "startup error left a scratch folder in {dir:?}");
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shutdown_preserves_nonempty_scratch_folder() {
|
||||
let (dir, mut command) = command("nonempty");
|
||||
let mut child = command.spawn().unwrap();
|
||||
let mut input = child.stdin.take().unwrap();
|
||||
writeln!(input, "{}", initialize()).unwrap();
|
||||
let mut reply = String::new();
|
||||
BufReader::new(child.stdout.take().unwrap()).read_line(&mut reply).unwrap();
|
||||
let reply: Value = serde_json::from_str(&reply).unwrap();
|
||||
assert!(reply.get("result").is_some(), "{reply}");
|
||||
let scratch = scratch_folders(&dir);
|
||||
assert_eq!(scratch.len(), 1);
|
||||
let saved = scratch[0].join("keep.txt");
|
||||
std::fs::write(&saved, "saved output").unwrap();
|
||||
drop(input);
|
||||
let out = child.wait_with_output().unwrap();
|
||||
assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr));
|
||||
assert_eq!(std::fs::read_to_string(saved).unwrap(), "saved output");
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_screenshot_scratch_folder_is_removed_without_a_path_scope() {
|
||||
let (dir, mut command) = command("screenshot");
|
||||
command.arg("--allow-any-path");
|
||||
// A headless server cannot capture a window. The request still allocates a scratch
|
||||
// path before the backend refuses it, without relying on a GPU or installed renderer.
|
||||
let out = run(
|
||||
command,
|
||||
json!({
|
||||
"jsonrpc": "2.0", "id": 1, "method": "tools/call",
|
||||
"params": {"name": "screenshot", "arguments": {"source": "window"}},
|
||||
}),
|
||||
);
|
||||
assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr));
|
||||
let reply: Value = serde_json::from_slice(&out.stdout).unwrap();
|
||||
assert_eq!(reply["result"]["isError"], true, "{reply}");
|
||||
assert!(reply["result"]["content"][0]["text"].as_str().unwrap().contains("the window gave no picture"), "{reply}");
|
||||
assert!(scratch_folders(&dir).is_empty(), "failed screenshot left a scratch folder in {dir:?}");
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,101 +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()));
|
||||
}
|
||||
|
||||
/// A design saved before body names were pinned (#6403) pins them as it opens, as its first
|
||||
/// command would, and opening it is no change. That command pinned them as part of itself, so
|
||||
/// undoing it went back to the unpinned design, which no evaluated result matched: the first
|
||||
/// undo after opening from the command line evaluated every feature again (82-93 s in a
|
||||
/// 5,484-feature design); `doc.open` pinned them after opening, which marked the design changed.
|
||||
#[test]
|
||||
fn a_design_saved_before_names_were_pinned_opens_pinned_and_unchanged() {
|
||||
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 mut doc = built.design().into_owned();
|
||||
for f in &mut doc.features {
|
||||
f.body_names.clear();
|
||||
}
|
||||
doc.reserved_body_names.clear();
|
||||
let unpinned = Session::new(doc);
|
||||
let design = unpinned.design();
|
||||
let saved = unpinned.model.saved(&design).unwrap();
|
||||
let bytes = solidcraft_engine::io::write_design_container_with_cache(&design, solidcraft_engine::io::DesignCompression::Fast, None, Some(&saved))
|
||||
.unwrap();
|
||||
let path = temp("unpinned");
|
||||
std::fs::write(&path, &bytes).unwrap();
|
||||
let file = path.to_string_lossy().to_string();
|
||||
// As the command line opens a design (`load`), and as the app does (`doc.open`).
|
||||
let (doc, saved) = solidcraft_engine::io::read_design_file(&file, None::<fn(solidcraft_engine::io::DesignPreview)>).unwrap();
|
||||
for (how, mut s) in [("opened_at", Session::opened_at(doc, saved.as_ref(), Some(&file))), ("doc.open", open(&path))] {
|
||||
assert_eq!(s.model.last_recomputed, 0, "{how}: taken from the file");
|
||||
assert!(s.doc.features.iter().any(|f| !f.body_names.is_empty()), "{how}: the names are pinned");
|
||||
assert!(!s.is_dirty(), "{how}: opening is no change");
|
||||
let read: Vec<Arc<ModelState>> = s.model.results.iter().map(|r| r.state.clone()).collect();
|
||||
s.execute("solid.box", &json!({"length": 3, "width": 3, "height": 3, "operation": "new"})).unwrap();
|
||||
// As in a large design, whose evaluation cache keeps only a few results: undo finds the
|
||||
// results in the model itself or evaluates them again.
|
||||
*s.model.cache.lock().unwrap() = Default::default();
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(s.model.last_recomputed, 0, "{how}: the first undo evaluates nothing");
|
||||
assert!(s.model.results.iter().zip(&read).all(|(r, st)| Arc::ptr_eq(&r.state, st)), "{how}: the states read from the file");
|
||||
assert!(!s.is_dirty(), "{how}: undone to the design as opened");
|
||||
}
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
@@ -142,14 +142,16 @@ fn a_parameter_edit_rebuilds_like_a_fresh_build_and_reverts_exactly() {
|
||||
assert!(problems.is_empty(), "{}", problems.join("\n"));
|
||||
}
|
||||
|
||||
/// The README demo's Raspberry Pi case (`examples/demo/enclosure_ui_mcp.py --story pi`): after
|
||||
/// each feature, each outer side (the four walls and the bottom) is one face and the case is as
|
||||
/// tall as its height, and a height changed in Parameters rebuilds the same topology as one
|
||||
/// built at that height: (a) built at height 24, (b) built at 24 and changed to 32, (c) built at
|
||||
/// 32. The port cut-outs and the floor vents used to split the long wall (or the end wall at 24)
|
||||
/// into strips along the openings' tops and bottoms.
|
||||
/// The README demo's Raspberry Pi case (`examples/demo/enclosure_ui_mcp.py --story pi`): its
|
||||
/// face count after each feature, and each outer side (the four walls and the bottom) one face
|
||||
/// throughout: (a) built at height 24, (b) built at 24 and changed to 32 in Parameters, (c)
|
||||
/// built at 32. The port cut-outs and the floor vents used to split the long wall (or the end
|
||||
/// wall at 24) into strips along the openings' tops and bottoms.
|
||||
#[test]
|
||||
fn pi_case_sides_stay_whole_at_both_heights() {
|
||||
fn pi_case_faces_per_step_at_both_heights() {
|
||||
// Sketch1 (1) .. RectangularPattern2 (14): the faces once features 1..=k are built.
|
||||
const FACES: [(usize, usize); 13] =
|
||||
[(2, 6), (3, 10), (4, 19), (5, 19), (6, 25), (7, 43), (8, 43), (9, 55), (10, 55), (11, 59), (12, 59), (13, 63), (14, 79)];
|
||||
let dir = scratch_dir("pi");
|
||||
let design: Value = serde_json::from_slice(&std::fs::read(root().join("tests/action_matrix/designs/pi-case.sc")).unwrap()).unwrap();
|
||||
let at = |height: &str, file: &str| {
|
||||
@@ -172,13 +174,12 @@ fn pi_case_sides_stay_whole_at_both_heights() {
|
||||
for (case, s, h) in [("(a) at 24", a, 24.), ("(b) 24 changed to 32", b, 32.), ("(c) at 32", c, 32.)] {
|
||||
assert!(errors(&s).is_empty(), "{case}: features fail: {:?}", errors(&s));
|
||||
let mut steps = Vec::new();
|
||||
// Sketch1 (1) is no body; the box (2) .. RectangularPattern2 (14).
|
||||
for k in 2..=14 {
|
||||
for (k, want) in FACES {
|
||||
let mut t = s.scratch();
|
||||
t.execute("timeline.rollback", &json!({"position": k})).unwrap();
|
||||
let st = t.world_state().unwrap();
|
||||
let st = t.world_state();
|
||||
let body = st.bodies.iter().find(|b| b.name == "Body1").unwrap();
|
||||
assert!(body.body.validity().is_empty(), "{case}: after feature {k}: {:?}", body.body.validity());
|
||||
assert_eq!(body.body.face_count(), want, "{case}: faces after feature {k}");
|
||||
// Each outer side: the faces in its plane.
|
||||
for (n, d) in [([0., -1., 0.], 33.), ([0., 1., 0.], 33.), ([-1., 0., 0.], 47.5), ([1., 0., 0.], 47.5), ([0., 0., -1.], 0.)] {
|
||||
let on = (0..body.body.face_count())
|
||||
|
||||
@@ -229,9 +229,7 @@ fn known_pending_concepts_and_released_files_round_trip_with_inert_metadata() {
|
||||
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);
|
||||
// #4907: `run --save` maps to Save As, and a borrowed design name
|
||||
// follows the destination file's stem instead of keeping the old name.
|
||||
assert_eq!(reopened.name, format!("resaved-{index}"));
|
||||
assert_eq!(reopened.name, document.name);
|
||||
assert_eq!(reopened.features, document.features);
|
||||
assert_eq!(std::fs::read(&source).unwrap(), bytes);
|
||||
}
|
||||
@@ -498,78 +496,6 @@ fn timings_and_profile_report_each_command() {
|
||||
assert!(String::from_utf8_lossy(&output.stderr).contains("--timings needs a script"));
|
||||
}
|
||||
|
||||
/// #6921: `--param-check` checks a script's steps against their contracts on every path `run`
|
||||
/// takes (a script alone, on an `--in` design or script, with `--results`, `--timings` or
|
||||
/// `--profile`), and a warning reaches stderr even when a later step fails.
|
||||
#[test]
|
||||
fn param_check_reaches_every_script_run_takes() {
|
||||
let dir = Scratch::new();
|
||||
let typo = dir.0.join("typo.json");
|
||||
let step = json!({"command": "solid.box", "params": {"length": 2, "width": 3, "height": 4, "operaton": "new"}});
|
||||
std::fs::write(&typo, json!({"commands": [step.clone()]}).to_string()).unwrap();
|
||||
let base = dir.0.join("base.solidcraft");
|
||||
json_output(cli().arg("run").arg(dir.script()).arg("--save").arg(&base).output().unwrap());
|
||||
let saved = dir.0.join("saved.solidcraft");
|
||||
let start = dir.script();
|
||||
let (typo, base, saved_arg, start) = (typo.to_str().unwrap(), base.to_str().unwrap(), saved.to_str().unwrap(), start.to_str().unwrap());
|
||||
let paths = [&[][..], &["--in", base], &["--in", start], &["--results"], &["--timings"], &["--timings", "--profile"]];
|
||||
let run = |level: Option<&str>, extra: &[&str]| {
|
||||
std::fs::remove_file(&saved).ok();
|
||||
let check = level.map(|l| ["--param-check", l]).into_iter().flatten();
|
||||
cli().args(["run", typo, "--save", saved_arg]).args(check).args(extra).output().unwrap()
|
||||
};
|
||||
|
||||
// Off, the default: the step runs as it always has, without a word.
|
||||
let off = run(None, &[]);
|
||||
assert!(off.status.success() && !String::from_utf8_lossy(&off.stderr).contains("operaton"), "{off:?}");
|
||||
assert_eq!(body_count(&saved), 1);
|
||||
|
||||
// Warn: each path runs the step, saves, and reports it on stderr; `--results` keeps it too.
|
||||
for extra in paths {
|
||||
let output = run(Some("warn"), extra);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(output.status.success(), "{extra:?}: {stderr}");
|
||||
assert!(stderr.contains("step 1: unknown key `operaton`; did you mean `operation`?"), "{extra:?}: {stderr}");
|
||||
assert!(saved.exists(), "{extra:?}");
|
||||
if extra == ["--results"] {
|
||||
let results: Value = serde_json::from_slice(&output.stdout).unwrap();
|
||||
assert!(results[0]["param_warnings"][0].as_str().is_some_and(|w| w.contains("operaton")), "{results}");
|
||||
}
|
||||
}
|
||||
|
||||
// Strict: on each path the run stops at the step, and nothing is written.
|
||||
for extra in paths {
|
||||
let output = run(Some("strict"), extra);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert_eq!(output.status.code(), Some(1), "{extra:?}: {stderr}");
|
||||
assert!(stderr.contains("unknown key `operaton`"), "{extra:?}: {stderr}");
|
||||
assert!(!saved.exists(), "{extra:?}");
|
||||
}
|
||||
|
||||
// Warn, with a later step that fails: the run fails, and the first step's warning is still
|
||||
// reported, timed or not.
|
||||
let failing = dir.0.join("failing.json");
|
||||
let fail = json!({"command": "solid.extrude", "params": {"sketch": "Nope"}});
|
||||
std::fs::write(&failing, json!({"commands": [step, fail]}).to_string()).unwrap();
|
||||
for extra in [&[][..], &["--timings"]] {
|
||||
let output = cli().arg("run").arg(&failing).args(["--param-check", "warn"]).args(extra).output().unwrap();
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert_eq!(output.status.code(), Some(1), "{extra:?}: {stderr}");
|
||||
assert!(stderr.contains("step 1: unknown key `operaton`") && stderr.contains("no sketch `Nope`"), "{extra:?}: {stderr}");
|
||||
}
|
||||
|
||||
// A level it doesn't know and a missing value fail.
|
||||
for (bad, says) in [
|
||||
(&["--param-check", "loud"][..], "--param-check wants off, warn or strict, not `loud`"),
|
||||
(&["--param-check"], "run: --param-check wants off, warn or strict"),
|
||||
] {
|
||||
let output = cli().args(["run", typo]).args(bad).output().unwrap();
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert_eq!(output.status.code(), Some(1), "{bad:?}: {stderr}");
|
||||
assert!(stderr.contains(says), "{bad:?}: {stderr}");
|
||||
}
|
||||
}
|
||||
|
||||
fn render_setup_script(dir: &Scratch, job: Value) -> PathBuf {
|
||||
let path = dir.0.join("render.json");
|
||||
std::fs::write(
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -123,42 +123,3 @@ fn a_design_naming_a_sketch_and_its_extrude_alike_replays_from_its_script() {
|
||||
assert_eq!(names, ["Block", "Block"], "{first:#}");
|
||||
assert_eq!(second["commands"], first["commands"], "the replayed design's script has the same steps");
|
||||
}
|
||||
|
||||
/// #10230: script export must disclose the print-face state that replay leaves out.
|
||||
#[test]
|
||||
fn script_export_warns_about_omitted_print_faces_on_stderr() {
|
||||
let dir = std::env::temp_dir().join(format!("solidcraft-print-face-script-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let saved = dir.join("block.sc");
|
||||
let exported = dir.join("block.json");
|
||||
let mut s = Session::default();
|
||||
s.execute("solid.box", &json!({"length": 20, "width": 10, "height": 5, "body_name": "Block"})).unwrap();
|
||||
s.execute("body.print_face", &json!({"body": "Block", "point": [10, 5, 0]})).unwrap();
|
||||
s.execute("file.save", &json!({"path": saved})).unwrap();
|
||||
let native = std::fs::read(&saved).unwrap();
|
||||
|
||||
for to_file in [false, true] {
|
||||
let mut cmd = std::process::Command::new(env!("CARGO_BIN_EXE_solidcraft-cli"));
|
||||
cmd.arg("script").arg(&saved);
|
||||
if to_file {
|
||||
cmd.arg("--out").arg(&exported);
|
||||
}
|
||||
let out = cmd.output().unwrap();
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(out.status.success(), "{stderr}");
|
||||
assert!(stderr.contains("note: the script leaves out print-face assignments (body.print_face)"), "{stderr}");
|
||||
let script = if to_file {
|
||||
assert!(out.stdout.is_empty(), "--out must keep stdout empty");
|
||||
std::fs::read(&exported).unwrap()
|
||||
} else {
|
||||
out.stdout
|
||||
};
|
||||
let script: Value = serde_json::from_slice(&script).unwrap();
|
||||
let mut replayed = Session::default();
|
||||
replayed.run_script(&script).unwrap();
|
||||
assert!(replayed.doc.print_faces.is_empty());
|
||||
assert_eq!(replayed.doc.features, s.doc.features);
|
||||
assert_eq!(std::fs::read(&saved).unwrap(), native, "export must not change the source design");
|
||||
}
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -14,11 +14,8 @@ name = "solidcraft-web"
|
||||
path = "src/main.rs"
|
||||
|
||||
[features]
|
||||
default = ["profile"]
|
||||
default = []
|
||||
bundled-render-library = ["solidcraft-engine/bundled-render-library"]
|
||||
# The profiler, compiled in so any build can say where its time goes (the Profile operations
|
||||
# preference, SOLIDCRAFT_PROFILE=1, diagnostics.profile); it records nothing until asked.
|
||||
profile = ["solidcraft-engine/profile"]
|
||||
|
||||
[dependencies]
|
||||
solidcraft-engine = { workspace = true }
|
||||
@@ -37,7 +34,7 @@ serde_json = { workspace = true }
|
||||
web-sys = { version = "0.3", features = [
|
||||
"Window", "Document", "Element", "HtmlElement", "HtmlCanvasElement", "Location", "Navigator", "StorageManager",
|
||||
"FileSystemHandle", "FileSystemDirectoryHandle", "FileSystemFileHandle", "FileSystemGetFileOptions", "FileSystemWritableFileStream",
|
||||
"WritableStream", "File", "FileList", "Blob", "BlobPropertyBag", "Url", "HtmlAnchorElement", "HtmlInputElement", "Storage", "StorageEvent",
|
||||
"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",
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
//! Links the browser build with an 8 MiB stack: solidcore's kernel algorithms (subdivision,
|
||||
//! intersection, filleting) recurse legitimately, and wasm has no guard page to catch an
|
||||
//! overflow (solidcore's README recommends 8 MiB).
|
||||
|
||||
fn main() {
|
||||
if std::env::var("CARGO_CFG_TARGET_ARCH").is_ok_and(|a| a == "wasm32") {
|
||||
println!("cargo:rustc-link-arg=-zstack-size=8388608");
|
||||
}
|
||||
}
|
||||
@@ -22,10 +22,6 @@
|
||||
<link data-trunk rel="rust" data-bin="solidcraft-web" data-wasm-opt="z" data-weak-refs />
|
||||
<link data-trunk rel="copy-file" href="../../assets/fonts/biz-udp-gothic/BIZUDPGothic-Regular.ttf" data-target-path="fonts" />
|
||||
<link data-trunk rel="copy-file" href="../../assets/fonts/biz-udp-gothic/OFL.txt" data-target-path="fonts" />
|
||||
<link data-trunk rel="copy-file" href="../../assets/fonts/craft-ui-sc/CraftUI-SC.otf" data-target-path="fonts" />
|
||||
<link data-trunk rel="copy-file" href="../../assets/fonts/craft-ui-sc/OFL.txt" data-target-path="fonts/craft-ui-sc" />
|
||||
<link data-trunk rel="copy-file" href="../../assets/fonts/craft-ui-tc/CraftUI-TC.otf" data-target-path="fonts" />
|
||||
<link data-trunk rel="copy-file" href="../../assets/fonts/craft-ui-tc/OFL.txt" data-target-path="fonts/craft-ui-tc" />
|
||||
<link data-trunk rel="css" href="../../assets/styles/web/loading.css" />
|
||||
<link data-trunk rel="copy-file" href="../../assets/styles/web/loading-logo.svg" />
|
||||
<!-- Installable as an app (manifest.webmanifest); the icons are the Linux exports of the app mark. -->
|
||||
|
||||
@@ -15,9 +15,6 @@ type Reply = Result<(String, Vec<u8>), String>;
|
||||
pub struct Download {
|
||||
reply: Rc<RefCell<Option<Reply>>>,
|
||||
pending: Option<(String, Vec<u8>)>,
|
||||
/// The downloaded design being opened (a few features a frame, #10198): the answer to come,
|
||||
/// the name it takes and the file it was read from.
|
||||
opening: Option<(std::sync::mpsc::Receiver<serde_json::Value>, String, String)>,
|
||||
}
|
||||
|
||||
impl Download {
|
||||
@@ -36,14 +33,13 @@ impl Download {
|
||||
}
|
||||
|
||||
pub fn tick(&mut self, app: &mut SolveApp, ctx: &egui::Context) {
|
||||
self.opened(app);
|
||||
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() || self.opening.is_some() || app.file_picker_busy() {
|
||||
if self.pending.is_none() || app.file_picker_busy() {
|
||||
return;
|
||||
}
|
||||
if solidcraft_ui_egui::documents::any_dirty(app) {
|
||||
@@ -75,33 +71,18 @@ impl Download {
|
||||
}
|
||||
}
|
||||
let Some((name, bytes)) = self.pending.take() else { return };
|
||||
// The extension selects the design loader even when the URL has no extension. Named
|
||||
// after the link, as the progress card shows the file's name.
|
||||
let file: String = name.chars().map(|c| if matches!(c, '/' | '\\') { '_' } else { c }).collect();
|
||||
let path = format!("/linked/{}.{}", if file.is_empty() { "design" } else { &file }, solidcraft_engine::io::DESIGN_EXT);
|
||||
vfs::insert(&path, bytes);
|
||||
// Opened as any design is, with its progress and Cancel; finished in `opened`.
|
||||
self.opening = Some((app.open_path_answered(&path), name, path));
|
||||
self.opened(app);
|
||||
}
|
||||
|
||||
/// The downloaded design is in: it is an unsaved copy under the link's name.
|
||||
fn opened(&mut self, app: &mut SolveApp) {
|
||||
let Some((rx, _, _)) = &self.opening else { return };
|
||||
let (ok, cancelled) = match rx.try_recv() {
|
||||
Ok(reply) => (reply["ok"].as_bool() == Some(true), reply["error"].as_str().is_some_and(|e| e.contains("cancelled"))),
|
||||
Err(std::sync::mpsc::TryRecvError::Empty) => return,
|
||||
Err(std::sync::mpsc::TryRecvError::Disconnected) => (false, false),
|
||||
};
|
||||
let Some((_, name, path)) = self.opening.take() else { return };
|
||||
if ok && app.session.path.as_deref() == Some(path.as_str()) {
|
||||
// The extension selects the design loader even when the URL has no extension.
|
||||
let path = &format!("/linked/design.{}", solidcraft_engine::io::DESIGN_EXT);
|
||||
vfs::insert(path, bytes);
|
||||
app.open_path(path);
|
||||
if app.session.path.as_deref() == Some(path.as_str()) {
|
||||
app.session.path = None;
|
||||
app.session.doc_mut().name = name;
|
||||
app.session.mark_unsaved();
|
||||
} else if !ok && !cancelled {
|
||||
} else {
|
||||
app.set_status("Could not open design link: the downloaded file is not a valid SolidCraft design", true);
|
||||
}
|
||||
let _ = vfs::remove(&path);
|
||||
let _ = vfs::remove(path);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -119,43 +119,6 @@ fn closes_design(key: &str, command: bool, shift: bool, alt: bool, design_shown:
|
||||
design_shown && command && !shift && !alt && key.eq_ignore_ascii_case("w")
|
||||
}
|
||||
|
||||
/// The settings a tab last wrote to the browser's storage, or loaded from another tab (#8644).
|
||||
/// Every open tab of the app shares that storage, and eframe saves each tab's whole copy every
|
||||
/// 30 s and when the page loses focus, so a tab still holding old settings put them back over a
|
||||
/// change made in another. A change is now written in the frame it is made, and the other tabs
|
||||
/// load it ([`settings_written`]).
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
#[derive(Default, Debug)]
|
||||
struct PrefsWriter {
|
||||
written: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
impl PrefsWriter {
|
||||
/// `prefs` if they are not the settings written or loaded last (they count as written from
|
||||
/// now on).
|
||||
fn take(&mut self, prefs: String) -> Option<String> {
|
||||
if self.written.as_deref() == Some(prefs.as_str()) {
|
||||
return None;
|
||||
}
|
||||
self.written = Some(prefs.clone());
|
||||
Some(prefs)
|
||||
}
|
||||
|
||||
/// The settings the tab holds after loading another tab's: the tab doesn't write them back.
|
||||
fn loaded(&mut self, prefs: String) {
|
||||
self.written = Some(prefs);
|
||||
}
|
||||
}
|
||||
|
||||
/// The settings another tab wrote, from a `storage` event (the browser sends one to every other
|
||||
/// tab of the page when one tab writes). Other keys, and settings taken away (clearing the site's
|
||||
/// data), leave this tab's settings as they are.
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
fn settings_written(key: Option<&str>, value: Option<String>) -> Option<String> {
|
||||
value.filter(|_| key == Some(solidcraft_ui_egui::prefs::STORAGE_KEY))
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod web {
|
||||
use std::rc::Rc;
|
||||
@@ -254,29 +217,9 @@ mod web {
|
||||
on_key.forget();
|
||||
}
|
||||
|
||||
/// Settings another tab wrote, waiting for this tab's next frame (#8644).
|
||||
type PrefsInbox = Rc<std::cell::Cell<Option<String>>>;
|
||||
|
||||
/// Settings another tab of the app writes go to `inbox`, and this tab draws a frame, which
|
||||
/// loads them.
|
||||
fn hear_settings_from_other_tabs(window: &web_sys::Window, ctx: &egui::Context, inbox: PrefsInbox) {
|
||||
let ctx = ctx.clone();
|
||||
let on_storage = wasm_bindgen::closure::Closure::<dyn FnMut(web_sys::StorageEvent)>::new(move |e: web_sys::StorageEvent| {
|
||||
if let Some(prefs) = crate::settings_written(e.key().as_deref(), e.new_value()) {
|
||||
inbox.set(Some(prefs));
|
||||
ctx.request_repaint();
|
||||
}
|
||||
});
|
||||
let _ = window.add_event_listener_with_callback("storage", on_storage.as_ref().unchecked_ref());
|
||||
// The listener lives as long as the page.
|
||||
on_storage.forget();
|
||||
}
|
||||
|
||||
const JAPANESE_FONT_PATH: &str = "./fonts/BIZUDPGothic-Regular.ttf";
|
||||
const CHINESE_SC_FONT_PATH: &str = "./fonts/CraftUI-SC.otf";
|
||||
const CHINESE_TC_FONT_PATH: &str = "./fonts/CraftUI-TC.otf";
|
||||
|
||||
type FontLoadResult = Rc<std::cell::RefCell<Option<Result<(String, Vec<u8>), String>>>>;
|
||||
type FontLoadResult = Rc<std::cell::RefCell<Option<Result<Vec<u8>, String>>>>;
|
||||
|
||||
struct Shell {
|
||||
app: SolveApp,
|
||||
@@ -294,63 +237,18 @@ mod web {
|
||||
rename_focus: bool,
|
||||
/// The start page's recent designs were filled from the stored ones.
|
||||
recents_synced: bool,
|
||||
/// On-demand script fonts: fetched only when a CJK language is selected.
|
||||
/// On-demand CJK font: fetched only when a CJK language is selected.
|
||||
font: solidcraft_ui_egui::fonts::CjkFontLoader,
|
||||
font_result: FontLoadResult,
|
||||
/// The fetched Japanese font's bytes: kept so a language change can re-order the fallbacks.
|
||||
/// The fetched CJK font's bytes: kept so a language change can re-order the fallbacks.
|
||||
cjk_bytes: Option<Vec<u8>>,
|
||||
/// The fetched Chinese font's bytes (Simplified or Traditional for this session).
|
||||
chinese_bytes: Option<Vec<u8>>,
|
||||
/// Which Chinese face `chinese_bytes` is: `"zh"` or `"zh-TW"`.
|
||||
chinese_kind: Option<&'static str>,
|
||||
/// Kind requested by the in-flight fetch (`"ja"`, `"zh"`, or `"zh-TW"`).
|
||||
font_fetch_kind: Option<&'static str>,
|
||||
/// The language the fonts were last set up for (a CJK UI leads with the CJK face).
|
||||
font_language: String,
|
||||
space_mouse: SpaceMouse,
|
||||
wheel_gestures: solidcraft_ui_egui::WebWheelGestures,
|
||||
/// The settings this tab wrote or loaded last, and those another tab wrote since (#8644).
|
||||
prefs_writer: crate::PrefsWriter,
|
||||
other_tab_prefs: PrefsInbox,
|
||||
}
|
||||
|
||||
impl Shell {
|
||||
/// The settings as they are kept between visits: with the user's own language, not a
|
||||
/// session-only English fallback.
|
||||
fn persisted_prefs(&mut self) -> String {
|
||||
let saved = self.font.persisted_language(&self.app.preferences.language).to_owned();
|
||||
let live = std::mem::replace(&mut self.app.preferences.language, saved);
|
||||
let prefs = self.app.prefs();
|
||||
self.app.preferences.language = live;
|
||||
prefs
|
||||
}
|
||||
|
||||
/// A settings change goes to the browser's storage in the frame it is made, and from
|
||||
/// there to the other open tabs (#8644). Input is what changes settings, so a frame
|
||||
/// without any is skipped; eframe's own save still catches anything else.
|
||||
fn store_prefs(&mut self, ctx: &egui::Context) {
|
||||
if !self.launch.persists() || ctx.input(|i| i.events.is_empty()) {
|
||||
return;
|
||||
}
|
||||
let prefs = self.persisted_prefs();
|
||||
let Some(prefs) = self.prefs_writer.take(prefs) else { return };
|
||||
let stored =
|
||||
web_sys::window().and_then(|w| w.local_storage().ok().flatten()).map(|s| s.set_item(solidcraft_ui_egui::prefs::STORAGE_KEY, &prefs));
|
||||
if !matches!(stored, Some(Ok(()))) {
|
||||
log::warn!("settings: the browser's storage did not take them; they are saved again later");
|
||||
self.prefs_writer = crate::PrefsWriter::default();
|
||||
}
|
||||
}
|
||||
|
||||
/// Settings another tab wrote replace this tab's, so this tab's own saves keep them
|
||||
/// instead of putting its older copy back (#8644).
|
||||
fn load_other_tabs_prefs(&mut self) {
|
||||
let Some(prefs) = self.other_tab_prefs.take() else { return };
|
||||
self.app.load_prefs(&prefs);
|
||||
let held = self.persisted_prefs();
|
||||
self.prefs_writer.loaded(held);
|
||||
}
|
||||
|
||||
/// Files from the computer, writes to store, messages and the autosave.
|
||||
fn files_tick(&mut self, ctx: &egui::Context) {
|
||||
if !self.launch.persists() {
|
||||
@@ -375,8 +273,7 @@ mod web {
|
||||
// Designs opened from the computer's disk (the files window): open them.
|
||||
let opened: Vec<_> = self.files.opened.borrow_mut().drain(..).collect();
|
||||
for path in opened {
|
||||
// A few features a frame, with progress and Cancel (#10198).
|
||||
self.app.open_path_async(&path);
|
||||
self.app.open_path(&path);
|
||||
self.files.panel.set(false);
|
||||
}
|
||||
// Recent designs whose file is gone are dropped from the list.
|
||||
@@ -417,7 +314,7 @@ mod web {
|
||||
path
|
||||
};
|
||||
match intent {
|
||||
Intent::Open => self.app.open_path_async(&path),
|
||||
Intent::Open => self.app.open_path(&path),
|
||||
Intent::Insert | Intent::Theme => self.app.drop_path(&path),
|
||||
}
|
||||
self.files.panel.set(false);
|
||||
@@ -581,7 +478,7 @@ mod web {
|
||||
let path = format!("{}{name}", files::OPFS);
|
||||
match what {
|
||||
"open" => {
|
||||
self.app.open_path_async(&path);
|
||||
self.app.open_path(&path);
|
||||
self.files.panel.set(false);
|
||||
}
|
||||
"insert" => {
|
||||
@@ -683,102 +580,65 @@ mod web {
|
||||
files::autosave(&self.files, None);
|
||||
}
|
||||
|
||||
/// Whether the shell already has the on-demand face the live language needs.
|
||||
fn script_font_ready(&self) -> bool {
|
||||
match solidcraft_ui_egui::fonts::web_script_font_kind(solidcraft_ui_egui::i18n::effective_language(&self.app.preferences.language)) {
|
||||
Some("zh") => self.chinese_bytes.is_some() && self.chinese_kind == Some("zh"),
|
||||
Some("zh-TW") => self.chinese_bytes.is_some() && self.chinese_kind == Some("zh-TW"),
|
||||
Some("ja") => self.cjk_bytes.is_some(),
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch and install a script font when a CJK language is selected. If that fails, this
|
||||
/// Fetch and install the CJK font when a CJK language is selected. If that fails, this
|
||||
/// session falls back to English (no tofu); the saved language preference is kept.
|
||||
fn tick_font(&mut self, ctx: &egui::Context) {
|
||||
let done = self.font_result.borrow_mut().take();
|
||||
if let Some(res) = done {
|
||||
let kind = self.font_fetch_kind.take();
|
||||
let installed = match res {
|
||||
Ok((path, bytes)) => {
|
||||
match kind {
|
||||
Some("zh") => {
|
||||
self.chinese_bytes = Some(bytes);
|
||||
self.chinese_kind = Some("zh");
|
||||
}
|
||||
Some("zh-TW") => {
|
||||
self.chinese_bytes = Some(bytes);
|
||||
self.chinese_kind = Some("zh-TW");
|
||||
}
|
||||
_ => {
|
||||
self.cjk_bytes = Some(bytes);
|
||||
}
|
||||
}
|
||||
log::info!("loaded web script font from {path}");
|
||||
Ok(bytes) => {
|
||||
self.cjk_bytes = Some(bytes);
|
||||
true
|
||||
}
|
||||
Err(err) => {
|
||||
let path = match kind {
|
||||
Some("zh") => CHINESE_SC_FONT_PATH,
|
||||
Some("zh-TW") => CHINESE_TC_FONT_PATH,
|
||||
_ => JAPANESE_FONT_PATH,
|
||||
};
|
||||
log::warn!("failed to fetch script font from {path}: {err}");
|
||||
log::warn!("failed to fetch CJK font from {JAPANESE_FONT_PATH}: {err}");
|
||||
false
|
||||
}
|
||||
};
|
||||
let effective = solidcraft_ui_egui::i18n::effective_language(&self.app.preferences.language);
|
||||
if !installed && solidcraft_ui_egui::fonts::needs_cjk_font(effective) {
|
||||
log::warn!("script font unavailable; showing English for this session");
|
||||
if installed {
|
||||
log::info!("loaded web CJK font from {JAPANESE_FONT_PATH}");
|
||||
} else if solidcraft_ui_egui::fonts::needs_cjk_font(&self.app.preferences.language) {
|
||||
log::warn!("CJK font unavailable; showing English for this session");
|
||||
}
|
||||
self.font.finished(installed, &mut self.app.preferences.language);
|
||||
self.apply_fonts(ctx);
|
||||
ctx.request_repaint();
|
||||
}
|
||||
let ready = self.script_font_ready();
|
||||
let effective = solidcraft_ui_egui::i18n::effective_language(&self.app.preferences.language);
|
||||
if self.font.tick(&mut self.app.preferences.language, ready) {
|
||||
self.fetch_font(ctx, solidcraft_ui_egui::fonts::web_script_font_kind(effective).unwrap_or("ja"));
|
||||
if self.font.tick(&mut self.app.preferences.language) {
|
||||
self.fetch_font(ctx);
|
||||
}
|
||||
// The CJK face leads only a CJK UI: its Cyrillic glyphs are full-width, so switching
|
||||
// to Ukrainian (or English) must put the Latin face back in front.
|
||||
if solidcraft_ui_egui::i18n::effective_language(&self.app.preferences.language) != self.font_language {
|
||||
if self.app.preferences.language != self.font_language {
|
||||
self.apply_fonts(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Set up the UI fonts for the live language, keeping fetched script fonts as fallbacks
|
||||
/// (Chinese leads a Chinese UI; see [`solidcraft_ui_egui::fonts::install_web_script_fonts`]).
|
||||
/// Set up the UI fonts for the live language, keeping the fetched CJK font as a fallback
|
||||
/// (it leads only for a CJK UI, see [`solidcraft_ui_egui::fonts::install_cjk_font_for`]).
|
||||
fn apply_fonts(&mut self, ctx: &egui::Context) {
|
||||
let effective = solidcraft_ui_egui::i18n::effective_language(&self.app.preferences.language);
|
||||
if self.cjk_bytes.is_none() && self.chinese_bytes.is_none() {
|
||||
solidcraft_ui_egui::fonts::setup_fonts(ctx);
|
||||
} else {
|
||||
solidcraft_ui_egui::fonts::install_web_script_fonts(ctx, self.cjk_bytes.clone(), self.chinese_bytes.clone(), effective);
|
||||
match self.cjk_bytes.clone() {
|
||||
Some(bytes) => {
|
||||
solidcraft_ui_egui::fonts::install_cjk_font_for(ctx, bytes, &self.app.preferences.language);
|
||||
}
|
||||
None => solidcraft_ui_egui::fonts::setup_fonts(ctx),
|
||||
}
|
||||
self.font_language = effective.to_owned();
|
||||
self.font_language.clone_from(&self.app.preferences.language);
|
||||
}
|
||||
|
||||
/// CJK text on screen with no Japanese font yet (an English UI showing a Japanese name):
|
||||
/// fetch the Japanese font, so the text does not stay tofu (#720).
|
||||
/// CJK text on screen with no CJK font yet (an English UI showing a Japanese name):
|
||||
/// fetch the font, so the text does not stay tofu (#720).
|
||||
fn watch_text_for_font(&mut self, ctx: &egui::Context) {
|
||||
if self.font.watching_text() && solidcraft_ui_egui::fonts::painted_cjk(ctx) && self.font.text_needs_font() {
|
||||
self.fetch_font(ctx, "ja");
|
||||
self.fetch_font(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
fn fetch_font(&mut self, ctx: &egui::Context, kind: &'static str) {
|
||||
let path = match kind {
|
||||
"zh" => CHINESE_SC_FONT_PATH,
|
||||
"zh-TW" => CHINESE_TC_FONT_PATH,
|
||||
_ => JAPANESE_FONT_PATH,
|
||||
};
|
||||
self.font_fetch_kind = Some(kind);
|
||||
fn fetch_font(&self, ctx: &egui::Context) {
|
||||
let slot = self.font_result.clone();
|
||||
let ctx = ctx.clone();
|
||||
let path = path.to_owned();
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
let res = fetch_font_bytes(&path).await.map(|bytes| (path, bytes));
|
||||
let res = fetch_font_bytes(JAPANESE_FONT_PATH).await;
|
||||
*slot.borrow_mut() = Some(res);
|
||||
ctx.request_repaint();
|
||||
});
|
||||
@@ -787,8 +647,6 @@ mod web {
|
||||
|
||||
impl eframe::App for Shell {
|
||||
fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
// First: a language another tab chose fetches its font this frame.
|
||||
self.load_other_tabs_prefs();
|
||||
self.tick_font(ctx);
|
||||
self.app.logic(ctx);
|
||||
self.api.poll();
|
||||
@@ -831,7 +689,6 @@ mod web {
|
||||
self.recovery_prompt(&ctx);
|
||||
}
|
||||
self.watch_text_for_font(&ctx);
|
||||
self.store_prefs(&ctx);
|
||||
}
|
||||
fn persist_egui_memory(&self) -> bool {
|
||||
self.launch.persists()
|
||||
@@ -840,8 +697,11 @@ mod web {
|
||||
if !self.launch.persists() {
|
||||
return;
|
||||
}
|
||||
let prefs = self.persisted_prefs();
|
||||
storage.set_string(solidcraft_ui_egui::prefs::STORAGE_KEY, prefs);
|
||||
// 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);
|
||||
storage.set_string(solidcraft_ui_egui::prefs::STORAGE_KEY, self.app.prefs());
|
||||
self.app.preferences.language = live;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -962,12 +822,6 @@ mod web {
|
||||
let (sender, receiver) = std::sync::mpsc::channel();
|
||||
let mut app = SolveApp::new(Session::default(), services).with_control(receiver);
|
||||
app.embedded = launch.embed;
|
||||
// Listening before reading: settings another tab writes from now on are
|
||||
// newer than those read here.
|
||||
let other_tab_prefs = PrefsInbox::default();
|
||||
if launch.persists() {
|
||||
hear_settings_from_other_tabs(&window, &cc.egui_ctx, other_tab_prefs.clone());
|
||||
}
|
||||
if launch.persists() && let Some(p) = cc.storage.and_then(|s| solidcraft_ui_egui::prefs::stored(|k| s.get_string(k))) {
|
||||
app.load_prefs(&p);
|
||||
}
|
||||
@@ -1008,14 +862,9 @@ mod web {
|
||||
font: Default::default(),
|
||||
font_result: Rc::new(std::cell::RefCell::new(None)),
|
||||
cjk_bytes: None,
|
||||
chinese_bytes: None,
|
||||
chinese_kind: None,
|
||||
font_fetch_kind: None,
|
||||
font_language,
|
||||
space_mouse: SpaceMouse::default(),
|
||||
wheel_gestures: solidcraft_ui_egui::WebWheelGestures::default(),
|
||||
prefs_writer: crate::PrefsWriter::default(),
|
||||
other_tab_prefs,
|
||||
}))
|
||||
}),
|
||||
)
|
||||
@@ -1152,28 +1001,4 @@ mod tests {
|
||||
assert!(!closes_design("w", false, false, false, true));
|
||||
assert!(!closes_design("s", true, false, false, true));
|
||||
}
|
||||
|
||||
/// A tab writes its settings when they change, and not again while they stay the same; the
|
||||
/// settings it loaded from another tab aren't written back (#8644).
|
||||
#[test]
|
||||
fn changed_settings_are_written_once_and_loaded_ones_not_at_all() {
|
||||
let mut writer = PrefsWriter::default();
|
||||
assert_eq!(writer.take(r#"{"a":1}"#.into()).as_deref(), Some(r#"{"a":1}"#), "the first settings are written");
|
||||
assert_eq!(writer.take(r#"{"a":1}"#.into()), None, "the same settings are not written again");
|
||||
assert_eq!(writer.take(r#"{"a":2}"#.into()).as_deref(), Some(r#"{"a":2}"#), "a change is written at once");
|
||||
writer.loaded(r#"{"a":3}"#.into());
|
||||
assert_eq!(writer.take(r#"{"a":3}"#.into()), None, "settings from another tab are not written back");
|
||||
assert_eq!(writer.take(r#"{"a":4}"#.into()).as_deref(), Some(r#"{"a":4}"#), "this tab's next change is written");
|
||||
}
|
||||
|
||||
/// Only the settings another tab wrote are loaded: not other keys, and not settings taken
|
||||
/// away by clearing the site's data (#8644).
|
||||
#[test]
|
||||
fn only_settings_another_tab_wrote_are_loaded() {
|
||||
let key = solidcraft_ui_egui::prefs::STORAGE_KEY;
|
||||
assert_eq!(settings_written(Some(key), Some("{}".into())).as_deref(), Some("{}"));
|
||||
assert_eq!(settings_written(Some("solidcraft.webgl"), Some("1".into())), None);
|
||||
assert_eq!(settings_written(Some(key), None), None, "removed settings leave this tab's");
|
||||
assert_eq!(settings_written(None, None), None, "clearing all storage leaves this tab's settings");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,12 +8,9 @@ publish = false
|
||||
default-run = "solidcraft"
|
||||
|
||||
[features]
|
||||
default = ["raytrace-cpu", "bundled-render-library", "profile"]
|
||||
default = ["raytrace-cpu", "bundled-render-library"]
|
||||
bundled-render-library = ["solidcraft-engine/bundled-render-library"]
|
||||
raytrace-cpu = ["solidcraft-engine/raytrace-cpu"]
|
||||
# The profiler, compiled in so any build can say where its time goes (the Profile operations
|
||||
# preference, SOLIDCRAFT_PROFILE=1, diagnostics.profile); it records nothing until asked.
|
||||
profile = ["solidcraft-engine/profile"]
|
||||
|
||||
# The workspace lints, except unsafe_code: denied, not forbidden, so that src/open_documents.rs
|
||||
# (macOS file-open events), src/native_crash.rs (Windows exception filter), src/native_crash_macos.rs
|
||||
@@ -107,10 +104,5 @@ winit = { version = "0.30", default-features = false, features = ["x11"] }
|
||||
raw-window-handle = "0.6"
|
||||
x11rb = { version = "0.13", default-features = false }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
# The desktop's dark or light preference through the XDG Desktop Portal (src/desktop_theme.rs,
|
||||
# #7944); already in the tree through eframe's accessibility support (accesskit_unix).
|
||||
zbus = { version = "5", default-features = false, features = ["async-io", "blocking-api"] }
|
||||
|
||||
[build-dependencies]
|
||||
embed-resource = "3.0.11"
|
||||
|
||||
@@ -22,10 +22,6 @@ 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);
|
||||
/// How long a new connection has to show the token. A client connects when it has a request to
|
||||
/// send, so this is plenty; one that doesn't send it loses the connection, and its seat among
|
||||
/// [`MAX_CONNECTIONS`] goes to someone else.
|
||||
const TOKEN_WAIT: Duration = Duration::from_secs(5);
|
||||
|
||||
static OPEN: AtomicUsize = AtomicUsize::new(0);
|
||||
/// The token files this launch wrote (its control port, the agent chat's endpoint) and the
|
||||
@@ -229,53 +225,23 @@ fn unauthorized() -> Value {
|
||||
/// Refuse the connection: the reply, then the end of what we send, then what the client had
|
||||
/// already sent read and dropped (a little, briefly). Closing a socket with unread input resets
|
||||
/// the connection, and a reset can reach the client before it has read the refusal.
|
||||
fn refuse(out: &mut TcpStream, reader: &mut BufReader<ClientInput>) {
|
||||
fn refuse(out: &mut TcpStream, reader: &mut BufReader<TcpStream>) {
|
||||
use std::io::Read;
|
||||
let _ = send(out, &unauthorized());
|
||||
let _ = out.shutdown(std::net::Shutdown::Write);
|
||||
reader.get_mut().deadline = Some(Instant::now() + Duration::from_millis(500));
|
||||
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());
|
||||
}
|
||||
|
||||
/// What a client sends. Until it shows the token, all of its reads share one deadline, so neither
|
||||
/// blank lines nor a line sent a byte at a time buy it more time. After that it may stay idle as
|
||||
/// long as it likes.
|
||||
struct ClientInput {
|
||||
socket: TcpStream,
|
||||
deadline: Option<Instant>,
|
||||
}
|
||||
|
||||
impl ClientInput {
|
||||
/// No deadline any more: the client showed the token.
|
||||
fn admit(&mut self) -> std::io::Result<()> {
|
||||
self.deadline = None;
|
||||
self.socket.set_read_timeout(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::io::Read for ClientInput {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
if let Some(deadline) = self.deadline {
|
||||
let left = deadline.saturating_duration_since(Instant::now());
|
||||
if left.is_zero() {
|
||||
return Err(std::io::ErrorKind::TimedOut.into());
|
||||
}
|
||||
self.socket.set_read_timeout(Some(left))?;
|
||||
}
|
||||
self.socket.read(buf)
|
||||
}
|
||||
}
|
||||
|
||||
/// How long a request waits for the app's reply: most requests, and a batch (or every request,
|
||||
/// on a patient connection); and how long a new connection has to show the token.
|
||||
/// on a patient connection).
|
||||
#[derive(Clone, Copy)]
|
||||
struct Waits {
|
||||
short: Duration,
|
||||
long: Duration,
|
||||
token: Duration,
|
||||
}
|
||||
|
||||
const WAITS: Waits = Waits { short: Duration::from_secs(60), long: Duration::from_secs(3600), token: TOKEN_WAIT };
|
||||
const WAITS: Waits = Waits { short: Duration::from_secs(60), long: Duration::from_secs(3600) };
|
||||
|
||||
/// A request that timed out but is still running in the app (#1618): its id and method, and where
|
||||
/// its reply will land.
|
||||
@@ -447,37 +413,11 @@ impl Lines {
|
||||
}
|
||||
}
|
||||
|
||||
/// A subscribed connection's seat on the watch: it leaves exactly once, whichever thread sees
|
||||
/// the connection's end first — the connection's own thread at the end of the stream, or the
|
||||
/// events thread at the first write a closed client refuses. The seat used to wait for the
|
||||
/// events thread alone, and that thread only leaves its wait when the design moves, so the
|
||||
/// count could outlive the connection by a second or more (#8315).
|
||||
struct Seat(Mutex<Option<Subscriber>>);
|
||||
|
||||
impl Seat {
|
||||
fn new(counted: Subscriber) -> Self {
|
||||
Seat(Mutex::new(Some(counted)))
|
||||
}
|
||||
|
||||
/// Leave the watch now: the seat goes once, whoever gets here first.
|
||||
fn release(&self) {
|
||||
let counted = self.0.lock().unwrap_or_else(PoisonError::into_inner).take();
|
||||
drop(counted);
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Seat {
|
||||
fn drop(&mut self) {
|
||||
self.release();
|
||||
}
|
||||
}
|
||||
|
||||
/// 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>,
|
||||
seat: Arc<Seat>,
|
||||
}
|
||||
|
||||
impl Subscription {
|
||||
@@ -493,17 +433,15 @@ impl Subscription {
|
||||
) -> 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 seat = Arc::new(Seat::new(counted));
|
||||
let first = lines.turn.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
let (l, s, watch_seat) = (lines.clone(), stop.clone(), Arc::clone(&seat));
|
||||
let (l, s) = (lines.clone(), stop.clone());
|
||||
// The connection's seat on the watch rides the events thread: it leaves when the thread
|
||||
// does, so the next failed write to a closed client retires it even while the connection's
|
||||
// own thread is slow to see the end of the stream (#7226). The connection's own thread
|
||||
// leaves it too, so a closed connection stops counting at once (#8315).
|
||||
std::thread::Builder::new().stack_size(64 * 1024).spawn(move || send_events(design, &l, &s, seen, watch_seat)).ok()?;
|
||||
// own thread is slow to see the end of the stream (#7226).
|
||||
std::thread::Builder::new().stack_size(64 * 1024).spawn(move || send_events(design, &l, &s, seen, counted)).ok()?;
|
||||
let sent = send(&mut &lines.socket, reply);
|
||||
drop(first);
|
||||
Some((Subscription { lines, stop, seat }, sent))
|
||||
Some((Subscription { lines, stop }, sent))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -516,9 +454,6 @@ fn design_of(reply: &Value) -> Option<(String, u64)> {
|
||||
impl Drop for Subscription {
|
||||
fn drop(&mut self) {
|
||||
self.stop.store(true, Ordering::Relaxed);
|
||||
// The seat on the watch goes now: the connection is over, and nothing counts it any more
|
||||
// (#8315).
|
||||
self.seat.release();
|
||||
// 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);
|
||||
@@ -526,10 +461,10 @@ impl Drop for Subscription {
|
||||
}
|
||||
|
||||
/// The `design.changed` lines of a subscribed connection, each time the design differs from the
|
||||
/// last one it was told of (`seen`), until `stop`. Holds the connection's seat on the watch
|
||||
/// (`seat`), which it leaves when the thread ends: on `stop`, or on the first write a closed
|
||||
/// client refuses (#7226).
|
||||
fn send_events(design: &DesignWatch, lines: &Lines, stop: &AtomicBool, mut seen: (String, u64), seat: Arc<Seat>) {
|
||||
/// last one it was told of (`seen`), until `stop`. Owns the connection's seat on the watch
|
||||
/// (`counted`), so the seat leaves exactly when this thread does: on `stop`, or on the first
|
||||
/// write a closed client refuses (#7226).
|
||||
fn send_events(design: &DesignWatch, lines: &Lines, stop: &AtomicBool, mut seen: (String, u64), _counted: Subscriber) {
|
||||
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() {
|
||||
@@ -538,17 +473,6 @@ fn send_events(design: &DesignWatch, lines: &Lines, stop: &AtomicBool, mut seen:
|
||||
seen = now;
|
||||
std::thread::sleep(EVENT_GAP);
|
||||
}
|
||||
// The client is gone: it is not counted on the watch any more, whatever the connection's own
|
||||
// thread still has to notice (#8315).
|
||||
seat.release();
|
||||
}
|
||||
|
||||
/// What this app's control channel does beyond the methods every version has, for a client to
|
||||
/// find out before it relies on it (an older app answers `control.capabilities` with "unknown
|
||||
/// method"). `param_check`: the levels `engine.execute`, `engine.batch` and `engine.script` take
|
||||
/// as a request's own parameter check (#9184).
|
||||
fn capabilities() -> Value {
|
||||
json!({"param_check": solidcraft_engine::schema::ParamCheck::ALL.map(|l| l.name())})
|
||||
}
|
||||
|
||||
/// Write `reply` on the connection: through its subscription once it has one.
|
||||
@@ -577,7 +501,7 @@ fn serve_with(
|
||||
let connection = running.connections.fetch_add(1, Ordering::Relaxed) + 1;
|
||||
let Ok(read) = stream.try_clone() else { return };
|
||||
let mut out = stream;
|
||||
let mut reader = BufReader::new(ClientInput { socket: read, deadline: Some(Instant::now() + waits.token) });
|
||||
let mut reader = BufReader::new(read);
|
||||
let mut bad = 0usize;
|
||||
let mut authenticated = false;
|
||||
let mut pending: Option<Pending> = None;
|
||||
@@ -588,10 +512,6 @@ fn serve_with(
|
||||
let line = match read_line(&mut reader, MAX_LINE) {
|
||||
Ok(Some(l)) => String::from_utf8_lossy(&l).into_owned(),
|
||||
Ok(None) => break,
|
||||
// Closed without a word: a client that connected early and only now sends its first
|
||||
// request would read a line here as the reply to it. It finds the connection closed
|
||||
// and opens another.
|
||||
Err(e) if !authenticated && matches!(e.kind(), std::io::ErrorKind::TimedOut | std::io::ErrorKind::WouldBlock) => break,
|
||||
Err(e) => {
|
||||
let _ = answer(&mut out, events.as_ref(), &json!({"ok": false, "error": e.to_string()}));
|
||||
break;
|
||||
@@ -603,12 +523,7 @@ fn serve_with(
|
||||
// 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) => {
|
||||
if reader.get_mut().admit().is_err() {
|
||||
break;
|
||||
}
|
||||
authenticated = true;
|
||||
}
|
||||
Some(t) if control_auth::tokens_match(&t, token) => authenticated = true,
|
||||
_ => {
|
||||
refuse(&mut out, &mut reader);
|
||||
break;
|
||||
@@ -620,20 +535,15 @@ fn serve_with(
|
||||
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!({}));
|
||||
// A request that timed out may have finished since (#1618). `control.running` and
|
||||
// `control.capabilities` only look, so they leave that reply where it is.
|
||||
let late = pending.as_ref().filter(|_| !matches!(method.as_str(), "control.running" | "control.capabilities")).and_then(|p| match p
|
||||
.reply
|
||||
.try_recv()
|
||||
{
|
||||
// A request that timed out may have finished since (#1618). `control.running` only
|
||||
// looks, so it leaves that reply where it is.
|
||||
let late = pending.as_ref().filter(|_| method != "control.running").and_then(|p| match p.reply.try_recv() {
|
||||
Ok(reply) => Some(reply),
|
||||
Err(TryRecvError::Disconnected) => Some(json!({"ok": false, "error": "the app ended the request without a reply"})),
|
||||
Err(TryRecvError::Empty) => None,
|
||||
});
|
||||
let mut r = if method == "control.running" {
|
||||
json!({"ok": true, "result": running.result(connection)})
|
||||
} else if method == "control.capabilities" {
|
||||
json!({"ok": true, "result": capabilities()})
|
||||
} else if method == "control.pending" {
|
||||
match (pending.take(), late) {
|
||||
(None, _) => json!({"ok": true, "result": {"pending": null}}),
|
||||
@@ -718,80 +628,6 @@ fn serve_with(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A connection that doesn't show the token in time is closed without a word, however it fills
|
||||
/// the time. One that does keeps working when it idles for longer than that.
|
||||
#[test]
|
||||
fn the_token_has_a_time_limit_and_an_authenticated_client_may_idle() {
|
||||
use std::io::Read;
|
||||
let token = "0123456789abcdef0123456789abcdef";
|
||||
let limit = Duration::from_millis(500);
|
||||
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
let (tx, rx) = channel::<ControlRequest>();
|
||||
std::thread::spawn(move || {
|
||||
for req in rx {
|
||||
let _ = req.reply.send(json!({"ok": true, "result": req.method}));
|
||||
}
|
||||
});
|
||||
static RUNNING: Running = Running::new();
|
||||
let waits = Waits { short: Duration::from_secs(10), long: Duration::from_secs(10), token: limit };
|
||||
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));
|
||||
}
|
||||
});
|
||||
let open = || {
|
||||
let s = TcpStream::connect(addr).unwrap();
|
||||
s.set_read_timeout(Some(Duration::from_secs(20))).unwrap();
|
||||
s
|
||||
};
|
||||
// Sends `drip` every 50 ms until the server closes; what it read, and when it closed.
|
||||
let closed_after = |drip: &[u8]| {
|
||||
let mut s = open();
|
||||
let start = Instant::now();
|
||||
s.set_read_timeout(Some(Duration::from_millis(50))).unwrap();
|
||||
let mut got = Vec::new();
|
||||
loop {
|
||||
let mut buf = [0u8; 256];
|
||||
match s.read(&mut buf) {
|
||||
Ok(0) => break,
|
||||
Ok(n) => got.extend_from_slice(buf.get(..n).unwrap()),
|
||||
Err(e) if matches!(e.kind(), std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut) => {
|
||||
assert!(start.elapsed() < Duration::from_secs(20), "still open");
|
||||
let _ = s.write_all(drip);
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
(got, start.elapsed())
|
||||
};
|
||||
for drip in [&b""[..], b"\n", b"\r\n \n", b" ", b"{"] {
|
||||
let (got, took) = closed_after(drip);
|
||||
assert!(got.is_empty(), "{drip:?} was answered: {}", String::from_utf8_lossy(&got));
|
||||
assert!(took >= limit.saturating_sub(Duration::from_millis(50)), "{drip:?} closed after {took:?}");
|
||||
}
|
||||
|
||||
let mut s = open();
|
||||
let mut lines = BufReader::new(s.try_clone().unwrap());
|
||||
let mut ask = |line: Value| -> Value {
|
||||
writeln!(s, "{line}").unwrap();
|
||||
let mut l = String::new();
|
||||
lines.read_line(&mut l).unwrap();
|
||||
serde_json::from_str(&l).unwrap()
|
||||
};
|
||||
assert_eq!(ask(json!({"id": 1, "method": "ui.view", "token": token}))["ok"], true);
|
||||
std::thread::sleep(limit * 3);
|
||||
assert_eq!(ask(json!({"id": 2, "method": "ui.view"}))["ok"], true, "idle for longer than the limit");
|
||||
|
||||
// A wrong token is still answered, at once.
|
||||
let mut wrong = open();
|
||||
writeln!(wrong, "{}", json!({"id": 1, "method": "ui.view", "token": "nope"})).unwrap();
|
||||
let mut l = String::new();
|
||||
BufReader::new(wrong).read_line(&mut l).unwrap();
|
||||
assert!(l.contains("unauthorized"), "{l}");
|
||||
}
|
||||
|
||||
/// #1618: a request that outlasts its wait says it is still running; the connection keeps its
|
||||
/// reply for `control.pending`, and refuses other requests as `busy` until it lands, so none
|
||||
/// is queued to run unseen behind it.
|
||||
@@ -815,7 +651,7 @@ mod tests {
|
||||
let _ = req.reply.send(json!({"ok": true, "result": req.method}));
|
||||
}
|
||||
});
|
||||
let waits = Waits { short: Duration::from_millis(100), long: Duration::from_millis(100), token: TOKEN_WAIT };
|
||||
let waits = Waits { short: Duration::from_millis(100), long: Duration::from_millis(100) };
|
||||
static RUNNING: Running = Running::new();
|
||||
std::thread::spawn(move || {
|
||||
if let Some(stream) = listener.incoming().flatten().next() {
|
||||
@@ -881,7 +717,7 @@ mod tests {
|
||||
let _ = req.reply.send(json!({"ok": true, "result": req.method}));
|
||||
}
|
||||
});
|
||||
let waits = Waits { short: Duration::from_millis(100), long: Duration::from_secs(60), token: TOKEN_WAIT };
|
||||
let waits = Waits { short: Duration::from_millis(100), long: Duration::from_secs(60) };
|
||||
std::thread::spawn(move || {
|
||||
for stream in listener.incoming().flatten() {
|
||||
let tx = tx.clone();
|
||||
@@ -914,9 +750,6 @@ mod tests {
|
||||
let mut watcher = open();
|
||||
let (r, rows) = running(&mut watcher, &|_| true);
|
||||
assert!(r["ok"] == true && rows.is_empty(), "idle: {r}");
|
||||
// What the app's control channel does beyond the methods every version has (#9184).
|
||||
let r = watcher(json!({"id": 0, "method": "control.capabilities", "token": token}));
|
||||
assert_eq!((&r["ok"], &r["result"]["param_check"]), (&json!(true), &json!(["off", "warn", "strict"])), "{r}");
|
||||
|
||||
let mut export = open();
|
||||
let path = "/private/part.step";
|
||||
@@ -948,9 +781,6 @@ mod tests {
|
||||
let own: Vec<&Value> = r["result"]["running"].as_array().unwrap().iter().filter(|row| row["self"] == true).collect();
|
||||
assert_eq!(own.len(), 1, "{r}");
|
||||
assert_eq!(own[0]["command"], "file.export", "{r}");
|
||||
// So may it ask what the app can do: answered at once, not refused as busy.
|
||||
let r = export(json!({"id": 8, "method": "control.capabilities"}));
|
||||
assert_eq!(r["result"]["param_check"], json!(["off", "warn", "strict"]), "{r}");
|
||||
|
||||
gate_tx.send(()).unwrap();
|
||||
assert_eq!(queued.join().unwrap()["result"], "engine.batch");
|
||||
@@ -1056,58 +886,6 @@ mod tests {
|
||||
assert!(!WATCH.subscribed());
|
||||
}
|
||||
|
||||
/// #8315: a closed connection leaves the watch at once. The seat of a subscribed connection
|
||||
/// goes when the connection's own thread sees the end of the stream, not only when the
|
||||
/// events thread notices: that thread waits for the design to move, so a connection that
|
||||
/// closed while the design stood still kept the watch counting it for a second or more.
|
||||
#[test]
|
||||
fn a_closed_connection_leaves_the_watch_at_once() {
|
||||
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 names the design once and then leaves it alone: nothing moves it, so the
|
||||
// events thread has nothing to write and only the end of the connection can retire it.
|
||||
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}}));
|
||||
} 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 s = TcpStream::connect(addr).unwrap();
|
||||
s.set_read_timeout(Some(Duration::from_secs(10))).unwrap();
|
||||
let mut out = s.try_clone().unwrap();
|
||||
let mut lines = BufReader::new(s);
|
||||
writeln!(out, "{}", json!({"id": 1, "method": "control.subscribe", "token": token})).unwrap();
|
||||
let mut l = String::new();
|
||||
lines.read_line(&mut l).unwrap();
|
||||
let reply: Value = serde_json::from_str(&l).unwrap();
|
||||
assert!(reply["ok"] == true, "{reply}");
|
||||
assert!(WATCH.subscribed(), "the connection is subscribed");
|
||||
// Let the events thread settle into its wait: the design stands still, so it has nothing
|
||||
// to write, and only the end of the connection can retire it.
|
||||
std::thread::sleep(Duration::from_millis(200));
|
||||
// The client goes away without asking anything else.
|
||||
drop((out, lines));
|
||||
let deadline = Instant::now() + Duration::from_millis(400);
|
||||
while WATCH.subscribed() && Instant::now() < deadline {
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
assert!(!WATCH.subscribed(), "a closed connection is not counted on the watch");
|
||||
}
|
||||
|
||||
/// 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]
|
||||
|
||||
@@ -1,195 +0,0 @@
|
||||
//! Linux: the desktop's dark or light preference, for Theme: System (#7944).
|
||||
//!
|
||||
//! The window toolkit does not pass it on everywhere: on GNOME (Wayland, or X11 through XWayland)
|
||||
//! the window never learns it, nor when it changes, so the app kept its colours. This reads the
|
||||
//! freedesktop setting through the XDG Desktop Portal's Settings interface on the session bus
|
||||
//! (`org.freedesktop.appearance color-scheme`, then GNOME's `color-scheme` and `gtk-theme`), the
|
||||
//! same setting other applications follow, and inside a Flatpak too. It runs on a thread of its
|
||||
//! own: the first reading, then the portal's `SettingChanged` signal; where the signal can't be
|
||||
//! had, a reading every few seconds. Without a portal it asks `gsettings` instead, on the same
|
||||
//! thread. The window reads the last answer with an atomic load
|
||||
//! (`solidcraft_ui_egui::Services::desktop_dark`), and the decision from the raw values is
|
||||
//! `solidcraft_ui_egui::prefs::desktop_prefers_dark`.
|
||||
|
||||
use std::io::Read;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU8, Ordering};
|
||||
use std::time::Duration;
|
||||
|
||||
use zbus::zvariant::{OwnedValue, Value};
|
||||
|
||||
const UNKNOWN: u8 = 0;
|
||||
const DARK: u8 = 1;
|
||||
const LIGHT: u8 = 2;
|
||||
|
||||
/// How often to read again where the portal's change signal is not available.
|
||||
const POLL: Duration = Duration::from_secs(5);
|
||||
/// How long one `gsettings` reading may take before it is given up.
|
||||
const GSETTINGS_TIMEOUT: Duration = Duration::from_secs(2);
|
||||
|
||||
const PORTAL: &str = "org.freedesktop.portal.Desktop";
|
||||
const PORTAL_PATH: &str = "/org/freedesktop/portal/desktop";
|
||||
const SETTINGS: &str = "org.freedesktop.portal.Settings";
|
||||
const APPEARANCE: &str = "org.freedesktop.appearance";
|
||||
const GNOME: &str = "org.gnome.desktop.interface";
|
||||
|
||||
/// Start following the desktop's preference; the closure answers the latest reading (`None`
|
||||
/// until there is one, or when the desktop says nothing).
|
||||
pub fn start(ctx: &egui::Context) -> Box<dyn Fn() -> Option<bool>> {
|
||||
let state = Arc::new(AtomicU8::new(UNKNOWN));
|
||||
let (thread_state, ctx) = (state.clone(), ctx.clone());
|
||||
if let Err(e) = std::thread::Builder::new().name("desktop-theme".into()).spawn(move || follow(&thread_state, &ctx)) {
|
||||
log::warn!("desktop theme: no thread to follow it: {e}");
|
||||
}
|
||||
Box::new(move || match state.load(Ordering::Relaxed) {
|
||||
DARK => Some(true),
|
||||
LIGHT => Some(false),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Keep `state` on the desktop's preference, asking for a frame whenever it changes.
|
||||
fn follow(state: &AtomicU8, ctx: &egui::Context) {
|
||||
let publish = |dark: Option<bool>| {
|
||||
let code = match dark {
|
||||
Some(true) => DARK,
|
||||
Some(false) => LIGHT,
|
||||
None => UNKNOWN,
|
||||
};
|
||||
if state.swap(code, Ordering::Relaxed) != code {
|
||||
ctx.request_repaint();
|
||||
}
|
||||
};
|
||||
let portal = match portal() {
|
||||
Ok(p) => Some(p),
|
||||
Err(e) => {
|
||||
log::info!("desktop theme: no settings portal ({e}); asking gsettings");
|
||||
None
|
||||
}
|
||||
};
|
||||
// The portal first; gsettings where there is none or it says nothing (its backend missing).
|
||||
let read = |portal: Option<&zbus::blocking::Proxy<'static>>| portal.and_then(read_portal).or_else(read_gsettings);
|
||||
let from_portal = portal.as_ref().and_then(read_portal);
|
||||
let first = from_portal.or_else(read_gsettings);
|
||||
publish(first);
|
||||
// Wait for the portal's signal only when the portal answers: else the reading is gsettings',
|
||||
// which only a new reading notices.
|
||||
if let Some(p) = portal.as_ref().filter(|_| from_portal.is_some()) {
|
||||
match p.receive_signal("SettingChanged") {
|
||||
Ok(changes) => {
|
||||
for message in changes {
|
||||
if relevant(&message) {
|
||||
publish(read(Some(p)));
|
||||
}
|
||||
}
|
||||
log::info!("desktop theme: the portal's change signal ended; reading every few seconds");
|
||||
}
|
||||
Err(e) => log::info!("desktop theme: no change signal ({e}); reading every few seconds"),
|
||||
}
|
||||
} else if portal.is_none() && first.is_none() && !gsettings_available() {
|
||||
// Neither the portal nor gsettings: nothing to follow; the window toolkit's theme stays.
|
||||
return;
|
||||
}
|
||||
loop {
|
||||
std::thread::sleep(POLL);
|
||||
publish(read(portal.as_ref()));
|
||||
}
|
||||
}
|
||||
|
||||
/// The portal's Settings interface on the session bus.
|
||||
fn portal() -> zbus::Result<zbus::blocking::Proxy<'static>> {
|
||||
let connection = zbus::blocking::Connection::session()?;
|
||||
zbus::blocking::Proxy::new(&connection, PORTAL, PORTAL_PATH, SETTINGS)
|
||||
}
|
||||
|
||||
/// One reading through the portal.
|
||||
fn read_portal(portal: &zbus::blocking::Proxy<'static>) -> Option<bool> {
|
||||
let appearance = read_setting(portal, APPEARANCE, "color-scheme").and_then(|v| as_u32(&v));
|
||||
// The GNOME settings only matter when the freedesktop one has no preference.
|
||||
if matches!(appearance, Some(1 | 2)) {
|
||||
return solidcraft_ui_egui::prefs::desktop_prefers_dark(appearance, None, None);
|
||||
}
|
||||
let gnome = read_setting(portal, GNOME, "color-scheme").and_then(|v| as_string(&v));
|
||||
let gtk_theme = read_setting(portal, GNOME, "gtk-theme").and_then(|v| as_string(&v));
|
||||
solidcraft_ui_egui::prefs::desktop_prefers_dark(appearance, gnome.as_deref(), gtk_theme.as_deref())
|
||||
}
|
||||
|
||||
/// A setting through `ReadOne` (portal version 2), else the older `Read`.
|
||||
fn read_setting(portal: &zbus::blocking::Proxy<'static>, namespace: &str, key: &str) -> Option<OwnedValue> {
|
||||
let one: zbus::Result<OwnedValue> = portal.call("ReadOne", &(namespace, key));
|
||||
one.or_else(|_| -> zbus::Result<OwnedValue> { portal.call("Read", &(namespace, key)) }).ok()
|
||||
}
|
||||
|
||||
/// Whether a `SettingChanged` signal is about a setting the preference is read from.
|
||||
fn relevant(message: &zbus::Message) -> bool {
|
||||
let body = message.body();
|
||||
let Ok((namespace, key, _)) = body.deserialize::<(String, String, OwnedValue)>() else { return false };
|
||||
matches!((namespace.as_str(), key.as_str()), (APPEARANCE, "color-scheme") | (GNOME, "color-scheme" | "gtk-theme"))
|
||||
}
|
||||
|
||||
/// The value inside the variants `Read` wraps it in (a portal answers one or two deep).
|
||||
fn unwrap_variants<'v, 'a>(value: &'v Value<'a>) -> &'v Value<'a> {
|
||||
let mut value = value;
|
||||
for _ in 0..4 {
|
||||
match value {
|
||||
Value::Value(inner) => value = &**inner,
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
value
|
||||
}
|
||||
|
||||
fn as_u32(value: &Value<'_>) -> Option<u32> {
|
||||
match unwrap_variants(value) {
|
||||
Value::U32(n) => Some(*n),
|
||||
Value::I32(n) => u32::try_from(*n).ok(),
|
||||
Value::U8(n) => Some(u32::from(*n)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_string(value: &Value<'_>) -> Option<String> {
|
||||
match unwrap_variants(value) {
|
||||
Value::Str(s) => Some(s.as_str().to_string()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Without a portal: GNOME's settings through `gsettings` (`None` when it is not there).
|
||||
fn read_gsettings() -> Option<bool> {
|
||||
let gnome = gsettings("color-scheme");
|
||||
let gtk_theme = gsettings("gtk-theme");
|
||||
solidcraft_ui_egui::prefs::desktop_prefers_dark(None, gnome.as_deref(), gtk_theme.as_deref())
|
||||
}
|
||||
|
||||
fn gsettings_available() -> bool {
|
||||
gsettings("gtk-theme").is_some()
|
||||
}
|
||||
|
||||
/// `gsettings get org.gnome.desktop.interface <key>`, its quoted answer, or `None` when the
|
||||
/// program is missing, fails or does not answer in time.
|
||||
fn gsettings(key: &str) -> Option<String> {
|
||||
let mut child = std::process::Command::new("gsettings")
|
||||
.args(["get", GNOME, key])
|
||||
.stdin(std::process::Stdio::null())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.spawn()
|
||||
.ok()?;
|
||||
let started = std::time::Instant::now();
|
||||
loop {
|
||||
match child.try_wait() {
|
||||
Ok(Some(status)) if status.success() => break,
|
||||
Ok(None) if started.elapsed() < GSETTINGS_TIMEOUT => std::thread::sleep(Duration::from_millis(20)),
|
||||
_ => {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut out = String::new();
|
||||
child.stdout.take()?.take(4096).read_to_string(&mut out).ok()?;
|
||||
let value = out.trim().trim_matches('\'').to_string();
|
||||
(!value.is_empty()).then_some(value)
|
||||
}
|
||||
@@ -5,25 +5,12 @@
|
||||
//! for recovery as a forced quit does, and starts again ([`restart`]), and the new start says what
|
||||
//! happened ([`note`]). A device lost again soon after such a restart starts the next one on the
|
||||
//! software renderer, which doesn't depend on the graphics driver.
|
||||
//!
|
||||
//! The old process ends without tearing down its GPU objects ([`end`]): on a lost NVIDIA device
|
||||
//! destroying the window's swap chain busy-waits in the driver and never returns (#9044).
|
||||
|
||||
use crate::startup;
|
||||
|
||||
/// Set on the start after a lost device: when the device was lost (seconds since 1970).
|
||||
pub const LOST_ENV: &str = "SOLIDCRAFT_DEVICE_LOST";
|
||||
|
||||
/// Set on the start after a lost device: why it was lost, as wgpu said it. The old process's
|
||||
/// session journal, which recorded it, is gone by then.
|
||||
pub const REASON_ENV: &str = "SOLIDCRAFT_DEVICE_LOST_REASON";
|
||||
|
||||
/// The longest reason passed on ([`REASON_ENV`]) and shown, in characters.
|
||||
const REASON_CHARS: usize = 300;
|
||||
|
||||
/// How long the process may take to end ([`end`]) before it is aborted.
|
||||
const END_GRACE: std::time::Duration = std::time::Duration::from_secs(3);
|
||||
|
||||
/// A loss this recent (seconds) counts for the start after it: its note, and a second loss
|
||||
/// within it starts on the software renderer. Inherited by later starts, an older stamp is
|
||||
/// ignored.
|
||||
@@ -45,54 +32,27 @@ pub fn restart_on_software(after_recent_loss: bool, cpu_renderer: bool) -> bool
|
||||
after_recent_loss || cpu_renderer
|
||||
}
|
||||
|
||||
/// `reason` on one line, at most [`REASON_CHARS`] long; None when there is nothing to say.
|
||||
pub fn reason(reason: Option<&str>) -> Option<String> {
|
||||
let line = reason?.split_whitespace().collect::<Vec<_>>().join(" ");
|
||||
(!line.is_empty()).then(|| line.chars().take(REASON_CHARS).collect())
|
||||
}
|
||||
|
||||
/// What the start after a lost device tells the user, `software` when it runs on the software
|
||||
/// renderer, with what the driver reported ([`REASON_ENV`]).
|
||||
pub fn note(software: bool, reason: Option<&str>) -> String {
|
||||
/// renderer.
|
||||
pub fn note(software: bool) -> String {
|
||||
let mut note = String::from(
|
||||
"The graphics device was reset (the graphics driver stopped responding), so SolidCraft closed and started again. Unsaved changes were kept for recovery.",
|
||||
);
|
||||
if software {
|
||||
note.push_str(" This start uses software rendering, which doesn't depend on the graphics driver.");
|
||||
}
|
||||
if let Some(reason) = self::reason(reason) {
|
||||
note.push_str(&format!(" The graphics driver reported: {reason}"));
|
||||
}
|
||||
note
|
||||
}
|
||||
|
||||
/// The command that starts `exe` again with `args`, the loss stamped at `lost_at` and its
|
||||
/// `reason`, on the software renderer when `software`.
|
||||
fn command(
|
||||
exe: std::path::PathBuf,
|
||||
args: impl IntoIterator<Item = std::ffi::OsString>,
|
||||
software: bool,
|
||||
lost_at: u64,
|
||||
reason: &str,
|
||||
) -> std::process::Command {
|
||||
/// Start SolidCraft again with the same arguments, the loss stamped at `lost_at`, on the software
|
||||
/// renderer when `software`.
|
||||
pub fn restart(software: bool, lost_at: u64) {
|
||||
let Ok(exe) = std::env::current_exe() else { return };
|
||||
let mut command = std::process::Command::new(exe);
|
||||
command.args(args).env(LOST_ENV, lost_at.to_string());
|
||||
match self::reason(Some(reason)) {
|
||||
Some(reason) => command.env(REASON_ENV, reason),
|
||||
// Not one inherited from an earlier loss.
|
||||
None => command.env_remove(REASON_ENV),
|
||||
};
|
||||
command.args(std::env::args_os().skip(1)).env(LOST_ENV, lost_at.to_string());
|
||||
if software {
|
||||
command.env(startup::RENDERER_ENV, "cpu");
|
||||
}
|
||||
command
|
||||
}
|
||||
|
||||
/// Start SolidCraft again with the same arguments, the loss stamped at `lost_at` and its `reason`,
|
||||
/// on the software renderer when `software`.
|
||||
pub fn restart(software: bool, lost_at: u64, reason: &str) {
|
||||
let Ok(exe) = std::env::current_exe() else { return };
|
||||
let mut command = command(exe, std::env::args_os().skip(1), software, lost_at, reason);
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
@@ -104,23 +64,6 @@ pub fn restart(software: bool, lost_at: u64, reason: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
/// End the process now, without tearing down what is left on the lost device: the window's swap
|
||||
/// chain, the viewport's buffers and pipelines. On NVIDIA's Vulkan driver destroying the swap
|
||||
/// chain of a lost device busy-waits forever, which left the old window frozen at 100% of a core
|
||||
/// beside the new one (#9044). Should the exit itself get stuck in a library's exit handler, the
|
||||
/// process is aborted after [`END_GRACE`].
|
||||
pub fn end() -> ! {
|
||||
let watchdog = std::thread::Builder::new().name("solidcraft-exit".into()).spawn(|| {
|
||||
std::thread::sleep(END_GRACE);
|
||||
eprintln!("SolidCraft graphics: still closing after the device was lost; aborting");
|
||||
std::process::abort();
|
||||
});
|
||||
if let Err(e) = watchdog {
|
||||
log::warn!("graphics: no exit watchdog: {e}");
|
||||
}
|
||||
std::process::exit(0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
/// Only a recent, well-formed stamp counts: a later start that inherited it says nothing.
|
||||
@@ -141,34 +84,8 @@ mod tests {
|
||||
assert!(!super::restart_on_software(false, false));
|
||||
assert!(super::restart_on_software(true, false));
|
||||
assert!(super::restart_on_software(false, true));
|
||||
assert!(super::note(true, None).contains("software rendering"));
|
||||
assert!(!super::note(false, None).contains("software rendering"));
|
||||
assert!(super::note(false, None).contains("kept for recovery"));
|
||||
}
|
||||
|
||||
/// The start after a loss is told why (#9044): the old process's journal held the reason, and
|
||||
/// it is deleted as that process closes.
|
||||
#[test]
|
||||
fn the_next_start_is_told_why_the_device_was_lost() {
|
||||
let exe = std::path::PathBuf::from("solidcraft");
|
||||
let args = ["--control".into(), "7979".into()];
|
||||
let command = super::command(exe.clone(), args, true, 1_000, "Lost: Device lost\n (vkQueueSubmit)");
|
||||
let env = |command: &std::process::Command, name: &str| {
|
||||
command.get_envs().find(|(k, _)| *k == name).map(|(_, v)| v.map(|v| v.to_string_lossy().into_owned()))
|
||||
};
|
||||
assert_eq!(env(&command, super::LOST_ENV), Some(Some("1000".into())));
|
||||
assert_eq!(env(&command, super::REASON_ENV), Some(Some("Lost: Device lost (vkQueueSubmit)".into())), "on one line");
|
||||
assert_eq!(env(&command, crate::startup::RENDERER_ENV), Some(Some("cpu".into())));
|
||||
assert_eq!(command.get_args().collect::<Vec<_>>(), ["--control", "7979"]);
|
||||
// No reason: none inherited from an earlier loss either.
|
||||
let command = super::command(exe, Vec::new(), false, 1_000, " ");
|
||||
assert_eq!(env(&command, super::REASON_ENV), Some(None));
|
||||
assert_eq!(env(&command, crate::startup::RENDERER_ENV), None);
|
||||
|
||||
let note = super::note(false, Some("Lost: Device lost"));
|
||||
assert!(note.ends_with("The graphics driver reported: Lost: Device lost"), "{note}");
|
||||
assert!(!super::note(false, Some(" \n ")).contains("reported"));
|
||||
let long = "x".repeat(1_000);
|
||||
assert_eq!(super::reason(Some(&long)).map(|r| r.chars().count()), Some(300));
|
||||
assert!(super::note(true).contains("software rendering"));
|
||||
assert!(!super::note(false).contains("software rendering"));
|
||||
assert!(super::note(false).contains("kept for recovery"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,6 @@
|
||||
mod app_icon;
|
||||
mod come_forward;
|
||||
mod control_server;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod desktop_theme;
|
||||
mod device_lost;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod native_crash;
|
||||
@@ -99,86 +97,6 @@ struct App {
|
||||
nav_bridge: nav_bridge::NavBridge,
|
||||
/// Where the next file dialog opens: it follows the open design (#7756).
|
||||
dialog_folder: DialogFolder,
|
||||
/// Settings go to `app.ron` in the frame they change (#8275).
|
||||
prefs_writer: PrefsWriter,
|
||||
}
|
||||
|
||||
/// Settings written to the settings file soon after they change (#8275). eframe saves them
|
||||
/// only every 30 s and when the window closes normally, so a change was lost whenever the app
|
||||
/// did not get that far: ended from the Task Manager, stopped by a sign-out or shutdown, or
|
||||
/// crashed or hung in the graphics driver on the way out.
|
||||
///
|
||||
/// Some settings change continuously (a panel's width while its edge is dragged), so the file
|
||||
/// is written at most once per [`PrefsWriter::INTERVAL`], and a change made in between is written
|
||||
/// when that interval ends.
|
||||
#[derive(Default)]
|
||||
struct PrefsWriter {
|
||||
/// The settings written (or read at start) last.
|
||||
written: Option<String>,
|
||||
/// The previous frame had input that can change settings: what it changed shows in this
|
||||
/// frame's settings.
|
||||
input_before: bool,
|
||||
/// When the settings were last written by this writer.
|
||||
last_write: Option<std::time::Instant>,
|
||||
/// A change waits until then to be written.
|
||||
pending_until: Option<std::time::Instant>,
|
||||
}
|
||||
|
||||
impl PrefsWriter {
|
||||
/// The least time between two writes of the settings file.
|
||||
const INTERVAL: std::time::Duration = std::time::Duration::from_secs(1);
|
||||
|
||||
/// The settings as they were read at start: they are on disk already.
|
||||
fn read(prefs: String) -> Self {
|
||||
Self { written: Some(prefs), ..Self::default() }
|
||||
}
|
||||
|
||||
/// Whether this frame's settings are worth comparing: input that can change them came in this
|
||||
/// frame or the one before, or a waiting change's time has come. Pointer motion alone isn't
|
||||
/// such input; eframe's own save still catches anything else.
|
||||
fn due(&mut self, input: bool, now: std::time::Instant) -> bool {
|
||||
let due = input || self.input_before || self.pending_until.is_some_and(|at| now >= at);
|
||||
self.input_before = input;
|
||||
due
|
||||
}
|
||||
|
||||
/// Write `prefs` and flush them to disk if they differ from the settings written last and the
|
||||
/// last write is at least [`Self::INTERVAL`] old. Returns how long a change waits otherwise:
|
||||
/// the caller asks for a frame then, so an idle app still writes it.
|
||||
fn store(&mut self, storage: &mut dyn eframe::Storage, prefs: String, now: std::time::Instant) -> Option<std::time::Duration> {
|
||||
if self.written.as_deref() == Some(prefs.as_str()) {
|
||||
self.pending_until = None;
|
||||
return None;
|
||||
}
|
||||
if let Some(wait) = self.last_write.map(|at| Self::INTERVAL.saturating_sub(now.saturating_duration_since(at))).filter(|w| !w.is_zero()) {
|
||||
self.pending_until = Some(now + wait);
|
||||
return Some(wait);
|
||||
}
|
||||
storage.set_string(solidcraft_ui_egui::prefs::STORAGE_KEY, prefs.clone());
|
||||
storage.flush();
|
||||
self.written = Some(prefs);
|
||||
self.last_write = Some(now);
|
||||
self.pending_until = None;
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Input that can change a setting: a click, a key, typed or pasted text, a wheel or a pinch.
|
||||
/// Pointer motion, which comes every frame while the mouse moves, cannot by itself.
|
||||
fn changes_settings(event: &egui::Event) -> bool {
|
||||
matches!(
|
||||
event,
|
||||
egui::Event::PointerButton { .. }
|
||||
| egui::Event::Key { .. }
|
||||
| egui::Event::Text(_)
|
||||
| egui::Event::Paste(_)
|
||||
| egui::Event::Cut
|
||||
| egui::Event::Ime(_)
|
||||
| egui::Event::MouseWheel { .. }
|
||||
| egui::Event::Zoom(_)
|
||||
| egui::Event::Rotate(_)
|
||||
| egui::Event::AccessKitActionRequest(_)
|
||||
)
|
||||
}
|
||||
|
||||
impl eframe::App for App {
|
||||
@@ -266,21 +184,11 @@ impl eframe::App for App {
|
||||
solidcraft_ui_egui::space_mouse::buttons_hook(&mut self.solve, ctx, self.space_mouse.buttons(), raw);
|
||||
self.solve.raw_input_hook(raw);
|
||||
}
|
||||
fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
|
||||
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
||||
content_ui(&mut self.solve, ui);
|
||||
// After the frame's UI, so a setting changed in it (Preferences' OK) is written now.
|
||||
let now = std::time::Instant::now();
|
||||
if self.prefs_writer.due(ui.input(|i| i.events.iter().any(changes_settings)), now)
|
||||
&& let Some(storage) = frame.storage_mut()
|
||||
&& let Some(wait) = self.prefs_writer.store(storage, self.solve.prefs(), now)
|
||||
{
|
||||
ui.ctx().request_repaint_after(wait);
|
||||
}
|
||||
}
|
||||
fn save(&mut self, storage: &mut dyn eframe::Storage) {
|
||||
let prefs = self.solve.prefs();
|
||||
storage.set_string(solidcraft_ui_egui::prefs::STORAGE_KEY, prefs.clone());
|
||||
self.prefs_writer.written = Some(prefs);
|
||||
storage.set_string(solidcraft_ui_egui::prefs::STORAGE_KEY, self.solve.prefs());
|
||||
}
|
||||
fn on_exit(&mut self) {
|
||||
// The agent chat's agent and everything it started end with the app.
|
||||
@@ -294,29 +202,13 @@ impl eframe::App for App {
|
||||
}
|
||||
solidcraft_ui_egui::recovery_ui::close(&mut self.solve);
|
||||
solidcraft_ui_egui::open_elsewhere::close(&mut self.solve);
|
||||
// Before a restart (`Drop for App`) starts the next instance, which would otherwise get
|
||||
// nothing from the 3D mouse driver (`solidcraft_3dx_client::stop`).
|
||||
// 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();
|
||||
// The next start (after a lost device, or into an update) begins once the app is dropped
|
||||
// (`Drop for App`), when the settings are on disk.
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for App {
|
||||
/// eframe drops its settings storage first, which waits for `app.ron` to be written, then the
|
||||
/// app, then the window and its GPU surface. So the next start begins here: started from
|
||||
/// `on_exit`, it read `app.ron` while the old process was still writing it, found it
|
||||
/// unreadable and moved it aside, resetting every setting (#9044).
|
||||
fn drop(&mut self) {
|
||||
if let Some(gpu) = &self.gpu
|
||||
&& let Some(lost_at) = gpu.lost_at
|
||||
{
|
||||
device_lost::restart(gpu.restart_on_software, lost_at, &gpu.reason);
|
||||
// The window's GPU surface is still to be dropped, and on a lost device that may
|
||||
// never return (#9044).
|
||||
device_lost::end();
|
||||
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);
|
||||
}
|
||||
// An update asked to restart into the new version or to run its installer.
|
||||
if let Some((program, args)) = solidcraft_ui_egui::update::take_launch(&mut self.solve) {
|
||||
@@ -344,8 +236,6 @@ struct LostWatch {
|
||||
restart_on_software: bool,
|
||||
/// When the loss was seen: the app is closing to start again.
|
||||
lost_at: Option<u64>,
|
||||
/// Why, as wgpu said it: passed to the next start ([`device_lost::REASON_ENV`]).
|
||||
reason: String,
|
||||
}
|
||||
|
||||
impl App {
|
||||
@@ -359,7 +249,6 @@ impl App {
|
||||
eprintln!("SolidCraft graphics: the device was lost ({reason}); starting again");
|
||||
solidcraft_engine::crash::phase(&format!("graphics device lost: {reason}"));
|
||||
gpu.lost_at = Some(device_lost::now_secs());
|
||||
gpu.reason = reason;
|
||||
self.solve.quit_requested = true;
|
||||
}
|
||||
}
|
||||
@@ -533,12 +422,6 @@ fn services(cc: &eframe::CreationContext<'_>, folder: &DialogFolder) -> Services
|
||||
graphics_adapters: Vec::new(),
|
||||
graphics_path: graphics_path().map(|p| p.to_string_lossy().to_string()),
|
||||
window_system: window_system_now(),
|
||||
// Linux: the desktop's dark or light setting, which the window is not told on every
|
||||
// desktop (#7944). Windows and macOS: the window's own theme.
|
||||
#[cfg(target_os = "linux")]
|
||||
desktop_dark: Some(desktop_theme::start(&cc.egui_ctx)),
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
desktop_dark: None,
|
||||
http_get: Some(std::sync::Arc::new(http_get)),
|
||||
http_download: Some(std::sync::Arc::new(http_download)),
|
||||
// Already an installed app: only the browser build offers to install.
|
||||
@@ -818,24 +701,6 @@ fn msaa() -> Option<u16> {
|
||||
/// (`solidcraft_ui_egui::titlebar`). macOS keeps its traffic lights over the integrated bar.
|
||||
const CUSTOM_TITLEBAR: bool = !cfg!(target_os = "macos");
|
||||
|
||||
/// Linux: the window manager's own decorations (server-side, or the compositor's frame) can be
|
||||
/// asked for instead of the application bar ([`titlebar_choice`]). Elsewhere always `true`.
|
||||
fn app_titlebar_wanted() -> bool {
|
||||
if !cfg!(all(unix, not(target_os = "macos"))) {
|
||||
return true;
|
||||
}
|
||||
let setting = graphics().and_then(|v| v.get("titlebar").and_then(serde_json::Value::as_str).map(str::to_string));
|
||||
titlebar_choice(solidcraft_engine::geom::env::var("TITLEBAR").as_deref(), setting.as_deref())
|
||||
}
|
||||
|
||||
/// Linux: whether the application bar is the title bar. `SOLIDCRAFT_TITLEBAR` (else
|
||||
/// `graphics.json`'s `titlebar`) set to `system` gives the window the desktop's own title bar and
|
||||
/// buttons; `app` (or anything else, or nothing) keeps the application bar as the title bar.
|
||||
fn titlebar_choice(env: Option<&str>, setting: Option<&str>) -> bool {
|
||||
let choice = env.map(str::trim).filter(|v| !v.is_empty()).or(setting.map(str::trim));
|
||||
!choice.is_some_and(|v| v.eq_ignore_ascii_case("system"))
|
||||
}
|
||||
|
||||
const DEPTH_BITS: u8 = 24;
|
||||
/// A 32-bit float depth buffer, drawn with reversed depth (1 near, 0 far) so its precision follows
|
||||
/// the distance from the eye (#1434). Only where every adapter offers it with a stencil
|
||||
@@ -885,18 +750,6 @@ fn main() -> eframe::Result {
|
||||
#[cfg(target_os = "macos")]
|
||||
objc2_foundation::NSProcessInfo::processInfo().setProcessName(objc2_foundation::ns_string!("SolidCraft"));
|
||||
mark("main");
|
||||
// On OCLP-patched Macs, wgpu picks a Metal Shading Language standard that the system's
|
||||
// compiler refuses (`invalid value 'metal3.2'`), because the patched OS reports a newer
|
||||
// macOS version than the GPU's Metal compiler supports. Re-launching with
|
||||
// SYSTEM_VERSION_COMPAT=1 makes the OS report the real (older) version so wgpu selects
|
||||
// a standard the compiler accepts. The variable must be set before the process starts, so
|
||||
// we detect OCLP here and re-exec immediately, before any macOS framework call (#3080, #4605).
|
||||
#[cfg(target_os = "macos")]
|
||||
if let Err(e) = startup::relaunch_with_version_compat() {
|
||||
log::warn!("startup: OCLP check failed (continuing normally): {e}");
|
||||
}
|
||||
// SOLIDCRAFT_PROFILE=1: the profiler records from the start (diagnostics.profile reads it).
|
||||
solidcraft_engine::timing::profile_from_env();
|
||||
// Validate before legacy adoption, crash markers or any user preference read.
|
||||
let settings = settings_dir().map_err(|error| eframe::Error::AppCreation(error.into()))?;
|
||||
if std::env::var_os("SOLIDCRAFT_SETTINGS_DIR").is_none() {
|
||||
@@ -968,11 +821,7 @@ fn main() -> eframe::Result {
|
||||
// The launch before this one closed because its graphics device was lost (`App::watch_device`).
|
||||
let after_loss = device_lost::recent(std::env::var(device_lost::LOST_ENV).ok().as_deref(), device_lost::now_secs());
|
||||
if after_loss {
|
||||
let reason = std::env::var(device_lost::REASON_ENV).ok();
|
||||
if let Some(reason) = device_lost::reason(reason.as_deref()) {
|
||||
log::warn!("graphics: the last launch lost its device ({reason})");
|
||||
}
|
||||
notes.push(device_lost::note(cpu_renderer, reason.as_deref()));
|
||||
notes.push(device_lost::note(cpu_renderer));
|
||||
}
|
||||
if safe_mode {
|
||||
notes.push("Safe mode: default settings (yours are kept for the next normal start), no recovery offer, software rendering.".into());
|
||||
@@ -1008,7 +857,7 @@ fn main() -> eframe::Result {
|
||||
eframe::egui_wgpu::WgpuSetup::CreateNew(setup) => setup.instance_descriptor.backends,
|
||||
_ => eframe::wgpu::Backends::all(),
|
||||
};
|
||||
let custom_titlebar = CUSTOM_TITLEBAR && app_titlebar_wanted() && !startup::system_title_bar(cfg!(target_os = "windows"), started);
|
||||
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 {
|
||||
@@ -1112,7 +961,6 @@ fn main() -> eframe::Result {
|
||||
lost: rs.device_lost.clone(),
|
||||
restart_on_software: device_lost::restart_on_software(after_loss, cpu_renderer),
|
||||
lost_at: None,
|
||||
reason: String::new(),
|
||||
});
|
||||
if !safe_mode {
|
||||
app.preferences.graphics_adapter =
|
||||
@@ -1128,12 +976,6 @@ fn main() -> eframe::Result {
|
||||
})
|
||||
.collect();
|
||||
let info = rs.adapter.get_info();
|
||||
if !app.preferences.graphics_adapter.is_empty() && !info.name.eq_ignore_ascii_case(app.preferences.graphics_adapter.as_str()) {
|
||||
eprintln!(
|
||||
"SolidCraft graphics: preferred adapter {:?} was not used; using {:?} instead",
|
||||
app.preferences.graphics_adapter, info.name
|
||||
);
|
||||
}
|
||||
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)
|
||||
@@ -1174,10 +1016,6 @@ fn main() -> eframe::Result {
|
||||
app.session.doc_mut().name = "Sample Plate".into();
|
||||
app.session.mark_saved();
|
||||
app.session.undo.clear();
|
||||
// Built straight into the session, so nothing asked for a fit: frame it at the
|
||||
// home view as any opened design is (#9195). The fit waits for the first layout.
|
||||
app.cam.set_view(solidcraft_engine::render::StandardView::Iso);
|
||||
app.fit_view();
|
||||
}
|
||||
// Lock files next to open designs: a design open on another computer says so (#5467),
|
||||
// the files given at launch included.
|
||||
@@ -1238,8 +1076,6 @@ fn main() -> eframe::Result {
|
||||
mark("app created");
|
||||
solidcraft_engine::crash::phase("first frame");
|
||||
Ok(Box::new(App {
|
||||
// Before `app` moves: the settings as read, already on disk.
|
||||
prefs_writer: PrefsWriter::read(app.prefs()),
|
||||
solve: app,
|
||||
starting,
|
||||
frames: 0,
|
||||
@@ -1263,13 +1099,12 @@ fn main() -> eframe::Result {
|
||||
}
|
||||
|
||||
/// egui-wgpu is about to start a step of getting the graphics device and the window's surface up:
|
||||
/// the crash journal keeps it until the app runs (a window opened later goes through the same
|
||||
/// steps), and the adapter once it is picked.
|
||||
/// the crash journal keeps it, and the adapter once it is picked.
|
||||
fn startup_step(step: &'static str, detail: &str) {
|
||||
if step == eframe::egui_wgpu::STEP_ADAPTER_CHOSEN {
|
||||
solidcraft_engine::crash::set_environment(&format!("Renderer: {detail}"));
|
||||
}
|
||||
solidcraft_engine::crash::startup_step(step);
|
||||
solidcraft_engine::crash::phase(step);
|
||||
}
|
||||
|
||||
/// The renderer settings: with `cpu`, the software adapter (WARP, lavapipe, llvmpipe). The
|
||||
@@ -1431,148 +1266,6 @@ mod backend_tests {
|
||||
assert!(!prefer_x11(true, true, Some("GNOME"), over(Some("wayland"))));
|
||||
assert!(prefer_x11(true, true, Some("niri"), over(Some("x11"))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_desktops_title_bar_only_when_asked_for() {
|
||||
use super::titlebar_choice;
|
||||
// Nothing set: the application bar is the title bar, as before.
|
||||
assert!(titlebar_choice(None, None));
|
||||
// graphics.json's `titlebar`.
|
||||
assert!(!titlebar_choice(None, Some("system")));
|
||||
assert!(titlebar_choice(None, Some("app")));
|
||||
// SOLIDCRAFT_TITLEBAR wins over the file; empty counts as unset; case and spaces don't matter.
|
||||
assert!(!titlebar_choice(Some(" System "), Some("app")));
|
||||
assert!(titlebar_choice(Some("app"), Some("system")));
|
||||
assert!(!titlebar_choice(Some(""), Some("system")));
|
||||
// Anything unknown keeps the application bar.
|
||||
assert!(titlebar_choice(Some("ssd"), None));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod prefs_writer_tests {
|
||||
use super::PrefsWriter;
|
||||
use solidcraft_ui_egui::prefs::{NavigatorStyle, STORAGE_KEY};
|
||||
use std::collections::HashMap;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// eframe's settings file: what was flushed to disk is what the next start reads.
|
||||
#[derive(Default)]
|
||||
struct Disk {
|
||||
pending: HashMap<String, String>,
|
||||
flushed: HashMap<String, String>,
|
||||
flushes: usize,
|
||||
}
|
||||
|
||||
impl eframe::Storage for Disk {
|
||||
fn get_string(&self, key: &str) -> Option<String> {
|
||||
self.pending.get(key).cloned()
|
||||
}
|
||||
fn set_string(&mut self, key: &str, value: String) {
|
||||
self.pending.insert(key.into(), value);
|
||||
}
|
||||
fn remove_string(&mut self, key: &str) {
|
||||
self.pending.remove(key);
|
||||
}
|
||||
fn flush(&mut self) {
|
||||
self.flushed = self.pending.clone();
|
||||
self.flushes += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn new_app() -> solidcraft_ui_egui::SolveApp {
|
||||
solidcraft_ui_egui::SolveApp::new(solidcraft_engine::Session::default(), solidcraft_ui_egui::Services::default())
|
||||
}
|
||||
|
||||
/// #8275: Preferences' View navigator and Display quality, changed and confirmed, are on disk
|
||||
/// in that frame, so the next start has them even when this one never closes normally (ended
|
||||
/// from the Task Manager, a sign-out, a hang or crash in the graphics driver on the way out).
|
||||
#[test]
|
||||
fn changed_settings_reach_the_disk_in_the_frame_they_change() {
|
||||
let mut app = new_app();
|
||||
let mut disk = Disk::default();
|
||||
let mut writer = PrefsWriter::read(app.prefs());
|
||||
let now = Instant::now();
|
||||
assert_eq!(writer.store(&mut disk, app.prefs(), now), None);
|
||||
assert_eq!(disk.flushes, 0, "the settings read at start are not written again");
|
||||
app.preferences.navigator_style = NavigatorStyle::Cube;
|
||||
app.preferences.display_quality = "fine".into();
|
||||
assert!(writer.due(true, now), "a frame with input is checked");
|
||||
assert_eq!(writer.store(&mut disk, app.prefs(), now), None);
|
||||
assert_eq!(disk.flushes, 1, "flushed at once, not left for eframe's next save");
|
||||
assert_eq!(writer.store(&mut disk, app.prefs(), now + Duration::from_secs(5)), None);
|
||||
assert_eq!(disk.flushes, 1, "unchanged settings are not written again");
|
||||
|
||||
// The next start reads them.
|
||||
let mut next = new_app();
|
||||
let stored = solidcraft_ui_egui::prefs::stored(|k| disk.flushed.get(k).cloned()).expect("settings on disk");
|
||||
next.load_prefs(&stored);
|
||||
assert_eq!(next.preferences.navigator_style, NavigatorStyle::Cube);
|
||||
assert_eq!(next.preferences.display_quality, "fine");
|
||||
assert!(disk.flushed.contains_key(STORAGE_KEY));
|
||||
}
|
||||
|
||||
/// A frame without input is checked only right after one with input (whose clicks the UI
|
||||
/// handled in that frame); after that, idle frames cost nothing. Pointer motion, which comes
|
||||
/// every frame while the mouse moves, doesn't count as input.
|
||||
#[test]
|
||||
fn frames_are_checked_with_input_and_once_after_it() {
|
||||
let now = Instant::now();
|
||||
let mut writer = PrefsWriter::default();
|
||||
assert!(!writer.due(false, now));
|
||||
assert!(writer.due(true, now));
|
||||
assert!(writer.due(false, now), "the frame after input");
|
||||
assert!(!writer.due(false, now));
|
||||
|
||||
let click = egui::Event::PointerButton {
|
||||
pos: egui::Pos2::ZERO,
|
||||
button: egui::PointerButton::Primary,
|
||||
pressed: false,
|
||||
modifiers: egui::Modifiers::NONE,
|
||||
};
|
||||
assert!(super::changes_settings(&click));
|
||||
assert!(super::changes_settings(&egui::Event::Text("5".into())));
|
||||
assert!(!super::changes_settings(&egui::Event::PointerMoved(egui::Pos2::ZERO)));
|
||||
assert!(!super::changes_settings(&egui::Event::MouseMoved(egui::Vec2::X)));
|
||||
}
|
||||
|
||||
/// A setting that changes in every frame (a panel's width while its edge is dragged) doesn't
|
||||
/// write the file in every frame: at most once a second, and the last change is on disk at
|
||||
/// most a second after it was made, with a frame asked for then so an idle app writes it.
|
||||
#[test]
|
||||
fn a_continuous_change_is_written_once_a_second_and_its_end_soon_after() {
|
||||
let mut app = new_app();
|
||||
let mut disk = Disk::default();
|
||||
let mut writer = PrefsWriter::read(app.prefs());
|
||||
let start = Instant::now();
|
||||
let frame = Duration::from_millis(16);
|
||||
let mut asked = None;
|
||||
// 1.5 s of dragging at 60 frames a second, with input in every frame.
|
||||
for n in 0..94u32 {
|
||||
let now = start + frame * n;
|
||||
app.preferences.browser_width = 200.0 + n as f32;
|
||||
assert!(writer.due(true, now));
|
||||
asked = writer.store(&mut disk, app.prefs(), now);
|
||||
}
|
||||
assert_eq!(disk.flushes, 2, "the first change at once, then one a second later");
|
||||
let wait = asked.expect("the last width waits, and a frame is asked for when it can be written");
|
||||
assert!(wait <= PrefsWriter::INTERVAL);
|
||||
|
||||
// The drag ends: no more input. Frames before the wait is over don't even look.
|
||||
let end = start + frame * 93;
|
||||
assert!(writer.due(false, end + frame), "the frame right after input");
|
||||
assert!(!writer.due(false, end + frame * 2), "an idle frame before the wait is over");
|
||||
// The frame asked for writes the last width.
|
||||
let then = end + wait;
|
||||
assert!(writer.due(false, then));
|
||||
assert_eq!(writer.store(&mut disk, app.prefs(), then), None);
|
||||
assert_eq!(disk.flushes, 3);
|
||||
let mut next = new_app();
|
||||
let stored = solidcraft_ui_egui::prefs::stored(|k| disk.flushed.get(k).cloned()).expect("settings on disk");
|
||||
next.load_prefs(&stored);
|
||||
assert_eq!(next.preferences.browser_width, 293.0, "the width the drag ended at");
|
||||
assert!(!writer.due(false, then + frame), "nothing waits any more");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -101,7 +101,6 @@ impl NativeMenu {
|
||||
])?;
|
||||
for (action, label) in [
|
||||
("docs", "Documentation"),
|
||||
("updates", app.tr("update.check_for_updates")),
|
||||
("shortcuts", "Keyboard Shortcuts"),
|
||||
("messages", "Messages\u{2026}"),
|
||||
("report", "Report an Issue\u{2026}"),
|
||||
|
||||
@@ -137,11 +137,8 @@ pub struct AdapterChoice {
|
||||
pub compatible: bool,
|
||||
}
|
||||
|
||||
/// Choose an adapter by name. The environment override follows wgpu's case-insensitive
|
||||
/// substring convention among surface-compatible adapters. A saved preference prefers an
|
||||
/// exact (then case-insensitive) name among compatible adapters, and if the named card is
|
||||
/// only listed as incompatible with the surface still picks it (#8341: otherwise the
|
||||
/// preference is kept in the dialog while wgpu silently uses another GPU).
|
||||
/// 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.
|
||||
@@ -152,14 +149,12 @@ pub fn choose_adapter(adapters: &[AdapterChoice], environment: Option<&str>, pre
|
||||
return Some(index);
|
||||
}
|
||||
}
|
||||
let name = preference.filter(|s| !s.is_empty())?;
|
||||
let find = |pred: &dyn Fn(&AdapterChoice) -> bool, only_compatible: bool| {
|
||||
adapters.iter().enumerate().find(|(_, a)| (!only_compatible || a.compatible) && pred(a)).map(|(i, _)| i)
|
||||
};
|
||||
find(&|a| a.name == name, true)
|
||||
.or_else(|| find(&|a| a.name.eq_ignore_ascii_case(name), true))
|
||||
.or_else(|| find(&|a| a.name == name, false))
|
||||
.or_else(|| find(&|a| a.name.eq_ignore_ascii_case(name), false))
|
||||
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.
|
||||
@@ -297,71 +292,6 @@ pub fn window_instance_flags(flags: eframe::wgpu::InstanceFlags, intel_mac: bool
|
||||
if intel_mac && env.map(str::trim).is_none_or(str::is_empty) { flags - eframe::wgpu::InstanceFlags::VALIDATION_INDIRECT_CALL } else { flags }
|
||||
}
|
||||
|
||||
/// The environment variable that makes the macOS compatibility shim report an older OS
|
||||
/// version to wgpu's Metal backend. On OCLP-patched machines the Metal compiler accepts
|
||||
/// only up to the MSL standard of the original GPU, not the one the patched OS reports,
|
||||
/// so wgpu picks a standard that the compiler refuses (`invalid value 'metal3.2'`).
|
||||
/// Setting this variable before the process starts causes the OS to report the older
|
||||
/// version and wgpu selects a supported MSL standard (#3080, #4605).
|
||||
#[cfg(target_os = "macos")]
|
||||
pub const SYSTEM_VERSION_COMPAT_ENV: &str = "SYSTEM_VERSION_COMPAT";
|
||||
|
||||
/// The NVRAM variable that OpenCore Legacy Patcher writes on every OCLP-booted system.
|
||||
/// Its presence is the primary signal that `SYSTEM_VERSION_COMPAT=1` is needed.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub const OCLP_NVRAM_KEY: &str = "4D1FDA02-38C7-4A6A-9CC6-4BCCA8B30102:OCLP-Version";
|
||||
|
||||
/// `true` if the OCLP NVRAM key is present on this system.
|
||||
///
|
||||
/// Runs `nvram <key>` and treats exit 0 as present. Any error (missing binary,
|
||||
/// permission denied, key absent) returns `false` — this is a best-effort probe.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn oclp_nvram_present() -> bool {
|
||||
std::process::Command::new("nvram").arg(OCLP_NVRAM_KEY).output().map(|o| o.status.success()).unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Whether to re-launch with `SYSTEM_VERSION_COMPAT=1`: it is not set yet and OCLP shows through
|
||||
/// the NVRAM key. The probe runs only when it is needed, so a re-launched process (variable
|
||||
/// set) never starts `nvram`.
|
||||
#[cfg(target_os = "macos")]
|
||||
fn compat_wanted(compat_set: bool, nvram: impl FnOnce() -> bool) -> bool {
|
||||
!compat_set && nvram()
|
||||
}
|
||||
|
||||
/// If this process is running under OCLP and `SYSTEM_VERSION_COMPAT` has not been set,
|
||||
/// re-launch the current binary with `SYSTEM_VERSION_COMPAT=1` added to the environment,
|
||||
/// then exit with the child's status code.
|
||||
///
|
||||
/// `SYSTEM_VERSION_COMPAT=1` must be present **before** the process starts: setting it
|
||||
/// with `std::env::set_var` inside `main` is too late because the macOS frameworks have
|
||||
/// already queried the OS version by then. Re-launching is the only safe approach that
|
||||
/// works without `unsafe` (a true `execve` replacement would require it).
|
||||
///
|
||||
/// Returns `Ok(false)` immediately when:
|
||||
/// - `SYSTEM_VERSION_COMPAT` is already set (re-launched process, no loop), or
|
||||
/// - the OCLP NVRAM key is absent.
|
||||
///
|
||||
/// Returns `Err` only when `std::env::current_exe()` fails or the child process cannot
|
||||
/// be spawned; the caller should log the error and continue normally.
|
||||
///
|
||||
/// Never returns `Ok(true)`: a successful re-launch exits the current process.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn relaunch_with_version_compat() -> Result<bool, String> {
|
||||
// A set variable means this is the re-launched process: no loop.
|
||||
if !compat_wanted(std::env::var_os(SYSTEM_VERSION_COMPAT_ENV).is_some(), oclp_nvram_present) {
|
||||
return Ok(false);
|
||||
}
|
||||
log::info!("startup: OCLP detected; re-launching with {}=1", SYSTEM_VERSION_COMPAT_ENV);
|
||||
eprintln!("SolidCraft: OCLP detected; re-launching with {}=1", SYSTEM_VERSION_COMPAT_ENV);
|
||||
let exe = std::env::current_exe().map_err(|e| format!("could not find own executable: {e}"))?;
|
||||
let status = std::process::Command::new(&exe)
|
||||
.args(std::env::args_os().skip(1))
|
||||
.env(SYSTEM_VERSION_COMPAT_ENV, "1")
|
||||
.status()
|
||||
.map_err(|e| format!("could not re-launch with {}: {e}", SYSTEM_VERSION_COMPAT_ENV))?;
|
||||
std::process::exit(status.code().unwrap_or(1));
|
||||
}
|
||||
|
||||
/// 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
|
||||
@@ -421,8 +351,7 @@ mod tests {
|
||||
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 small")), Some(1), "preference matches without regard to case");
|
||||
assert_eq!(choose_adapter(&cards, None, Some("Discrete")), None, "a preference is the full name, not a substring");
|
||||
assert_eq!(choose_adapter(&cards, None, Some("Discrete")), None, "saved names are exact");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -431,20 +360,11 @@ mod tests {
|
||||
assert_eq!(choose_adapter(&cards, None, None), None);
|
||||
assert_eq!(choose_adapter(&cards, Some(""), Some("")), None);
|
||||
cards[2].compatible = false;
|
||||
// Environment still needs a compatible adapter; a preference keeps the named card (#8341).
|
||||
assert_eq!(choose_adapter(&cards, Some("large"), Some("Discrete large")), Some(2));
|
||||
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 preference_keeps_a_named_card_even_when_surface_says_incompatible() {
|
||||
let mut cards = cards();
|
||||
cards[1].compatible = false;
|
||||
assert_eq!(choose_adapter(&cards, None, Some("Discrete small")), Some(1));
|
||||
assert_eq!(choose_adapter(&cards, None, Some("discrete SMALL")), Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn automatic_adapter_preserves_the_previous_request_configuration() {
|
||||
use eframe::egui_wgpu::WgpuSetupCreateNew;
|
||||
@@ -494,27 +414,6 @@ mod tests {
|
||||
assert_eq!(window_instance_flags(on, false, None), on);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn oclp_relaunch_needs_the_signal_and_no_compat_variable() {
|
||||
use super::compat_wanted;
|
||||
let probed = std::cell::Cell::new(false);
|
||||
let probe = |found: bool| {
|
||||
let probed = &probed;
|
||||
move || {
|
||||
probed.set(true);
|
||||
found
|
||||
}
|
||||
};
|
||||
// The re-launched process has the variable: no probe, no second re-launch.
|
||||
assert!(!compat_wanted(true, probe(true)));
|
||||
assert!(!probed.get());
|
||||
// The OCLP NVRAM key is the signal.
|
||||
assert!(compat_wanted(false, probe(true)));
|
||||
// No signal: an ordinary Mac starts as it is.
|
||||
assert!(!compat_wanted(false, probe(false)));
|
||||
}
|
||||
|
||||
#[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")));
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
Copyright 2014-2021 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'.
|
||||
Copyright 2014-2021 Google Inc. with Reserved Font Name 'Noto'.
|
||||
Subset for SolidCraft web UI (glyphs used by the zh locale); renamed Craft UI SC (no Reserved Font Name).
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License,
|
||||
Version 1.1.
|
||||
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font
|
||||
creation efforts of academic and linguistic communities, and to
|
||||
provide a free and open framework in which fonts may be shared and
|
||||
improved in partnership with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply to
|
||||
any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software
|
||||
components as distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to,
|
||||
deleting, or substituting -- in part or in whole -- any of the
|
||||
components of the Original Version, by changing formats or by porting
|
||||
the Font Software to a new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed,
|
||||
modify, redistribute, and sell modified and unmodified copies of the
|
||||
Font Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components, in
|
||||
Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the
|
||||
corresponding Copyright Holder. This restriction only applies to the
|
||||
primary font name as presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created using
|
||||
the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -1,96 +0,0 @@
|
||||
Copyright 2014-2021 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'.
|
||||
Copyright 2014-2021 Google Inc. with Reserved Font Name 'Noto'.
|
||||
Subset for SolidCraft web UI (glyphs used by the zh-TW locale); renamed Craft UI TC (no Reserved Font Name).
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License,
|
||||
Version 1.1.
|
||||
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font
|
||||
creation efforts of academic and linguistic communities, and to
|
||||
provide a free and open framework in which fonts may be shared and
|
||||
improved in partnership with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply to
|
||||
any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software
|
||||
components as distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to,
|
||||
deleting, or substituting -- in part or in whole -- any of the
|
||||
components of the Original Version, by changing formats or by porting
|
||||
the Font Software to a new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed,
|
||||
modify, redistribute, and sell modified and unmodified copies of the
|
||||
Font Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components, in
|
||||
Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the
|
||||
corresponding Copyright Holder. This restriction only applies to the
|
||||
primary font name as presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created using
|
||||
the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -188,7 +188,7 @@ surface = [
|
||||
sketch = [
|
||||
"arc3", "arc_center", "arc_slot", "arc_slot_center", "arc_tangent",
|
||||
"blend", "circle", "circle2", "circle3", "circle_tt",
|
||||
"circle_ttt", "conic", "constrainer", "dimension", "ellipse", "equation_curve",
|
||||
"circle_ttt", "conic", "constrainer", "dimension", "ellipse",
|
||||
"extend", "finish", "fit_section", "include",
|
||||
"intersect", "intersection_curve", "iso_curve", "line", "line_mid",
|
||||
"offset", "offset_two_sides", "offset_symmetric", "point", "polygon", "polygon_circumscribed", "polygon_edge",
|
||||
@@ -204,10 +204,7 @@ constraint = [
|
||||
"c_polygon", "c_smooth", "c_symmetry", "c_tangent",
|
||||
]
|
||||
construction = [
|
||||
"centerline", "construction", "construction_line", "construction_line_angle",
|
||||
"construction_line_bisector", "construction_line_horizontal", "construction_line_parallel",
|
||||
"construction_line_perpendicular", "construction_line_tangent_at", "construction_line_tangent_external",
|
||||
"construction_line_tangent_two", "construction_line_vertical", "infinite",
|
||||
"centerline", "construction", "infinite",
|
||||
]
|
||||
mesh = [
|
||||
"mesh_body",
|
||||
|
||||
@@ -70,16 +70,6 @@ configuration_table false
|
||||
conic false
|
||||
constrainer false
|
||||
construction false
|
||||
construction_line false
|
||||
construction_line_angle false
|
||||
construction_line_bisector false
|
||||
construction_line_horizontal false
|
||||
construction_line_parallel false
|
||||
construction_line_perpendicular false
|
||||
construction_line_tangent_at false
|
||||
construction_line_tangent_external false
|
||||
construction_line_tangent_two false
|
||||
construction_line_vertical false
|
||||
contact_all false
|
||||
contact_new false
|
||||
contact_off false
|
||||
@@ -105,7 +95,6 @@ edit false
|
||||
ellipse false
|
||||
emboss false
|
||||
environment_map false
|
||||
equation_curve false
|
||||
error false
|
||||
explode false
|
||||
export false
|
||||
@@ -181,7 +170,6 @@ pipe false
|
||||
plane false
|
||||
plane_along_path false
|
||||
plane_angle false
|
||||
plane_axis_point false
|
||||
plane_midplane false
|
||||
plane_perpendicular false
|
||||
plane_tangent false
|
||||
@@ -202,7 +190,6 @@ polygon false
|
||||
polygon_circumscribed false
|
||||
polygon_edge false
|
||||
presspull false
|
||||
print_face false
|
||||
project false
|
||||
project_surface false
|
||||
rack_pinion false
|
||||
@@ -237,7 +224,6 @@ settings_monitor false
|
||||
settings_mouse false
|
||||
settings_palette false
|
||||
settings_plug false
|
||||
settings_profile false
|
||||
sheet false
|
||||
sheet_rules false
|
||||
shell false
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Settings profile</title>
|
||||
<circle cx="32" cy="19" r="8" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12 53C12 44.7157 20.9543 38 32 38C43.0457 38 52 44.7157 52 53" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 413 B |
@@ -1,8 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Construction line through two points</title>
|
||||
<path d="M8 52L56 12" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round" stroke-dasharray="10 5"/>
|
||||
<circle cx="20" cy="42" r="5.8" fill="#ffffff"/>
|
||||
<circle cx="20" cy="42" r="3.4" fill="#236ead"/>
|
||||
<circle cx="44" cy="22" r="5.8" fill="#ffffff"/>
|
||||
<circle cx="44" cy="22" r="3.4" fill="#236ead"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 472 B |
@@ -1,8 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Construction line at an angle</title>
|
||||
<path d="M10.26 48.66L58.26 48.66" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M28.65 33.23A24 24 0 0 1 34.26 48.66" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M10.26 48.66L54.26 11.66" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<circle cx="10.26" cy="48.66" r="5.8" fill="#ffffff"/>
|
||||
<circle cx="10.26" cy="48.66" r="3.4" fill="#236ead"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 646 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Angle bisector</title>
|
||||
<path d="M13.92 12.18L9.92 52.17L54.92 44.17" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M12.11 30.29A22 22 0 0 1 31.58 48.33" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M9.92 52.17L47.3 11.83" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 526 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Horizontal construction line</title>
|
||||
<path d="M8 32L56 32" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round" stroke-dasharray="10 5"/>
|
||||
<circle cx="32" cy="32" r="5.8" fill="#ffffff"/>
|
||||
<circle cx="32" cy="32" r="3.4" fill="#236ead"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 362 B |
@@ -1,5 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Parallel construction line</title>
|
||||
<path d="M9 37L41 9" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M23 55L55 27" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round" stroke-dasharray="10 5"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 380 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Perpendicular construction line</title>
|
||||
<path d="M8 44L56 44" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M32 8L32 56" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M32 34L42 34L42 44" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 490 B |
@@ -1,7 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Tangent at a curve point</title>
|
||||
<circle cx="32" cy="34.26" r="17" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M8 17.26L56 17.26" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<circle cx="32" cy="17.26" r="5.8" fill="#ffffff"/>
|
||||
<circle cx="32" cy="17.26" r="3.4" fill="#236ead"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 480 B |
@@ -1,7 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Tangent through an external point</title>
|
||||
<circle cx="21.74" cy="38.77" r="14" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M53.6 8.23L28.55 55.9" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<circle cx="51.74" cy="11.77" r="5.8" fill="#ffffff"/>
|
||||
<circle cx="51.74" cy="11.77" r="3.4" fill="#236ead"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 502 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Common tangent to two curves</title>
|
||||
<circle cx="20" cy="33.59" r="12" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<circle cx="46" cy="33.59" r="8" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M6 19.27L58 27.36" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 509 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Vertical construction line</title>
|
||||
<path d="M32 8L32 56" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round" stroke-dasharray="10 5"/>
|
||||
<circle cx="32" cy="32" r="5.8" fill="#ffffff"/>
|
||||
<circle cx="32" cy="32" r="3.4" fill="#236ead"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 360 B |
@@ -1,9 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Equation curve</title>
|
||||
<path d="M12 8L12 56M6 32L58 32" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M12 32C15.42 24.83 18.72 18 22.5 18C26.28 18 29.58 24.83 33 32C36.42 39.17 39.72 46 43.5 46C47.28 46 50.58 39.17 54 32" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<circle cx="12" cy="32" r="5.8" fill="#ffffff"/>
|
||||
<circle cx="12" cy="32" r="3.4" fill="#236ead"/>
|
||||
<circle cx="54" cy="32" r="5.8" fill="#ffffff"/>
|
||||
<circle cx="54" cy="32" r="3.4" fill="#236ead"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 666 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Plane through axis and point</title>
|
||||
<path d="M26 15L56 32.32L38 42.71L8 25.39Z" fill="#b9ddf8" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round" stroke-dasharray="0.1 5"/>
|
||||
<path d="M15 18.4L53 40.34" fill="none" stroke="currentColor" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<circle cx="22" cy="29" r="3.1" fill="currentColor"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 475 B |
@@ -1,19 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Print Face</title>
|
||||
<path d="M9.72 26.17L4.5 20.95L4.5 40.85L9.72 46.07Z" fill="#b8c3cf"/>
|
||||
<path d="M9.72 26.17L4.5 20.95L4.5 40.85L9.72 46.07Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M18.77 23.15L9.72 26.17L9.72 46.07L18.77 43.05Z" fill="#328bd3"/>
|
||||
<path d="M18.77 23.15L9.72 26.17L9.72 46.07L18.77 43.05Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M13.55 17.93L18.77 23.15L9.72 26.17L4.5 20.95Z" fill="#e2e8ef"/>
|
||||
<path d="M13.55 17.93L18.77 23.15L9.72 26.17L4.5 20.95Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M50.45 37.54L38.27 25.35L38.27 33.88L50.45 46.07Z" fill="#b8c3cf"/>
|
||||
<path d="M50.45 37.54L38.27 25.35L38.27 33.88L50.45 46.07Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M59.5 34.53L50.45 37.54L50.45 46.07L59.5 43.05Z" fill="#8e9faf"/>
|
||||
<path d="M59.5 34.53L50.45 37.54L50.45 46.07L59.5 43.05Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M47.31 22.34L59.5 34.53L50.45 37.54L38.27 25.35Z" fill="#e2e8ef"/>
|
||||
<path d="M47.31 22.34L59.5 34.53L50.45 37.54L38.27 25.35Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M50.45 46.07L38.27 33.88" fill="none" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M59.5 43.05L50.45 46.07" fill="none" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M24.17 34.2L28.31 34.2" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M35.31 34.2L27.31 38.7L27.31 29.7Z" fill="#236ead"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -1,42 +1,22 @@
|
||||
{
|
||||
"A Single hole snaps to the middle of its face and to the centres of its round edges": "A Single hole snaps to the middle of its face and to the centres of its round edges",
|
||||
"Base Center": "Base Centre",
|
||||
"Center X": "Centre X",
|
||||
"Center Y": "Centre Y",
|
||||
"Center of mass": "Centre of mass",
|
||||
"Centre to centre": "Centre to centre",
|
||||
"Circle center": "Circle centre",
|
||||
"Colour": "Colour",
|
||||
"Copy all licences": "Copy all licences",
|
||||
"Customize Toolbar": "Customise Toolbar",
|
||||
"Customize…": "Customise…",
|
||||
"Face center": "Face centre",
|
||||
"Favorites": "Favourites",
|
||||
"Favourite": "Favourite",
|
||||
"Mass Center": "Mass Centre",
|
||||
"Modeled": "Modelled",
|
||||
"Select the centre the pattern turns around first": "Select the centre the pattern turns around first",
|
||||
"Snap to face and edge centres": "Snap to face and edge centres",
|
||||
"Specialise": "Specialise",
|
||||
"cmd.appearance_favorite": "Favourite Look",
|
||||
"cmd.component_cycle_color": "Cycle Component Colour",
|
||||
"cmd.construct_point_center": "Point At Centre Of Circle/Sphere/Torus",
|
||||
"cmd.inspect_center_of_mass": "Centre of Mass",
|
||||
"cmd.inspect_colored_components": "Enable Component Colours",
|
||||
"cmd.inspect_colored_components_disable": "Disable Component Colours",
|
||||
"cmd.parameters_favorite": "Favourite Parameter",
|
||||
"cmd.render_aging_describe": "Layered Surface Aging",
|
||||
"cmd.render_effects_describe": "Lens and Creative Effects",
|
||||
"cmd.sketch.construction_line": "Construction Line",
|
||||
"cmd.sketch.construction_line.angle": "Construction Line at Angle",
|
||||
"cmd.sketch.construction_line.bisector": "Angle Bisector",
|
||||
"cmd.sketch.construction_line.horizontal": "Horizontal Construction Line",
|
||||
"cmd.sketch.construction_line.parallel": "Parallel Construction Line",
|
||||
"cmd.sketch.construction_line.perpendicular": "Perpendicular Construction Line",
|
||||
"cmd.sketch.construction_line.tangent_at": "Tangent at Curve Point",
|
||||
"cmd.sketch.construction_line.tangent_external": "Tangent through External Point",
|
||||
"cmd.sketch.construction_line.tangent_two": "Common Tangent",
|
||||
"cmd.sketch.construction_line.vertical": "Vertical Construction Line",
|
||||
"cmd.sketch_arc_center_point": "Centre Point Arc",
|
||||
"cmd.sketch_circle_center": "Centre Diameter Circle",
|
||||
"cmd.sketch_rectangle_center": "Centre Rectangle",
|
||||
@@ -47,65 +27,29 @@
|
||||
"color.edit": "Click to edit colour",
|
||||
"color.gamma": "Showing colour values in 0-255 gamma space",
|
||||
"color.linear": "Showing colour values in 0-1 linear space",
|
||||
"color.recent": "Recent colours",
|
||||
"color.recent_help": "Colours you pick in any colour field appear here",
|
||||
"color.saturation": "Saturation (per cent)",
|
||||
"color.selected": "Selected colour",
|
||||
"color.step_down": "Step down",
|
||||
"color.step_up": "Step up",
|
||||
"color.value": "Value, the brightness (per cent)",
|
||||
"dxf.cancelled": "DXF export cancelled",
|
||||
"dxf.cancelling": "Cancelling DXF export",
|
||||
"help.apache": "Apache licence 2.0",
|
||||
"help.appearance.assign": "Gives the selected bodies, faces or components a colour or a material look, without changing their weight. Use it to tell parts apart or to preview a finish.",
|
||||
"help.face.delete": "Removes the selected faces and lets the neighbouring faces close the gap, or leaves the gap open as a surface. Use it to take off a rounded edge, a hole or a boss that is in the way.",
|
||||
"help.font_details": "The interface uses egui's built-in fonts (Ubuntu Light and Hack, under the Ubuntu Font Licence and the MIT/Bitstream Vera licence; Noto Emoji and the emoji icon font, under the OFL and MIT). Sketch text uses Liberation Sans (SIL OFL 1.1). The Crafting Apps' optional font pack, craft-fonts (BIZ UDPGothic, Shippori Mincho, BIZ UDMincho, Noto Sans CJK SC, Noto Sans Arabic; SIL OFL 1.1), is not bundled in SolidCraft builds.",
|
||||
"help.licences": "Licences",
|
||||
"help.material.assign": "Sets what the selected bodies are made of, such as steel, aluminium or ABS. Use it so that the mass and weight match the real part.",
|
||||
"help.mit": "MIT licence",
|
||||
"help.solid.draft": "Tilts the selected faces by the angle you set, measured from a neutral plane. Use it on moulded or cast parts so that they slide out of the mould.",
|
||||
"help.solid.revolve": "Spins a sketch profile around an axis to make a round solid. Use it for shafts, knobs, bowls and any part that is round about a centre line.",
|
||||
"help.solid.split_body": "Cuts a body into separate bodies with a plane, the face of another body or a sketch line. Use it to make parts that are printed, machined or coloured on their own.",
|
||||
"help.solid.split_face": "Divides the selected faces along a plane, the face of another body or sketch curves, without cutting the body. Use it to give part of a face its own draft, colour or offset.",
|
||||
"help.third_party": "Third-party licences",
|
||||
"home.new_mm_hint": "Millimetres · opens in Design",
|
||||
"navbar.custom.customise": "Customise navigation bar…",
|
||||
"navbar.custom.position_centre": "Centre",
|
||||
"navbar.description.separator": "A thin line that groups neighbouring slots.",
|
||||
"pattern.alignment_hint": "Counts include the original. Centre balances copies; Flip chooses the extra side for even totals.",
|
||||
"prefs.appearance.colour_vision_help": "Changes the colours that carry a meaning: sketch states, axes, bends, analyses and icons.",
|
||||
"prefs.appearance.viewport_colours": "3D view colours",
|
||||
"prefs.bottom_centre": "Bottom centre",
|
||||
"prefs.logo_theme": "Theme's colours",
|
||||
"prefs.max_file_size": "Largest file to open",
|
||||
"prefs.max_file_size_design": "Design files",
|
||||
"prefs.max_file_size_flag": "This run started with --max-file-size {size}, which applies until SolidCraft closes. These settings apply when it starts without it.",
|
||||
"prefs.max_file_size_note": "Defaults: 512 MB and 256 MB; at most 16 GB. Opening a file takes several times its size in memory. A design that keeps a large import may need a higher design file limit to save and reopen.",
|
||||
"prefs.max_file_size_step": "STEP and import files",
|
||||
"prefs.max_file_size_web": "The web app opens STEP and import files up to 512 MB and designs up to 256 MB: a browser tab can't hold larger files. The desktop app can raise these limits.",
|
||||
"prefs.megabytes": "MB",
|
||||
"prefs.network_locations_note": "For organisations that keep designs on file servers. Off: only servers you allow.",
|
||||
"prefs.sketch_conflict_colour": "Conflicting geometry in the conflict colour",
|
||||
"prefs.sketch_conflict_colour_note": "While a sketch's constraints conflict, the curves and points a failing constraint holds are drawn in the conflict colour too, not only its glyph or dimension. Off: they keep their usual colours.",
|
||||
"prefs.sketch_construction_lines": "Extended construction line tools",
|
||||
"prefs.sketch_recent_snap": "Auto snap to recent centres and midpoints",
|
||||
"prefs.sketch_recent_snap_note": "While drawing, snap the cursor where the guides of the two most recent circle centres or straight-edge midpoints cross. Off: those guides stay visual and the cursor does not move.",
|
||||
"prefs.sketch_state_icons": "Sketch icons in the colour of their state",
|
||||
"prefs.sketch_state_icons_note": "In History and the browser, a sketch's icon takes the colour of its state (under constrained, fully constrained or conflicting) and its tooltip names the state. Off: every sketch has the same icon.",
|
||||
"prefs.sketch_tracking_guides": "Centre and midpoint alignment guides",
|
||||
"prefs.sketch_tracking_guides_note": "Hover over a circle centre or straight-edge midpoint, then move horizontally or vertically to show a temporary dotted guide.",
|
||||
"prefs.status_bar_note": "A thin bar along the window's bottom edge shows what is being edited, the open sketch's state in its colour and the design's units; a click on the units changes them. The sketch's state then no longer shows in the view's corner.",
|
||||
"prefs.structure.appearance_note": "Colours, text and accessibility.",
|
||||
"prefs.structure.tools_note": "How modelling tools behave.",
|
||||
"prefs.theme_follow": "Follow a theme file",
|
||||
"prefs.theme_follow_error": "Not applied: {error}. The last good theme stays on.",
|
||||
"prefs.theme_follow_launch": "Followed for this run only (--theme-file); reloaded whenever another tool rewrites it.",
|
||||
"prefs.theme_follow_note": "Reloaded whenever another tool rewrites it. Choosing a theme stops following it.",
|
||||
"prefs.theme_follow_off": "Off",
|
||||
"prefs.theme_follow_stop": "Stop following",
|
||||
"prefs.theme_option_opacity_help": "Adjust floating panel opacity. A readability floor protects text over viewport colours.",
|
||||
"prefs.theme_option_tab_colours": "Tab colours",
|
||||
"prefs.theme_option_tab_colours_help": "Use this theme’s panel and viewport colours for each ribbon tab.",
|
||||
"prefs.theme_option_tab_fade_ms_help": "Fade panel and viewport colours when switching tabs. Zero switches instantly.",
|
||||
"prefs.trackpad_zoom_modifier.help": "Hold only this key to zoom at the pointer with vertical scroll. Off keeps the existing scroll behaviour.",
|
||||
"prefs.view": "View behaviour",
|
||||
"prefs.viewport_background_colour": "Colour",
|
||||
"render.aging_add": "Add aging layer",
|
||||
@@ -135,7 +79,6 @@
|
||||
"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.appearance_editor.base_colour": "Base colour",
|
||||
"render.backdrop_first": "Start / centre colour",
|
||||
"render.backdrop_second": "End / edge colour",
|
||||
"render.backdrop_x": "Centre X",
|
||||
@@ -152,31 +95,21 @@
|
||||
"render.base_color": "Base colour · sRGB",
|
||||
"render.bloom_help": "Creative output-only glow from positive linear highlights, spread with three normalised box passes. Radius is in output pixels. Added in linear display light before sRGB; not a lens, volumetric effect or convergence improvement. The raw EXR is unchanged.",
|
||||
"render.chromatic_offset": "Chromatic edge offset",
|
||||
"render.cpu_area_editor_help": "Full side vectors in millimeters; U cross V points towards the emitting side. This differs from GPU half-edge vectors.",
|
||||
"render.cpu_light_help": "Up to eight directional lights. An empty custom list turns them off; selecting a preset restores its lights. Colours are linear RGB and intensity is relative, not watts.",
|
||||
"render.cpu_linear_color": "Linear colour (R, G, B)",
|
||||
"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.env_colour": "Colour",
|
||||
"render.environment.radial_only": "Centre and radius apply to Radial mode.",
|
||||
"render.error_finish_gesture": "finish the modelling gesture before previewing",
|
||||
"render.error_finish_operation": "finish the current modelling operation before entering Render",
|
||||
"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.frame_centre_mark": "Centre mark",
|
||||
"render.hdr_primaries_declared": "The source declares its colour primaries.",
|
||||
"render.hdr_primaries_srgb": "Colour primaries: sRGB/D65",
|
||||
"render.hdr_primaries_undeclared": "The source does not declare primaries; review the renderer colour requirements.",
|
||||
"render.image_cancelled": "Render cancelled",
|
||||
"render.image_cancelling": "Cancelling",
|
||||
"render.image_scene_stale": "Finish the modelling gesture or refresh the scene before checking or starting an image.",
|
||||
"render.inspector_colour": "Colour",
|
||||
"render.layer_down": "Move down",
|
||||
"render.layer_enabled": "Layer enabled",
|
||||
"render.layer_remove": "Remove layer",
|
||||
@@ -191,11 +124,6 @@
|
||||
"render.library_rig_help": "Fit once at an explicit centre and physical extent. Complete rigs include passive cards; calibration blockers still refuse. Conservative emitter/card clearance is checked before saving.",
|
||||
"render.library_source_preview_help": "Authored base-colour source map. This is not a rendered or measured material preview.",
|
||||
"render.library_unfavorite": "Remove favourite",
|
||||
"render.light_behaviour": "BEHAVIOUR",
|
||||
"render.light_preview_help": "Live view is unshadowed; Area lights use a centre-sample approximation. Render uses the complete light model.",
|
||||
"render.light_product_centre": "Product centre",
|
||||
"render.light_temperature": "Colour temperature",
|
||||
"render.lighting_colour": "Colour",
|
||||
"render.look_add": "Add colour look",
|
||||
"render.look_bleach": "Bleach-bypass-inspired",
|
||||
"render.look_cool": "Cool",
|
||||
@@ -219,48 +147,30 @@
|
||||
"render.radial_distortion": "Radial distortion",
|
||||
"render.ribbon_aging": "Aging",
|
||||
"render.ribbon_effects": "Effects",
|
||||
"render.ribbon_studios_help": "Browse implemented studio compositions and review their replacements before applying. Concept-only scenes remain labelled.",
|
||||
"render.sharpening": "Detail sharpening",
|
||||
"render.soft_focus": "Soft focus",
|
||||
"render.stage_panels.centre": "Centre",
|
||||
"render.stage_panels.source_note": "Authored base-colour source map. Not a measured material.",
|
||||
"render.studio_area_help": "Up to four one-sided rectangles. Orthogonal half-edges U and V define size; U × V faces the lit side. Coordinates are world-space millimetres.",
|
||||
"render.studio_center": "Centre X, Y, Z (mm)",
|
||||
"render.studio_hdr_help": "Radiance .hdr, 2:1 panorama, up to 2048 × 1024. EXR is unavailable. The colour assumption applies only when primaries are unspecified.",
|
||||
"sketch_text.align_center": "Centre",
|
||||
"ui.3d_mouse_kind_cap": "Return to Centre",
|
||||
"ui.3d_mouse_looks_like_cap": "The reading stays put while you hold it, the way a Return to Centre device does.",
|
||||
"ui.3d_mouse_orbit_tip": "Traced: the 3D mouse turns the view around the part of the model in the middle of the view when the turn begins, marked by a faint ball. Classic: around the view's centre",
|
||||
"ui.3d_mouse_step_pull": "Pull the cap towards you, hold it, then let go.",
|
||||
"ui.3d_mouse_switch_cap": "Calibrate as Return to Centre",
|
||||
"ui.agent_chat_minimize": "Minimise to a strip",
|
||||
"ui.ai_mode_edits_note": "Modelling commands run without asking; saving, opening, exporting and other file or destructive actions still ask.",
|
||||
"ui.center": "Centre",
|
||||
"ui.center_arc_follows_cursor": "Centre point arc follows the cursor's direction",
|
||||
"ui.center_arc_follows_cursor_tip": "A centre point arc runs clockwise or counter-clockwise the way the cursor goes round its centre. Off: always counter-clockwise from the start.",
|
||||
"ui.centerline": "Centreline",
|
||||
"ui.colour_vision": "Colour vision",
|
||||
"ui.colour_vision_note": "Changes the colours that carry a meaning: sketch states, axes, bends, analyses and icons. If you use Windows colour filters, keep this Off.",
|
||||
"ui.colour_vision_off_tip": "The theme's own colours",
|
||||
"ui.construction.ribbon_label": "X-Lines",
|
||||
"ui.customizeToolbar": "Customise Toolbar…",
|
||||
"ui.default_modelling_orientation": "Default modelling orientation",
|
||||
"ui.design_properties.license": "Licence",
|
||||
"ui.design_properties.organization": "Organisation",
|
||||
"ui.details_curvature_caption": "Faces are coloured by their local radius of curvature.",
|
||||
"ui.details_draft_caption": "Faces are coloured by their angle to the pull direction.",
|
||||
"ui.export_keymap_tip": "Keys, S box favourites and the toolbar, as a file",
|
||||
"ui.instances": "Instances",
|
||||
"ui.layout_compact": "Compact",
|
||||
"ui.layout_floating": "Floating",
|
||||
"ui.material_review.claims": "Sources and licences are unverified claims. Missing values remain unknown.",
|
||||
"ui.point_cloud_color_by": "Colour By",
|
||||
"ui.point_cloud_file_colors": "File Colours",
|
||||
"ui.point_cloud_no_file_colors": "The file has no colours",
|
||||
"ui.point_cloud_single_color": "Single Colour",
|
||||
"ui.properties.inertia_note": "About the centre of mass, in world axes (g·{unit}²)",
|
||||
"ui.ribbon_labels": "Show text under ribbon icons",
|
||||
"ui.view_centre": "View centre",
|
||||
"ui.workspace_layout": "Workspace layout",
|
||||
"ui.zoom_to_cursor_tip": "The wheel zooms towards the point under the cursor (off: towards the view's centre)",
|
||||
"update.prefs_flatpak": "Flatpak updates this copy ({version}): run {command} or use your software centre. SolidCraft doesn't check."
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"en": {
|
||||
"render.cpu_glass_continue": "Continue reflections",
|
||||
"render.cpu_glass_help": "Standard uses bounded paths and may darken reflective glass. Continue reflections stops with an error if a supported optical limit is reached. Selected opaque reflections and unsupported glass shapes remain unavailable.",
|
||||
"render.cpu_glass_label": "Glass rendering",
|
||||
"render.cpu_glass_standard": "Standard"
|
||||
},
|
||||
"ar": {
|
||||
"render.cpu_glass_continue": "متابعة الانعكاسات",
|
||||
"render.cpu_glass_help": "يستخدم الوضع القياسي مسارات ضوء محدودة، وقد يجعل الزجاج العاكس أغمق. يوقف خيار «متابعة الانعكاسات» التصيير مع ظهور خطأ عند بلوغ أحد الحدود البصرية المدعومة. تظل الانعكاسات المحددة على الأجسام المعتمة وأشكال الزجاج غير المدعومة غير متاحة.",
|
||||
"render.cpu_glass_label": "تصيير الزجاج",
|
||||
"render.cpu_glass_standard": "قياسي"
|
||||
},
|
||||
"de": {
|
||||
"render.cpu_glass_continue": "Reflexionen fortsetzen",
|
||||
"render.cpu_glass_help": "Standard begrenzt die Lichtwege und kann reflektierendes Glas abdunkeln. Reflexionen fortsetzen stoppt das Rendering mit einem Fehler, wenn eine unterstützte optische Grenze erreicht wird. Ausgewählte opake Reflexionen und nicht unterstützte Glasformen bleiben nicht verfügbar.",
|
||||
"render.cpu_glass_label": "Glasdarstellung",
|
||||
"render.cpu_glass_standard": "Standard"
|
||||
},
|
||||
"es": {
|
||||
"render.cpu_glass_continue": "Continuar reflejos",
|
||||
"render.cpu_glass_help": "Estándar limita los recorridos y puede oscurecer el vidrio reflectante. Continuar reflejos detiene el render con un error si alcanza un límite óptico admitido. Los reflejos opacos seleccionados y las formas de vidrio no compatibles siguen sin estar disponibles.",
|
||||
"render.cpu_glass_label": "Render de vidrio",
|
||||
"render.cpu_glass_standard": "Estándar"
|
||||
},
|
||||
"he": {
|
||||
"render.cpu_glass_continue": "המשך השתקפויות",
|
||||
"render.cpu_glass_help": "המצב הרגיל מגביל את מסלולי האור ועלול להכהות זכוכית מחזירת אור. האפשרות «המשך השתקפויות» עוצרת את הרינדור עם שגיאה כאשר מגיעים לאחת המגבלות האופטיות הנתמכות. השתקפויות נבחרות על גופים אטומים וצורות זכוכית שאינן נתמכות נשארות לא זמינות.",
|
||||
"render.cpu_glass_label": "רינדור זכוכית",
|
||||
"render.cpu_glass_standard": "רגיל"
|
||||
},
|
||||
"ja": {
|
||||
"render.cpu_glass_continue": "反射を継続",
|
||||
"render.cpu_glass_help": "標準では光路を制限するため、反射するガラスが暗くなる場合があります。反射を継続する設定では、対応する光学的な上限に達するとエラーでレンダリングを停止します。選択された不透明面の反射や未対応のガラス形状は引き続き使用できません。",
|
||||
"render.cpu_glass_label": "ガラスの描画",
|
||||
"render.cpu_glass_standard": "標準"
|
||||
},
|
||||
"ko": {
|
||||
"render.cpu_glass_continue": "반사 계속",
|
||||
"render.cpu_glass_help": "표준은 광선 경로를 제한하므로 반사 유리가 어둡게 보일 수 있습니다. 반사 계속은 지원되는 광학 한계에 도달하면 오류와 함께 렌더링을 중지합니다. 선택한 불투명 표면 반사와 지원되지 않는 유리 형상은 계속 사용할 수 없습니다.",
|
||||
"render.cpu_glass_label": "유리 렌더링",
|
||||
"render.cpu_glass_standard": "표준"
|
||||
},
|
||||
"vi": {
|
||||
"render.cpu_glass_continue": "Tiếp tục phản xạ",
|
||||
"render.cpu_glass_help": "Chế độ tiêu chuẩn giới hạn đường truyền ánh sáng và có thể làm kính phản xạ trông tối hơn. Tùy chọn Tiếp tục phản xạ dừng kết xuất kèm lỗi khi đạt một giới hạn quang học được hỗ trợ. Các phản xạ đã chọn trên vật thể đục và các hình dạng kính không được hỗ trợ vẫn không khả dụng.",
|
||||
"render.cpu_glass_label": "Kết xuất kính",
|
||||
"render.cpu_glass_standard": "Tiêu chuẩn"
|
||||
},
|
||||
"zh": {
|
||||
"render.cpu_glass_continue": "继续反射",
|
||||
"render.cpu_glass_help": "标准模式限制光线路径,可能使反射玻璃变暗。继续反射模式在达到支持的光学限制时会报错并停止渲染。所选不透明表面反射和不支持的玻璃形状仍不可用。",
|
||||
"render.cpu_glass_label": "玻璃渲染",
|
||||
"render.cpu_glass_standard": "标准"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"en": {
|
||||
"render.cpu_directional_shadow_help": "CPU optics are temporary and require certified sheets or axis-aligned boxes with opaque body and face appearances. Directional transmitted shadows require global shadows on and all active assignments to agree. Mixed choices, nesting, environment lighting and local lights are unsupported in that mode. This is straight RGB visibility, not refracted shadows or caustics. Subsurface, coats and maps remain unsupported; fixed and progressive sampling retain finite-depth limits.",
|
||||
"render.cpu_shadow_conflict": "The global directional transmission option conflicts with an optical assignment whose transmitted shadows are off. No authored option was changed.",
|
||||
"render.cpu_shadow_mixed": "CPU uses one transmitted-shadow switch for all active optical assignments. Mixed on/off choices cannot be represented and have been preserved."
|
||||
},
|
||||
"es": {
|
||||
"render.cpu_directional_shadow_help": "La óptica CPU es temporal y requiere láminas o cajas alineadas con los ejes certificadas, con aspectos opacos de cuerpo y caras. Las sombras transmitidas direccionales requieren sombras globales activadas y que todas las asignaciones activas coincidan. Ese modo no admite mezclas, nesting, iluminación de entorno ni luces locales. Es visibilidad RGB recta, sin sombras refractadas ni cáusticas. La dispersión subsuperficial, las capas y los mapas siguen sin admitirse; el muestreo fijo y progresivo conserva los límites de profundidad.",
|
||||
"render.cpu_shadow_conflict": "La opción global de transmisión direccional contradice una asignación óptica con sombras transmitidas desactivadas. No se ha cambiado ninguna opción definida.",
|
||||
"render.cpu_shadow_mixed": "CPU usa un único control de sombras transmitidas para todas las asignaciones ópticas activas. No puede representar opciones activadas y desactivadas mezcladas; se han conservado."
|
||||
},
|
||||
"de": {
|
||||
"render.cpu_directional_shadow_help": "CPU-Optik ist temporär und erfordert zertifizierte Scheiben oder achsenparallele Quader mit opaken Körper- und Flächenerscheinungen. Gerichtete durchgelassene Schatten erfordern globale Schatten und übereinstimmende aktive Zuweisungen. Gemischte Optionen, Verschachtelung, Umgebungslicht und lokale Lichter sind in diesem Modus nicht unterstützt. Dies ist geradlinige RGB-Sichtbarkeit, keine gebrochenen Schatten oder Kaustiken. Subsurface-Streuung, Beschichtungen und Maps bleiben nicht unterstützt; feste und progressive Abtastung behalten die endliche Tiefengrenze.",
|
||||
"render.cpu_shadow_conflict": "Die globale gerichtete Transmission widerspricht einer optischen Zuweisung mit ausgeschalteten durchgelassenen Schatten. Keine festgelegte Option wurde geändert.",
|
||||
"render.cpu_shadow_mixed": "CPU verwendet einen Schalter für durchgelassene Schatten aller aktiven optischen Zuweisungen. Gemischte Ein-/Aus-Werte sind nicht darstellbar und wurden beibehalten."
|
||||
},
|
||||
"ko": {
|
||||
"render.cpu_directional_shadow_help": "CPU 광학은 임시 설정이며 불투명한 바디와 면 외관을 가진 인증된 판 또는 축 정렬 상자가 필요합니다. 방향성 투과 그림자는 전역 그림자가 켜져 있고 모든 활성 할당의 설정이 같아야 합니다. 이 모드는 혼합 설정, 중첩, 환경 조명 및 로컬 광원을 지원하지 않습니다. 이는 직선 RGB 가시성이며 굴절 그림자나 코스틱이 아닙니다. 표면하 산란, 코팅 및 맵은 계속 지원하지 않으며 고정 및 점진적 샘플링은 유한 깊이 제한을 유지합니다.",
|
||||
"render.cpu_shadow_conflict": "전역 방향성 투과 옵션이 투과 그림자가 꺼진 광학 할당과 충돌합니다. 작성된 옵션은 변경되지 않았습니다.",
|
||||
"render.cpu_shadow_mixed": "CPU는 모든 활성 광학 할당에 하나의 투과 그림자 스위치를 사용합니다. 켜짐과 꺼짐이 섞인 설정은 표현할 수 없으며 그대로 보존되었습니다."
|
||||
},
|
||||
"ar": {
|
||||
"render.cpu_directional_shadow_help": "بصريات CPU مؤقتة وتتطلب صفائح أو صناديق محاذية للمحاور اجتازت التحقق، بمظاهر معتمة للجسم والأوجه. تتطلب الظلال النافذة الاتجاهية تفعيل الظلال العامة وتطابق كل التعيينات النشطة. لا يدعم هذا الوضع الخيارات المختلطة أو التداخل أو إضاءة البيئة أو الأضواء المحلية. هذه رؤية RGB مستقيمة وليست ظلالًا منكسرة أو كاوستيك. يبقى التشتت تحت السطحي والطلاءات والخرائط غير مدعوم، وتبقى حدود العمق المحدودة لأخذ العينات الثابت والتدريجي.",
|
||||
"render.cpu_shadow_conflict": "يتعارض خيار النفاذ الاتجاهي العام مع تعيين بصري تكون ظلاله النافذة معطلة. لم يتغير أي خيار محدد.",
|
||||
"render.cpu_shadow_mixed": "يستخدم CPU مفتاح ظلال نافذة واحدًا لجميع التعيينات البصرية النشطة. لا يمكن تمثيل خليط من التفعيل والتعطيل، وقد تم الاحتفاظ بالخيارات."
|
||||
},
|
||||
"he": {
|
||||
"render.cpu_directional_shadow_help": "אופטיקת CPU היא זמנית ודורשת יריעות או תיבות מיושרות לצירים שעברו אימות, עם מראה אטום של הגוף והפאות. צללים מועברים כיווניים דורשים צללים כלליים פעילים ואחידות בכל ההקצאות הפעילות. מצב זה אינו תומך בערכים מעורבים, קינון, תאורת סביבה או אורות מקומיים. זו נראות RGB בקו ישר, ללא צללים נשברים או קאוסטיקה. פיזור תת-משטחי, ציפויים ומפות עדיין אינם נתמכים; דגימה קבועה והדרגתית שומרות על מגבלות העומק הסופי.",
|
||||
"render.cpu_shadow_conflict": "אפשרות ההעברה הכיוונית הכללית סותרת הקצאה אופטית שהצללים המועברים שלה כבויים. שום אפשרות שנקבעה לא שונתה.",
|
||||
"render.cpu_shadow_mixed": "CPU משתמש במתג צללים מועברים אחד לכל ההקצאות האופטיות הפעילות. לא ניתן לייצג ערכים מעורבים של פעיל וכבוי, והם נשמרו."
|
||||
},
|
||||
"ja": {
|
||||
"render.cpu_directional_shadow_help": "CPU 光学設定は一時的で、不透明なボディと面の外観を持つ認証済みの薄板または軸に平行なボックスが必要です。平行光源の透過シャドウには全体のシャドウを有効にし、すべての有効な割り当ての設定を一致させる必要があります。このモードでは混在設定、入れ子、環境光、ローカルライトは非対応です。これは直線上の RGB 可視性であり、屈折シャドウやコースティクスではありません。表面下散乱、コート、マップは引き続き非対応で、固定およびプログレッシブサンプリングでも有限深度制限は残ります。",
|
||||
"render.cpu_shadow_conflict": "全体の平行光透過オプションが、透過シャドウを無効にした光学割り当てと競合しています。設定したオプションは変更されていません。",
|
||||
"render.cpu_shadow_mixed": "CPU はすべての有効な光学割り当てに単一の透過シャドウスイッチを使用します。有効と無効の混在は表現できないため、その設定を保持しています。"
|
||||
},
|
||||
"vi": {
|
||||
"render.cpu_directional_shadow_help": "Quang học CPU là tạm thời và cần tấm hoặc hộp song song các trục đã được kiểm chứng, với diện mạo thân và mặt đục. Bóng truyền qua từ đèn định hướng cần bật bóng toàn cục và mọi phép gán đang bật phải cùng lựa chọn. Chế độ này không hỗ trợ lựa chọn lẫn bật/tắt, lồng nhau, ánh sáng môi trường hoặc đèn cục bộ. Đây là độ nhìn thấy RGB theo đường thẳng, không phải bóng khúc xạ hay caustic. Tán xạ dưới bề mặt, lớp phủ và bản đồ vẫn không được hỗ trợ; lấy mẫu cố định và lũy tiến vẫn có giới hạn độ sâu hữu hạn.",
|
||||
"render.cpu_shadow_conflict": "Tùy chọn truyền sáng định hướng toàn cục xung đột với phép gán quang học có bóng truyền qua đang tắt. Không thay đổi tùy chọn đã soạn nào.",
|
||||
"render.cpu_shadow_mixed": "CPU dùng một công tắc bóng truyền qua cho tất cả phép gán quang học đang bật. Không thể biểu diễn lựa chọn lẫn bật và tắt; các lựa chọn đã được giữ nguyên."
|
||||
},
|
||||
"zh": {
|
||||
"render.cpu_directional_shadow_help": "CPU 光学设置是临时的,需要通过认证的薄片或轴对齐盒体,以及不透明的实体和面外观。方向光透射阴影要求开启全局阴影,并且所有启用的分配采用相同设置。此模式不支持混合开关、嵌套、环境照明或局部光源。这是直线 RGB 可见性,并非折射阴影或焦散。次表面散射、涂层和贴图仍不支持;固定和渐进采样仍保留有限深度限制。",
|
||||
"render.cpu_shadow_conflict": "全局方向光透射选项与关闭透射阴影的光学分配冲突。未更改任何已设置的选项。",
|
||||
"render.cpu_shadow_mixed": "CPU 对所有启用的光学分配使用一个透射阴影开关。无法表示开启和关闭混合的设置,已保留这些设置。"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"en": {
|
||||
"render.cpu_optics_certification": "CPU Check and Start validate captured identity, opaque body and face appearances, and the requested sheet or axis-aligned box certificate. Choosing a boundary does not certify geometry. Start rechecks the capture; passing Check does not guarantee later admission."
|
||||
},
|
||||
"es": {
|
||||
"render.cpu_optics_certification": "Comprobar e Iniciar en CPU validan la identidad capturada, los aspectos opacos del cuerpo y sus caras, y el certificado de lámina o caja alineada con los ejes solicitado. Elegir una frontera no certifica la geometría. Iniciar comprueba de nuevo la captura; superar Comprobar no garantiza una admisión posterior."
|
||||
},
|
||||
"de": {
|
||||
"render.cpu_optics_certification": "Prüfen und Starten auf der CPU validieren die erfasste Identität, opake Körper- und Flächenerscheinungen sowie das angeforderte Zertifikat für Scheiben oder achsenparallele Quader. Die Wahl einer Grenze zertifiziert keine Geometrie. Starten prüft die Erfassung erneut; erfolgreiches Prüfen garantiert keine spätere Zulassung."
|
||||
},
|
||||
"ko": {
|
||||
"render.cpu_optics_certification": "CPU의 확인과 시작은 캡처한 식별 정보, 불투명한 바디와 면 외관, 요청한 판 또는 축 정렬 상자의 인증을 검증합니다. 경계를 선택하는 것만으로 형상이 인증되지는 않습니다. 시작 시 캡처를 다시 확인하며, 확인에 성공해도 이후 승인이 보장되지는 않습니다."
|
||||
},
|
||||
"ar": {
|
||||
"render.cpu_optics_certification": "يتحقق الفحص والبدء على CPU من الهوية الملتقطة ومظاهر الجسم والأوجه المعتمة وشهادة الصفيحة أو الصندوق المحاذي للمحاور المطلوبة. اختيار حد لا يثبت صلاحية الهندسة. يعيد البدء فحص اللقطة؛ نجاح الفحص لا يضمن القبول لاحقًا."
|
||||
},
|
||||
"he": {
|
||||
"render.cpu_optics_certification": "בדיקה והתחלה ב-CPU מאמתות את הזהות שנלכדה, מראה אטום של הגוף והפאות ואת האישור המבוקש ליריעה או לתיבה מיושרת לצירים. בחירת גבול אינה מאשרת את הגאומטריה. התחלה בודקת שוב את הלכידה; הצלחה בבדיקה אינה מבטיחה אישור מאוחר יותר."
|
||||
},
|
||||
"ja": {
|
||||
"render.cpu_optics_certification": "CPU の確認と開始では、取得した識別情報、不透明なボディと面の外観、要求された薄板または軸に平行なボックスの認証を検証します。境界の選択だけでは形状は認証されません。開始時に取得内容を再確認するため、確認の成功は後の受け入れを保証しません。"
|
||||
},
|
||||
"vi": {
|
||||
"render.cpu_optics_certification": "Kiểm tra và Bắt đầu trên CPU xác thực danh tính đã chụp, diện mạo thân và mặt đục, cùng chứng nhận tấm hoặc hộp song song các trục được yêu cầu. Chọn biên không chứng nhận hình học. Bắt đầu kiểm tra lại bản chụp; vượt qua Kiểm tra không bảo đảm được chấp nhận về sau."
|
||||
},
|
||||
"zh": {
|
||||
"render.cpu_optics_certification": "CPU 的检查和开始会验证捕获的身份、不透明的实体和面外观,以及请求的薄片或轴对齐盒体认证。选择边界本身并不能认证几何体。开始时会重新检查捕获内容;通过检查并不保证之后仍能获准执行。"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"en": {
|
||||
"render.cpu_optics_boundary": "CPU requires an explicit Thin sheet or Box boundary. The captured geometry must pass certification.",
|
||||
"render.cpu_optics_conflict": "CPU optical assignments cannot replace existing dielectric overrides.",
|
||||
"render.cpu_optics_enable": "Use these optics on CPU",
|
||||
"render.cpu_optics_explicit": "Review this assignment and explicitly enable its CPU optical route.",
|
||||
"render.cpu_optics_fixed": "The installed CPU transport cannot admit this optical sampling request.",
|
||||
"render.cpu_optics_help": "Temporary CPU optics: fixed or progressive sampling, opaque body and face appearances, certified sheets or axis-aligned boxes. Transmitted shadows, subsurface, coats and maps are unsupported. Box nesting needs explicit opt-in and certification. Accumulation retains the finite-depth transport limits.",
|
||||
"render.cpu_optics_limit": "CPU supports at most eight optical body assignments.",
|
||||
"render.cpu_optics_request": "CPU optics require the typed CPU image request and its existing options.",
|
||||
"render.cpu_optics_saved": "Saved GPU optics cannot be mixed with temporary CPU optical assignments.",
|
||||
"render.cpu_optics_unsupported": "This CPU route supports glass without transmitted shadows, subsurface, coats or maps. Review the authored options; they have been preserved."
|
||||
},
|
||||
"es": {
|
||||
"render.cpu_optics_boundary": "CPU requiere elegir explícitamente Lámina fina o Caja. La geometría capturada debe superar la certificación.",
|
||||
"render.cpu_optics_conflict": "Las asignaciones ópticas CPU no pueden sustituir las modificaciones dieléctricas existentes.",
|
||||
"render.cpu_optics_enable": "Usar esta óptica en CPU",
|
||||
"render.cpu_optics_explicit": "Revisa esta asignación y activa explícitamente su ruta óptica CPU.",
|
||||
"render.cpu_optics_fixed": "El transporte CPU instalado no puede admitir esta solicitud de muestreo óptico.",
|
||||
"render.cpu_optics_help": "Óptica CPU temporal: muestreo fijo o progresivo, aspectos opacos de cuerpo y caras, láminas o cajas alineadas con los ejes certificadas. No admite sombras transmitidas, dispersión subsuperficial, capas ni mapas. Las cajas anidadas requieren activación explícita y certificación. La acumulación conserva los límites de profundidad del transporte.",
|
||||
"render.cpu_optics_limit": "CPU admite como máximo ocho asignaciones ópticas de cuerpo.",
|
||||
"render.cpu_optics_request": "La óptica CPU requiere la solicitud de imagen CPU tipada y sus opciones existentes.",
|
||||
"render.cpu_optics_saved": "La óptica GPU guardada no se puede mezclar con asignaciones ópticas CPU temporales.",
|
||||
"render.cpu_optics_unsupported": "Esta ruta CPU admite vidrio sin sombras transmitidas, dispersión subsuperficial, capas ni mapas. Revisa las opciones definidas; se han conservado."
|
||||
},
|
||||
"de": {
|
||||
"render.cpu_optics_boundary": "CPU erfordert die ausdrückliche Wahl von Dünner Scheibe oder Quader. Die erfasste Geometrie muss zertifiziert werden.",
|
||||
"render.cpu_optics_conflict": "Optische CPU-Zuweisungen können vorhandene dielektrische Überschreibungen nicht ersetzen.",
|
||||
"render.cpu_optics_enable": "Diese Optik auf der CPU verwenden",
|
||||
"render.cpu_optics_explicit": "Prüfen Sie diese Zuweisung und aktivieren Sie ihren optischen CPU-Pfad ausdrücklich.",
|
||||
"render.cpu_optics_fixed": "Der installierte CPU-Transport kann diese optische Abtastanforderung nicht zulassen.",
|
||||
"render.cpu_optics_help": "Temporäre CPU-Optik: feste oder progressive Abtastung, opake Körper- und Flächenerscheinungen, zertifizierte Scheiben oder achsenparallele Quader. Durchgelassene Schatten, Subsurface-Streuung, Beschichtungen und Maps werden nicht unterstützt. Verschachtelte Quader erfordern ausdrückliche Aktivierung und Zertifizierung. Die Akkumulation behält die endliche Transporttiefe bei.",
|
||||
"render.cpu_optics_limit": "CPU unterstützt höchstens acht optische Körperzuweisungen.",
|
||||
"render.cpu_optics_request": "CPU-Optik erfordert die typisierte CPU-Bildanforderung und deren vorhandene Optionen.",
|
||||
"render.cpu_optics_saved": "Gespeicherte GPU-Optik kann nicht mit temporären optischen CPU-Zuweisungen kombiniert werden.",
|
||||
"render.cpu_optics_unsupported": "Dieser CPU-Pfad unterstützt Glas ohne durchgelassene Schatten, Subsurface-Streuung, Beschichtungen oder Maps. Prüfen Sie die unverändert erhaltenen Optionen."
|
||||
},
|
||||
"ko": {
|
||||
"render.cpu_optics_boundary": "CPU에서는 얇은 판 또는 상자 경계를 명시적으로 선택해야 합니다. 캡처한 형상은 인증을 통과해야 합니다.",
|
||||
"render.cpu_optics_conflict": "CPU 광학 할당은 기존 유전체 재정의를 대체할 수 없습니다.",
|
||||
"render.cpu_optics_enable": "이 광학 설정을 CPU에서 사용",
|
||||
"render.cpu_optics_explicit": "이 할당을 검토하고 CPU 광학 경로를 명시적으로 활성화하세요.",
|
||||
"render.cpu_optics_fixed": "설치된 CPU 전송 모델은 이 광학 샘플링 요청을 허용할 수 없습니다.",
|
||||
"render.cpu_optics_help": "임시 CPU 광학: 고정 또는 점진적 샘플링, 불투명한 바디와 면 외관, 인증된 판 또는 축 정렬 상자. 투과 그림자, 표면하 산란, 코팅 및 맵은 지원하지 않습니다. 중첩 상자는 명시적 활성화와 인증이 필요합니다. 누적해도 전송의 유한 깊이 제한은 유지됩니다.",
|
||||
"render.cpu_optics_limit": "CPU는 최대 8개의 광학 바디 할당을 지원합니다.",
|
||||
"render.cpu_optics_request": "CPU 광학에는 형식화된 CPU 이미지 요청과 기존 옵션이 필요합니다.",
|
||||
"render.cpu_optics_saved": "저장된 GPU 광학을 임시 CPU 광학 할당과 혼합할 수 없습니다.",
|
||||
"render.cpu_optics_unsupported": "이 CPU 경로는 투과 그림자, 표면하 산란, 코팅 및 맵이 없는 유리를 지원합니다. 작성된 옵션을 검토하세요. 옵션은 그대로 보존되었습니다."
|
||||
},
|
||||
"ar": {
|
||||
"render.cpu_optics_boundary": "يتطلب CPU اختيار حد صفيحة رقيقة أو صندوق صراحةً. يجب أن تجتاز الهندسة الملتقطة التحقق.",
|
||||
"render.cpu_optics_conflict": "لا يمكن لتعيينات البصريات على CPU استبدال تجاوزات العوازل الموجودة.",
|
||||
"render.cpu_optics_enable": "استخدام هذه البصريات على CPU",
|
||||
"render.cpu_optics_explicit": "راجع هذا التعيين وفعّل مساره البصري على CPU صراحةً.",
|
||||
"render.cpu_optics_fixed": "لا يمكن لنموذج النقل المثبت على CPU قبول طلب أخذ العينات البصري هذا.",
|
||||
"render.cpu_optics_help": "بصريات CPU مؤقتة: أخذ عينات ثابت أو تدريجي، ومظاهر معتمة للجسم والأوجه، وصفائح أو صناديق محاذية للمحاور اجتازت التحقق. الظلال النافذة والتشتت تحت السطحي والطلاءات والخرائط غير مدعومة. تتطلب الصناديق المتداخلة تفعيلًا صريحًا والتحقق. يبقى عمق النقل محدودًا حتى مع التراكم.",
|
||||
"render.cpu_optics_limit": "يدعم CPU ثمانية تعيينات بصرية للأجسام كحد أقصى.",
|
||||
"render.cpu_optics_request": "تتطلب بصريات CPU طلب صورة CPU محدد النوع وخياراته الموجودة.",
|
||||
"render.cpu_optics_saved": "لا يمكن خلط بصريات GPU المحفوظة مع تعيينات بصريات CPU المؤقتة.",
|
||||
"render.cpu_optics_unsupported": "يدعم مسار CPU هذا الزجاج دون ظلال نافذة أو تشتت تحت سطحي أو طلاءات أو خرائط. راجع الخيارات المحددة؛ فقد تم الاحتفاظ بها."
|
||||
},
|
||||
"he": {
|
||||
"render.cpu_optics_boundary": "CPU דורש בחירה מפורשת בגבול יריעה דקה או תיבה. הגאומטריה שנלכדה חייבת לעבור אימות.",
|
||||
"render.cpu_optics_conflict": "הקצאות אופטיות של CPU אינן יכולות להחליף דריסות דיאלקטריות קיימות.",
|
||||
"render.cpu_optics_enable": "שימוש באופטיקה זו ב-CPU",
|
||||
"render.cpu_optics_explicit": "יש לבדוק הקצאה זו ולהפעיל במפורש את הנתיב האופטי שלה ב-CPU.",
|
||||
"render.cpu_optics_fixed": "מודל התעבורה המותקן ב-CPU אינו יכול לאשר בקשת דגימה אופטית זו.",
|
||||
"render.cpu_optics_help": "אופטיקה זמנית ב-CPU: דגימה קבועה או הדרגתית, מראה אטום של הגוף והפאות, יריעות או תיבות מיושרות לצירים שעברו אימות. צללים מועברים, פיזור תת-משטחי, ציפויים ומפות אינם נתמכים. תיבות מקוננות דורשות הפעלה מפורשת ואימות. הצבירה שומרת על מגבלת עומק התעבורה הסופית.",
|
||||
"render.cpu_optics_limit": "CPU תומך בעד שמונה הקצאות אופטיות לגופים.",
|
||||
"render.cpu_optics_request": "אופטיקת CPU דורשת בקשת תמונת CPU מטיפוס מוגדר ואת האפשרויות הקיימות שלה.",
|
||||
"render.cpu_optics_saved": "לא ניתן לערבב אופטיקת GPU שמורה עם הקצאות אופטיות זמניות של CPU.",
|
||||
"render.cpu_optics_unsupported": "נתיב CPU זה תומך בזכוכית ללא צללים מועברים, פיזור תת-משטחי, ציפויים או מפות. יש לבדוק את האפשרויות שנקבעו; הן נשמרו ללא שינוי."
|
||||
},
|
||||
"ja": {
|
||||
"render.cpu_optics_boundary": "CPU では薄板またはボックス境界を明示的に選択する必要があります。取得した形状は認証に合格する必要があります。",
|
||||
"render.cpu_optics_conflict": "CPU の光学割り当てで既存の誘電体オーバーライドを置き換えることはできません。",
|
||||
"render.cpu_optics_enable": "この光学設定を CPU で使用",
|
||||
"render.cpu_optics_explicit": "この割り当てを確認し、CPU 光学経路を明示的に有効にしてください。",
|
||||
"render.cpu_optics_fixed": "インストール済みの CPU 光輸送では、この光学サンプリング要求を受け付けられません。",
|
||||
"render.cpu_optics_help": "一時的な CPU 光学設定:固定またはプログレッシブサンプリング、不透明なボディと面の外観、認証済みの薄板または軸に平行なボックス。透過シャドウ、表面下散乱、コート、マップは非対応です。入れ子のボックスには明示的な有効化と認証が必要です。累積しても光輸送の有限深度制限は残ります。",
|
||||
"render.cpu_optics_limit": "CPU はボディへの光学割り当てを最大 8 個までサポートします。",
|
||||
"render.cpu_optics_request": "CPU 光学設定には型付きの CPU 画像リクエストと既存のオプションが必要です。",
|
||||
"render.cpu_optics_saved": "保存済み GPU 光学設定と一時的な CPU 光学割り当ては混在できません。",
|
||||
"render.cpu_optics_unsupported": "この CPU 経路は透過シャドウ、表面下散乱、コート、マップのないガラスに対応します。設定したオプションを確認してください。内容は保持されています。"
|
||||
},
|
||||
"vi": {
|
||||
"render.cpu_optics_boundary": "CPU yêu cầu chọn rõ ràng biên Tấm mỏng hoặc Hộp. Hình học đã chụp phải vượt qua kiểm chứng.",
|
||||
"render.cpu_optics_conflict": "Các gán quang học CPU không thể thay thế các ghi đè điện môi hiện có.",
|
||||
"render.cpu_optics_enable": "Dùng quang học này trên CPU",
|
||||
"render.cpu_optics_explicit": "Hãy xem lại phép gán này và bật rõ ràng tuyến quang học CPU của nó.",
|
||||
"render.cpu_optics_fixed": "Mô hình truyền ánh sáng CPU đã cài không thể chấp nhận yêu cầu lấy mẫu quang học này.",
|
||||
"render.cpu_optics_help": "Quang học CPU tạm thời: lấy mẫu cố định hoặc lũy tiến, diện mạo thân và mặt đục, tấm hoặc hộp song song các trục đã được kiểm chứng. Không hỗ trợ bóng truyền qua, tán xạ dưới bề mặt, lớp phủ và bản đồ. Hộp lồng nhau cần được bật rõ ràng và kiểm chứng. Tích lũy vẫn giữ giới hạn độ sâu hữu hạn của truyền ánh sáng.",
|
||||
"render.cpu_optics_limit": "CPU hỗ trợ tối đa tám phép gán quang học cho thân.",
|
||||
"render.cpu_optics_request": "Quang học CPU yêu cầu yêu cầu ảnh CPU có kiểu cùng các tùy chọn hiện có.",
|
||||
"render.cpu_optics_saved": "Không thể trộn quang học GPU đã lưu với các gán quang học CPU tạm thời.",
|
||||
"render.cpu_optics_unsupported": "Tuyến CPU này hỗ trợ kính không có bóng truyền qua, tán xạ dưới bề mặt, lớp phủ hoặc bản đồ. Hãy xem lại các tùy chọn đã soạn; chúng đã được giữ nguyên."
|
||||
},
|
||||
"zh": {
|
||||
"render.cpu_optics_boundary": "CPU 要求明确选择薄片或盒体边界。捕获的几何体必须通过认证。",
|
||||
"render.cpu_optics_conflict": "CPU 光学分配不能替换现有的电介质覆盖设置。",
|
||||
"render.cpu_optics_enable": "在 CPU 上使用此光学设置",
|
||||
"render.cpu_optics_explicit": "请检查此分配并明确启用其 CPU 光学路径。",
|
||||
"render.cpu_optics_fixed": "已安装的 CPU 光传输无法接受此光学采样请求。",
|
||||
"render.cpu_optics_help": "临时 CPU 光学设置:固定或渐进采样、不透明的实体和面外观、通过认证的薄片或轴对齐盒体。不支持透射阴影、次表面散射、涂层或贴图。嵌套盒体需要明确启用并通过认证。累积仍保留光传输的有限深度限制。",
|
||||
"render.cpu_optics_limit": "CPU 最多支持八个实体光学分配。",
|
||||
"render.cpu_optics_request": "CPU 光学设置要求类型化的 CPU 图像请求及其现有选项。",
|
||||
"render.cpu_optics_saved": "已保存的 GPU 光学设置不能与临时 CPU 光学分配混用。",
|
||||
"render.cpu_optics_unsupported": "此 CPU 路径支持不含透射阴影、次表面散射、涂层或贴图的玻璃。请检查所编写的选项;这些选项已保留。"
|
||||
}
|
||||
}
|
||||