mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-06 12:01:10 +00:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fa691707c2 | |||
| b9a55c74e2 | |||
| 2bb9c81069 | |||
| cedb51b8ea | |||
| 66d13059dd | |||
| 800ea40c0d | |||
| 20442eecd3 | |||
| 22d1cd7032 | |||
| 6a0dc18655 | |||
| e006dbf1af | |||
| 8241a750fa | |||
| 41779cf7bb | |||
| b01b9689c8 | |||
| 4426da6f5d | |||
| 5ead82e627 | |||
| c29b0d2f13 | |||
| 480fd8d517 | |||
| 4c09fa5355 |
@@ -1,14 +0,0 @@
|
||||
# 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)
|
||||
# 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", "--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",
|
||||
]
|
||||
+2
-31
@@ -1,9 +1,8 @@
|
||||
# cargo-nextest settings for CI (`cargo xtask test` does not use nextest).
|
||||
# CI builds the tests once and runs them in shards: 2 for a pull request, 4 for main and the nightly
|
||||
# (.github/workflows/ci.yml).
|
||||
# PR runs build the tests once and run them in 4 shards (.github/workflows/ci.yml).
|
||||
|
||||
[profile.ci]
|
||||
# Heavy tests (one to twelve minutes each on a CI runner) that pull requests skip. They run on
|
||||
# Heavy tests (one to four minutes each on a CI runner) that pull requests skip. They run on
|
||||
# every push to main and nightly (`--ignore-default-filter`), and in `cargo xtask test`. The
|
||||
# boolean_props property tests run only nightly (ci.yml excludes them from pushes to main).
|
||||
# Keep this list in sync with docs/book/src/dev/testing.md and CONTRIBUTING.md.
|
||||
@@ -18,7 +17,6 @@ not (
|
||||
| test(=cmd::plastic::tests::enclosure_snap_fit_follows_resized_front_wall)
|
||||
))
|
||||
| (package(solidcraft-kernel) & test(=tests::boolean_identity_on_tangent_primitives))
|
||||
| (package(solidcraft-kernel) & test(=tspline::tests::hostile_tsplines_never_panic))
|
||||
| (package(solidcraft-kernel) & binary(boolean_props))
|
||||
| (package(solidcraft-engine) & binary(atomic_commands))
|
||||
| (package(solidcraft-ui-egui) & binary(scenarios) & test(=acceptance_sensor_enclosure))
|
||||
@@ -41,30 +39,3 @@ path = "junit.xml"
|
||||
filter = "binary(action_matrix) & test(=action_matrix_sample)"
|
||||
threads-required = "num-cpus"
|
||||
|
||||
|
||||
# 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 (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_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)
|
||||
| test(=tests::pleat_join::the_pleats_join_at_every_wall_from_2_4_to_2_6_mm)
|
||||
| test(=tests::flush_join_loft::a_flange_flush_with_the_end_of_a_bent_tapered_tube_joins)
|
||||
| test(=cmd::sheet::tests::perforated_sheet_takes_a_thousand_round_holes)
|
||||
| test(=boolean_regression_tests::joined_loft_frames_plan_their_display_at_every_quality)
|
||||
))
|
||||
| (package(solidcraft-engine) & (binary(atomic_commands) | binary(step_export_regressions)))
|
||||
| (package(solidcraft-kernel) & (
|
||||
test(=curveblend::tests::rolling_ball_turns_about_a_rounded_corner_of_its_radius)
|
||||
| test(=tests::fillet_tabbed_plate_outer_loop_and_partial_selection_after_hole)
|
||||
| test(=tspline::tests::hostile_tsplines_never_panic)
|
||||
| test(=fill::tests::regular_rims_of_three_to_eight_sides_fill_with_one_face_and_close_the_cup)
|
||||
| test(/^issue_788_tests::equal_bores_crossing_/)
|
||||
))
|
||||
"""
|
||||
priority = 100
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
name: Bug report
|
||||
description: Something in SolidCraft does the wrong thing
|
||||
labels: [bug]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to report this! Inside the app, **Report an Issue** fills most of this in for you, with the command, its settings and your environment.
|
||||
- type: textarea
|
||||
id: what
|
||||
attributes:
|
||||
label: What happened
|
||||
description: What did you expect, and what happened instead?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: |
|
||||
What you clicked or typed, in order. For a problem on a particular design, `solidcraft-cli script design.solidcraft --out repro.json` turns its history into a script that `solidcraft-cli eval repro.json` replays; attach it, trimmed to the failing step if you can.
|
||||
placeholder: |
|
||||
1. Sketch a circle on the XY plane
|
||||
2. Extrude it 10 mm
|
||||
3. Fillet the top edge with 6 mm
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: SolidCraft version and OS
|
||||
description: Help → About shows the version.
|
||||
placeholder: 0.15.0, Windows 11
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: media
|
||||
attributes:
|
||||
label: Screenshots or recording
|
||||
description: |
|
||||
For anything you can see in the app, a screenshot or short recording of a small example helps the most. Please leave out private designs and personal details.
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Help reproduce and fix a SolidCraft problem
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
## Behavior
|
||||
|
||||
What did you expect, and what happened instead?
|
||||
|
||||
## Reproduction
|
||||
|
||||
Include steps, SolidCraft version/commit and relevant environment details. Prefer a small example without private design data.
|
||||
|
||||
When the problem shows on a particular design, `solidcraft-cli script design.solidcraft --out repro.json` turns its history into a script that `solidcraft-cli eval repro.json` replays; attach it, trimmed to the failing step if you can.
|
||||
|
||||
## Screenshots of the behavior (when observable)
|
||||
|
||||
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.
|
||||
@@ -1,7 +1,5 @@
|
||||
# Blank issues stay on: the app's Report an Issue opens issues/new?title=…&body=… with the report
|
||||
# filled in (crates/ui-egui/src/issue_report.rs), which GitHub drops when blank issues are off.
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Questions and chat
|
||||
url: https://discord.gg/FtU4NQNVDH
|
||||
url: https://solidcraft.io/discord
|
||||
about: Ask questions and talk with users and contributors on the SolidCraft Discord.
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
name: Feature request
|
||||
description: Something SolidCraft should do
|
||||
labels: [enhancement]
|
||||
body:
|
||||
- type: textarea
|
||||
id: goal
|
||||
attributes:
|
||||
label: What are you trying to do
|
||||
description: The part or workflow you have in mind, and where SolidCraft gets in the way today.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: idea
|
||||
attributes:
|
||||
label: How you imagine it working
|
||||
description: Optional. Describe the behaviour in your own words; please don't paste another product's documentation.
|
||||
@@ -1,9 +1,3 @@
|
||||
self-hosted-runner:
|
||||
labels:
|
||||
- self-hosted-fast
|
||||
- self-hosted-light
|
||||
- blacksmith-4vcpu-ubuntu-2404
|
||||
- blacksmith-8vcpu-ubuntu-2404
|
||||
- blacksmith-16vcpu-ubuntu-2404
|
||||
- blacksmith-4vcpu-ubuntu-2404-arm
|
||||
- blacksmith-6vcpu-macos-15
|
||||
|
||||
+64
-267
@@ -5,48 +5,23 @@
|
||||
# those. Changes to the lockfile, root manifest, toolchain/cargo/nextest config, .github/,
|
||||
# vendor/, tests/, scripts/ or any file no package owns select the whole workspace, as do
|
||||
# pushes to main, the nightly run and Run workflow.
|
||||
# Tests are built once and run in shards. A few heavy tests (see .config/nextest.toml) are
|
||||
# Tests are built once and run in 4 shards. A few heavy tests (see .config/nextest.toml) are
|
||||
# skipped on pull requests and run on pushes to main and nightly. The Windows jobs (D3D12 shaders
|
||||
# and app startup) run on pull requests only when what they test changes (the GPU crate, the app,
|
||||
# the viewport's GPU glue, the graphics dependencies, their scripts or this file); main and
|
||||
# nightly always run them. Only main saves the build caches; pull requests restore them.
|
||||
# The merge queue runs the full suite on exactly the commit it then puts on main, so a push to
|
||||
# main that a passing merge-queue run already tested runs only the repository checks; a push
|
||||
# that skipped the queue still runs everything. Pull requests run the reference tests when the
|
||||
# command line (or anything it depends on) changes, and doc tests for the affected packages.
|
||||
# Heavy checks wait for review: a pull request without the `approved-for-ci` label runs only
|
||||
# Select checks, the format and repository checks and a quick compile. Adding the label (after
|
||||
# review) runs everything above, and every later push to the PR does too. Pushes to main, merge
|
||||
# groups, the nightly run and Run workflow are not pull requests and always run the full suite.
|
||||
# GitHub cannot filter `labeled` events by label name, so any other label starts a run whose jobs
|
||||
# are all skipped; it gets its own concurrency group (below) so it never cancels the real run.
|
||||
# Cross-compilation is opt-in through Run workflow; release builds remain in release.yml.
|
||||
# Trusted runs use both pools: pushes to main, merge-queue groups and pull requests from a branch
|
||||
# of this repository (people with write access; fork pull requests never run on this machine).
|
||||
# There, Select checks and ci run on the self-hosted light runner (`self-hosted-light`, quick
|
||||
# jobs, so they never wait behind a build). For pushes to main and the merge queue only, the
|
||||
# repository checks, Clippy, the doc tests, the reference tests and all four test shards run on the
|
||||
# self-hosted runners (`self-hosted-fast`, warm build directories via scripts/ci/warm-target.sh);
|
||||
# a pull request runs them on Blacksmith's runners (blacksmith-<N>vcpu-ubuntu-2404, ephemeral
|
||||
# VMs sized per job), so it never holds up the queue. Every fork PR's Linux jobs run there too.
|
||||
# The self-hosted-untrusted pool (isolated gVisor boxes) is not routed to now; to use it again,
|
||||
# send fork PR jobs to `self-hosted-untrusted` in the `runs-on` of the jobs that had it.
|
||||
# Where the test shards run on Blacksmith's runners, the test archive is built there too: the
|
||||
# self-hosted machine has a newer glibc, so its binaries wouldn't start on Ubuntu 24.04, while the
|
||||
# archive runs there unchanged (same /home/runner/work checkout path). The self-hosted shards
|
||||
# (main and the merge queue) skip the archive and build the tests in their warm build directories.
|
||||
# Blacksmith's runners count as self-hosted to GitHub, so steps tell our machines apart by
|
||||
# runner name (minisforum-*, ci-box-*), not by runner.environment. The Windows jobs run on
|
||||
# GitHub's runners. The nightly run and Run workflow use Blacksmith's runners only; the
|
||||
# self-hosted runners refuse any other job.
|
||||
# Pushes to main and merge-queue groups use both pools: Clippy, the doc tests and test shards
|
||||
# 1-2 run on the self-hosted runner (`self-hosted-fast`, warm build directories via
|
||||
# scripts/ci/warm-target.sh), everything else on GitHub's runners. The test archive is built on
|
||||
# GitHub's runners: the self-hosted machine has a newer glibc, so its binaries wouldn't start
|
||||
# on ubuntu-24.04, while the archive runs there unchanged (same /home/runner/work checkout
|
||||
# path). Pull requests, the nightly run and Run workflow use GitHub's runners only; the
|
||||
# self-hosted runner refuses any job that isn't a push to main or a merge-queue group.
|
||||
name: CI
|
||||
|
||||
on:
|
||||
# Draft pull requests run no CI (see `changes`, `checks` and `ci`): it starts when the PR is
|
||||
# marked ready for review, and on every push after that. `labeled` is there for approved-for-ci
|
||||
# only (other labels are skipped, see the top of this file).
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review, labeled]
|
||||
# The merge queue: each queued group runs the full suite before it lands on main.
|
||||
merge_group:
|
||||
push:
|
||||
@@ -66,10 +41,7 @@ permissions:
|
||||
concurrency:
|
||||
# A new PR push replaces the old run. Only one run per group may wait, so a busy main would
|
||||
# cancel the waiting nightly (or a Run workflow) before it starts: they get groups of their own.
|
||||
# A `labeled` event for any label but approved-for-ci does no work and must not cancel (or queue
|
||||
# behind) the PR's real run, so it gets a group of its own; approved-for-ci replaces the
|
||||
# unlabelled run with the full one.
|
||||
group: ci-${{ github.event_name }}-${{ github.ref }}${{ github.event.action == 'labeled' && github.event.label.name != 'approved-for-ci' && format('-ignored-{0}', github.run_id) || '' }}
|
||||
group: ci-${{ github.event_name }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
@@ -79,7 +51,7 @@ jobs:
|
||||
dependency-advisories:
|
||||
name: Dependency advisories
|
||||
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
@@ -91,28 +63,13 @@ jobs:
|
||||
|
||||
changes:
|
||||
name: Select checks
|
||||
# Not on a draft: every other check needs this one, so they skip too. Not on a label other than
|
||||
# approved-for-ci either (the same skip, for the same reason).
|
||||
if: (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event.action != 'labeled' || github.event.label.name == 'approved-for-ci')
|
||||
# Trusted runs (see the top of this file) use the self-hosted runners.
|
||||
runs-on: ${{ (github.event_name == 'merge_group' || (github.event_name == 'push' && github.ref == 'refs/heads/main') || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)) && 'self-hosted-light' || 'blacksmith-4vcpu-ubuntu-2404' }}
|
||||
permissions:
|
||||
contents: read
|
||||
# To ask whether the merge queue already tested a commit pushed to main.
|
||||
actions: read
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
native: ${{ steps.paths.outputs.native }}
|
||||
kernel: ${{ steps.paths.outputs.kernel }}
|
||||
goldens: ${{ steps.paths.outputs.goldens }}
|
||||
windows: ${{ steps.paths.outputs.windows }}
|
||||
full: ${{ steps.paths.outputs.full }}
|
||||
# Code changed on a pull request that has no approved-for-ci label yet: only Quick compile.
|
||||
quick: ${{ steps.paths.outputs.quick }}
|
||||
packages: ${{ steps.paths.outputs.packages }}
|
||||
oracle: ${{ steps.paths.outputs.oracle }}
|
||||
# The fast self-hosted runners are kept for main and the merge queue (the pre-merge gate);
|
||||
# pull requests, same-repo ones included, run on Blacksmith's runners.
|
||||
local: ${{ (startsWith(runner.name, 'minisforum-') || startsWith(runner.name, 'ci-box-')) && github.event_name != 'pull_request' }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
@@ -123,32 +80,13 @@ jobs:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
PUSH_SHA: ${{ github.sha }}
|
||||
REPO: ${{ github.repository }}
|
||||
APPROVED: ${{ contains(github.event.pull_request.labels.*.name, 'approved-for-ci') }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
native=false
|
||||
kernel=false
|
||||
goldens=false
|
||||
windows=false
|
||||
full=true
|
||||
packages=
|
||||
if [ "$EVENT_NAME" = push ]; then
|
||||
# The queue lands the very commit its merge_group run tested. If that run passed,
|
||||
# testing the push again proves nothing new; otherwise (a merge that skipped the
|
||||
# queue, or the API cannot answer) test everything.
|
||||
# curl and grep only: Select checks of a push runs on the self-hosted runner.
|
||||
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=merge_group&head_sha=$PUSH_SHA&status=success&per_page=1" \
|
||||
| grep -o '"total_count": *[0-9]*' | grep -o '[0-9]*$' || echo 0)
|
||||
if [ "${tested:-0}" -gt 0 ]; then
|
||||
echo "The merge queue already tested $PUSH_SHA: repository checks only."
|
||||
else
|
||||
native=true
|
||||
windows=true
|
||||
fi
|
||||
elif [ "$EVENT_NAME" != pull_request ]; then
|
||||
if [ "$EVENT_NAME" != pull_request ]; then
|
||||
native=true
|
||||
windows=true
|
||||
else
|
||||
@@ -156,10 +94,6 @@ 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 can change a robustness golden: the kernel and the crates a design is built with.
|
||||
case "$path" in
|
||||
crates/kernel/*|crates/engine/*|crates/doc/*|crates/sketch/*|crates/geom/*|vendor/truck-*) goldens=true ;;
|
||||
esac
|
||||
# What the Windows D3D12 jobs exercise: the GPU crate and its shaders, the app and
|
||||
# its startup, the viewport's wgpu glue, the vendored egui-wgpu, their scripts.
|
||||
case "$path" in
|
||||
@@ -170,8 +104,6 @@ jobs:
|
||||
.github/workflows/ci.yml) native=true ;;
|
||||
*.rs|Cargo.toml|Cargo.lock|*/Cargo.toml|.cargo/*|rust-toolchain*) native=true ;;
|
||||
docs/*|*.md|.github/*|packaging/*|LICENSE*|NOTICE|THIRD-PARTY-LICENSES.txt) ;;
|
||||
# The GitHub bots' scripts: no Rust, tested in this job (Claim and triage bot tests).
|
||||
scripts/claim/*|scripts/triage/*) ;;
|
||||
*) native=true ;;
|
||||
esac
|
||||
done < "$RUNNER_TEMP/ci-paths"
|
||||
@@ -188,56 +120,25 @@ jobs:
|
||||
packages=$(sed -n 's/^packages=//p' "$RUNNER_TEMP/ci-affected")
|
||||
# Code changed but nothing was selected: be safe and test everything.
|
||||
if [ "$native" = true ] && [ "$full" != true ] && [ -z "$packages" ]; then full=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.
|
||||
oracle=$native
|
||||
if [ "$EVENT_NAME" = pull_request ] && [ "$full" != true ]; then
|
||||
case " $packages " in *" solidcraft-cli "*) ;; *) oracle=false ;; esac
|
||||
fi
|
||||
# The pull request fast lane: Windows and the reference tests run on main (and the nightly),
|
||||
# not on every pull request push; a break there is fixed forward on main.
|
||||
if [ "$EVENT_NAME" = pull_request ]; then windows=false; oracle=false; fi
|
||||
# Until a maintainer adds approved-for-ci, a pull request gets a quick compile instead of
|
||||
# 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
|
||||
{
|
||||
echo "native=$native"
|
||||
echo "kernel=$kernel"
|
||||
echo "goldens=$goldens"
|
||||
echo "windows=$windows"
|
||||
echo "oracle=$oracle"
|
||||
echo "full=$full"
|
||||
echo "quick=$quick"
|
||||
echo "packages=$packages"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
checks:
|
||||
name: Format and repository checks
|
||||
if: (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event.action != 'labeled' || github.event.label.name == 'approved-for-ci')
|
||||
runs-on: ${{ (github.event_name == 'merge_group' || (github.event_name == 'push' && github.ref == 'refs/heads/main')) && 'self-hosted-fast' || 'blacksmith-4vcpu-ubuntu-2404' }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Warm build directory (self-hosted runner)
|
||||
if: (startsWith(runner.name, 'minisforum-') || startsWith(runner.name, 'ci-box-'))
|
||||
run: scripts/ci/warm-target.sh checks
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: rustfmt
|
||||
# Our self-hosted runners keep their build directory and Cargo home instead.
|
||||
- if: ${{ !(startsWith(runner.name, 'minisforum-') || startsWith(runner.name, 'ci-box-')) }}
|
||||
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: repo-checks
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
@@ -251,53 +152,29 @@ jobs:
|
||||
run: cargo xtask licences --check
|
||||
- name: Dependency layers
|
||||
run: cargo xtask layers
|
||||
- name: Sorted lists
|
||||
run: cargo xtask lists
|
||||
- name: Design format pending changes
|
||||
run: bash scripts/test-format-pending.sh
|
||||
- name: Web app offline use and updates (headless Chrome)
|
||||
run: cargo xtask web-offline-test
|
||||
- name: Affected-package selection self-test
|
||||
run: python3 -I scripts/ci/test_affected.py
|
||||
- name: Claim and triage bot tests
|
||||
run: node --test scripts/claim/*.test.js scripts/triage/*.test.js
|
||||
|
||||
# Fast feedback for a pull request that has not been approved for the full CI yet.
|
||||
quick-compile:
|
||||
name: Quick compile
|
||||
needs: changes
|
||||
if: needs.changes.outputs.quick == 'true'
|
||||
runs-on: ${{ needs.changes.outputs.local == 'true' && 'self-hosted-light' || 'blacksmith-8vcpu-ubuntu-2404' }}
|
||||
timeout-minutes: 30
|
||||
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
|
||||
- if: ${{ !(startsWith(runner.name, 'minisforum-') || startsWith(runner.name, 'ci-box-')) }}
|
||||
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: quick-compile
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: Cargo check
|
||||
run: cargo check --workspace --all-targets --locked
|
||||
- name: Claim bot tests
|
||||
run: node --test scripts/claim/*.test.js
|
||||
|
||||
clippy:
|
||||
name: Native Linux Clippy
|
||||
needs: changes
|
||||
if: needs.changes.outputs.native == 'true'
|
||||
runs-on: ${{ needs.changes.outputs.local == 'true' && 'self-hosted-fast' || 'blacksmith-16vcpu-ubuntu-2404' }}
|
||||
runs-on: ${{ (github.event_name == 'merge_group' || (github.event_name == 'push' && github.ref == 'refs/heads/main')) && 'self-hosted-fast' || 'ubuntu-24.04' }}
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Warm build directory (self-hosted runner)
|
||||
if: (startsWith(runner.name, 'minisforum-') || startsWith(runner.name, 'ci-box-'))
|
||||
if: runner.environment == 'self-hosted'
|
||||
run: scripts/ci/warm-target.sh clippy
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy
|
||||
# Our self-hosted runners keep their build directory and Cargo home instead.
|
||||
- if: ${{ !(startsWith(runner.name, 'minisforum-') || startsWith(runner.name, 'ci-box-')) }}
|
||||
# The self-hosted runner keeps its build directory and Cargo home instead.
|
||||
- if: runner.environment == 'github-hosted'
|
||||
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: native-clippy
|
||||
@@ -315,12 +192,9 @@ jobs:
|
||||
build-tests:
|
||||
name: Build tests
|
||||
needs: changes
|
||||
# The self-hosted shards (main and the merge queue) build from their warm build directories
|
||||
# instead: downloading the 3 GB archive took them longer than building.
|
||||
if: needs.changes.outputs.native == 'true' && needs.changes.outputs.local != 'true'
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
# Fork builds compile inside gVisor on shared boxes; give them room to finish and upload.
|
||||
timeout-minutes: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository && (endsWith(format('{0}', github.event.pull_request.number), '7') || endsWith(format('{0}', github.event.pull_request.number), '8') || endsWith(format('{0}', github.event.pull_request.number), '9'))) && 90 || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) && 60 || 45 }}
|
||||
if: needs.changes.outputs.native == 'true'
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
env:
|
||||
CARGO_INCREMENTAL: "0"
|
||||
RUSTC_WRAPPER: sccache
|
||||
@@ -328,8 +202,6 @@ jobs:
|
||||
SCCACHE_IGNORE_SERVER_IO_ERROR: "1"
|
||||
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
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
@@ -364,14 +236,12 @@ jobs:
|
||||
tests:
|
||||
name: Native Linux tests (${{ matrix.shard }}/4)
|
||||
needs: [changes, build-tests]
|
||||
if: ${{ !cancelled() && needs.changes.outputs.native == 'true' && (needs.changes.outputs.local == 'true' || needs.build-tests.result == 'success') }}
|
||||
runs-on: ${{ needs.changes.outputs.local == 'true' && 'self-hosted-fast' || 'blacksmith-8vcpu-ubuntu-2404' }}
|
||||
runs-on: ${{ ((github.event_name == 'merge_group' || (github.event_name == 'push' && github.ref == 'refs/heads/main')) && matrix.shard <= 2) && 'self-hosted-fast' || 'ubuntu-24.04' }}
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# Pull requests test their affected crates in 2 shards; main and the nightly use 4.
|
||||
shard: ${{ fromJSON(github.event_name == 'pull_request' && '[1, 2]' || '[1, 2, 3, 4]') }}
|
||||
shard: [1, 2, 3, 4]
|
||||
env:
|
||||
# Pictures (snapshots, screenshots, ui.render, the viewport's offscreen tests) are drawn on
|
||||
# the GPU. The runner has none: Mesa's lavapipe (software Vulkan) draws them, and a test that
|
||||
@@ -382,78 +252,48 @@ jobs:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Install a software Vulkan driver (lavapipe)
|
||||
# Preinstalled on the self-hosted runner, which has no sudo.
|
||||
if: ${{ !(startsWith(runner.name, 'minisforum-') || startsWith(runner.name, 'ci-box-')) }}
|
||||
if: runner.environment == 'github-hosted'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends mesa-vulkan-drivers libvulkan1
|
||||
- uses: taiki-e/install-action@1168f6810a3cb30a3514bf5756a3b7f0422c6df8 # v2.87.28
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
# On the self-hosted runners each shard builds the tests in its warm build directory (a few
|
||||
# minutes for what changed); elsewhere it runs the archive the Build tests job made.
|
||||
- name: Size parallel compiles to the job's memory
|
||||
if: needs.changes.outputs.local == 'true'
|
||||
run: scripts/ci/build-jobs.sh
|
||||
- name: Warm build directory (self-hosted runner)
|
||||
if: needs.changes.outputs.local == 'true'
|
||||
run: scripts/ci/warm-target.sh build-tests
|
||||
- if: needs.changes.outputs.local == 'true'
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- if: needs.changes.outputs.local != 'true'
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
name: nextest-archive
|
||||
- name: Run tests
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
GOLDENS: ${{ needs.changes.outputs.goldens }}
|
||||
LOCAL: ${{ needs.changes.outputs.local }}
|
||||
FULL: ${{ needs.changes.outputs.full }}
|
||||
PACKAGES: ${{ needs.changes.outputs.packages }}
|
||||
SHARD: ${{ matrix.shard }}
|
||||
SHARDS: ${{ github.event_name == 'pull_request' && 2 || 4 }}
|
||||
# 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).
|
||||
filter=()
|
||||
all=()
|
||||
# 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.
|
||||
if [ "$EVENT_NAME" != pull_request ]; then
|
||||
# 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.
|
||||
filter=(--ignore-default-filter -E '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.
|
||||
filter=(--ignore-default-filter -E "default() | (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
|
||||
# The warm build directory keeps the last run's junit.xml: never upload that one.
|
||||
rm -f "$junit"
|
||||
if [ "$LOCAL" = true ]; then
|
||||
scope=(--workspace)
|
||||
if [ "$FULL" != true ]; then scope=(); for p in $PACKAGES; do scope+=(-p "$p"); done; fi
|
||||
run=(cargo nextest run "${scope[@]}" --cargo-profile ci --locked)
|
||||
else
|
||||
# Extract into ./target: tests that run a built binary (env!("CARGO_BIN_EXE_*")) have
|
||||
# the build job's path, which is the same checkout path here.
|
||||
run=(cargo nextest run --archive-file tests.tar.zst --workspace-remap . --extract-to . --extract-overwrite)
|
||||
fi
|
||||
if ! "${run[@]}" --profile ci --partition "slice:$SHARD/$SHARDS" --no-tests=pass "${filter[@]}"; 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
|
||||
all=(--ignore-default-filter -E 'not (package(solidcraft-kernel) & binary(boolean_props))')
|
||||
fi
|
||||
# Extract into ./target: tests that run a built binary (env!("CARGO_BIN_EXE_*")) have
|
||||
# the build job's path, which is the same checkout path here.
|
||||
cargo nextest run --archive-file tests.tar.zst --workspace-remap . --extract-to . --extract-overwrite \
|
||||
--profile ci --partition "slice:$SHARD/4" --no-tests=pass "${all[@]}"
|
||||
- name: Robustness goldens sample (pull requests that touch the kernel)
|
||||
# Every 8th seed of tests/robustness/goldens.json (each seed is a test), a quarter per
|
||||
# shard; pushes to main and the nightly run every seed in the step above.
|
||||
if: github.event_name == 'pull_request' && needs.changes.outputs.kernel == 'true'
|
||||
env:
|
||||
SHARD: ${{ matrix.shard }}
|
||||
run: |
|
||||
cargo nextest run --archive-file tests.tar.zst --workspace-remap . --extract-to . --extract-overwrite \
|
||||
--profile ci --ignore-default-filter --partition "slice:$SHARD/4" --no-tests=pass \
|
||||
-E 'package(solidcraft-engine) & test(/^tests::robustness_goldens::seed_(00|08|16|24|32|40|48|56|64|72|80|88)$/)'
|
||||
- name: Upload test timings
|
||||
if: always()
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: junit-${{ matrix.shard }}
|
||||
path: ${{ env.CARGO_TARGET_DIR || 'target' }}/nextest/ci/junit.xml
|
||||
path: target/nextest/ci/junit.xml
|
||||
retention-days: 7
|
||||
|
||||
action-matrix:
|
||||
@@ -464,7 +304,7 @@ jobs:
|
||||
name: Action matrix (${{ matrix.shard }}/16)
|
||||
needs: build-tests
|
||||
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 55
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -506,7 +346,7 @@ jobs:
|
||||
name: Action matrix determinism
|
||||
needs: build-tests
|
||||
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
@@ -528,7 +368,7 @@ jobs:
|
||||
name: Property tests (many cases)
|
||||
needs: build-tests
|
||||
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
@@ -552,7 +392,7 @@ jobs:
|
||||
name: Doc tests
|
||||
needs: changes
|
||||
if: needs.changes.outputs.native == 'true'
|
||||
runs-on: ${{ needs.changes.outputs.local == 'true' && 'self-hosted-fast' || 'blacksmith-8vcpu-ubuntu-2404' }}
|
||||
runs-on: ${{ (github.event_name == 'merge_group' || (github.event_name == 'push' && github.ref == 'refs/heads/main')) && 'self-hosted-fast' || 'ubuntu-24.04' }}
|
||||
timeout-minutes: 45
|
||||
env:
|
||||
CARGO_INCREMENTAL: "0"
|
||||
@@ -561,14 +401,12 @@ jobs:
|
||||
SCCACHE_IGNORE_SERVER_IO_ERROR: "1"
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Size parallel compiles to the job's memory
|
||||
run: scripts/ci/build-jobs.sh
|
||||
- name: Warm build directory (self-hosted runner)
|
||||
if: (startsWith(runner.name, 'minisforum-') || startsWith(runner.name, 'ci-box-'))
|
||||
if: runner.environment == 'self-hosted'
|
||||
run: scripts/ci/warm-target.sh doc-tests
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
# Our self-hosted runners keep their build directory and Cargo home instead.
|
||||
- if: ${{ !(startsWith(runner.name, 'minisforum-') || startsWith(runner.name, 'ci-box-')) }}
|
||||
# The self-hosted runner keeps its build directory and Cargo home instead.
|
||||
- if: runner.environment == 'github-hosted'
|
||||
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: doc-tests
|
||||
@@ -578,33 +416,13 @@ jobs:
|
||||
with:
|
||||
version: v0.16.0
|
||||
# nextest does not run doc tests.
|
||||
- name: Doc tests
|
||||
env:
|
||||
FULL: ${{ needs.changes.outputs.full }}
|
||||
PACKAGES: ${{ needs.changes.outputs.packages }}
|
||||
run: |
|
||||
# Pull requests test the affected packages that have a library (only libraries have doc
|
||||
# tests); everything else, the whole workspace.
|
||||
scope=(--workspace)
|
||||
if [ "$FULL" != true ]; then
|
||||
scope=()
|
||||
libs=$(cargo metadata --no-deps --format-version 1 --locked \
|
||||
| python3 -I -c 'import json, sys; print(" ".join(p["name"] for p in json.load(sys.stdin)["packages"] if any("lib" in t["kind"] for t in p["targets"])))')
|
||||
for p in $PACKAGES; do
|
||||
case " $libs " in *" $p "*) scope+=(-p "$p") ;; esac
|
||||
done
|
||||
fi
|
||||
if [ ${#scope[@]} -eq 0 ]; then
|
||||
echo "No affected package has a library: no doc tests to run."
|
||||
exit 0
|
||||
fi
|
||||
cargo test "${scope[@]}" --profile ci --locked --doc
|
||||
- run: cargo test --workspace --profile ci --locked --doc
|
||||
|
||||
oracle:
|
||||
name: Reference tests
|
||||
needs: changes
|
||||
if: needs.changes.outputs.oracle == 'true'
|
||||
runs-on: ${{ needs.changes.outputs.local == 'true' && 'self-hosted-fast' || 'blacksmith-8vcpu-ubuntu-2404' }}
|
||||
if: needs.changes.outputs.native == 'true'
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
CARGO_INCREMENTAL: "0"
|
||||
@@ -613,15 +431,8 @@ jobs:
|
||||
SCCACHE_IGNORE_SERVER_IO_ERROR: "1"
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Size parallel compiles to the job's memory
|
||||
run: scripts/ci/build-jobs.sh
|
||||
- name: Warm build directory (self-hosted runner)
|
||||
if: (startsWith(runner.name, 'minisforum-') || startsWith(runner.name, 'ci-box-'))
|
||||
run: scripts/ci/warm-target.sh oracle
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
# Our self-hosted runners keep their build directory and Cargo home instead.
|
||||
- if: ${{ !(startsWith(runner.name, 'minisforum-') || startsWith(runner.name, 'ci-box-')) }}
|
||||
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: oracle
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
@@ -632,7 +443,7 @@ jobs:
|
||||
- name: Build the command line
|
||||
run: cargo build -p solidcraft-cli --profile ci --locked
|
||||
- name: Replay tests/oracle and compare with expected.json
|
||||
run: cargo xtask oracle --cli "${CARGO_TARGET_DIR:-target}/ci/solidcraft-cli"
|
||||
run: cargo xtask oracle --cli target/ci/solidcraft-cli
|
||||
- name: Upload the report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
@@ -699,23 +510,16 @@ jobs:
|
||||
|
||||
# Preserve the existing check name for branch protection, including docs-only PRs.
|
||||
ci:
|
||||
# On the skipped run another label starts, this job is named differently, so that skipped job
|
||||
# can never stand in for the required `ci` check of the pull request's real run.
|
||||
name: ${{ github.event.action == 'labeled' && github.event.label.name != 'approved-for-ci' && 'ci (ignored label)' || 'ci' }}
|
||||
needs: [changes, checks, quick-compile, clippy, build-tests, tests, doc-tests, oracle, windows-fxc, windows-startup]
|
||||
if: always() && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event.action != 'labeled' || github.event.label.name == 'approved-for-ci')
|
||||
runs-on: ${{ needs.changes.outputs.local == 'true' && 'self-hosted-light' || 'blacksmith-4vcpu-ubuntu-2404' }}
|
||||
needs: [changes, checks, clippy, build-tests, tests, doc-tests, oracle, windows-fxc, windows-startup]
|
||||
if: always()
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Verify required checks
|
||||
env:
|
||||
CHANGES: ${{ needs.changes.result }}
|
||||
CHECKS: ${{ needs.checks.result }}
|
||||
NATIVE: ${{ needs.changes.outputs.native }}
|
||||
QUICK: ${{ needs.changes.outputs.quick }}
|
||||
QUICK_COMPILE: ${{ needs.quick-compile.result }}
|
||||
LOCAL: ${{ needs.changes.outputs.local }}
|
||||
WINDOWS: ${{ needs.changes.outputs.windows }}
|
||||
ORACLE_RUN: ${{ needs.changes.outputs.oracle }}
|
||||
CLIPPY: ${{ needs.clippy.result }}
|
||||
BUILD: ${{ needs.build-tests.result }}
|
||||
TESTS: ${{ needs.tests.result }}
|
||||
@@ -726,18 +530,11 @@ jobs:
|
||||
run: |
|
||||
test "$CHANGES" = success
|
||||
test "$CHECKS" = success
|
||||
# An unapproved pull request: the heavy jobs are skipped on purpose.
|
||||
if [ "$QUICK" = true ]; then
|
||||
test "$QUICK_COMPILE" = success
|
||||
fi
|
||||
if [ "$NATIVE" = true ]; then
|
||||
test "$CLIPPY" = success
|
||||
# The self-hosted test shards build their own tests (no Build tests job there).
|
||||
[ "$BUILD" = success ] || { [ "$LOCAL" = true ] && [ "$BUILD" = skipped ]; }
|
||||
test "$BUILD" = success
|
||||
test "$TESTS" = success
|
||||
test "$DOCS" = success
|
||||
fi
|
||||
if [ "$ORACLE_RUN" = true ]; then
|
||||
test "$ORACLE" = success
|
||||
fi
|
||||
if [ "$WINDOWS" = true ]; then
|
||||
@@ -748,7 +545,7 @@ jobs:
|
||||
wasm:
|
||||
name: Compile WebAssembly (manual)
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.cross_compile
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
@@ -764,7 +561,7 @@ jobs:
|
||||
riscv64-gates:
|
||||
name: Compile RISC-V (manual)
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.cross_compile
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
# `/claim` and `/unclaim` on issues (CONTRIBUTING.md, "Sending a change"), and a daily sweep that
|
||||
# frees claims nobody is working on. A pull request that references an issue (`<summary> (#N)`,
|
||||
# `Fixes #N`) claims it too. The logic is in scripts/claim/claim.js (tested in ci.yml).
|
||||
# frees claims nobody is working on. The logic is in scripts/claim/claim.js (tested in ci.yml).
|
||||
name: Claim
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
# pull_request_target so PRs from forks can label and assign. It runs the base branch's
|
||||
# scripts/claim and only reads the PR's title, body and author; the PR's code is never checked out.
|
||||
pull_request_target:
|
||||
types: [opened, reopened]
|
||||
schedule:
|
||||
- cron: '17 5 * * *'
|
||||
workflow_dispatch:
|
||||
@@ -21,13 +16,13 @@ permissions:
|
||||
|
||||
# One run at a time per issue, so two simultaneous /claim comments can't both win.
|
||||
concurrency:
|
||||
group: claim-${{ github.event.issue.number || github.event.pull_request.number || 'sweep' }}
|
||||
group: claim-${{ github.event.issue.number || 'sweep' }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
claim:
|
||||
name: Claim bot
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
if: >-
|
||||
github.event_name != 'issue_comment' ||
|
||||
@@ -43,9 +38,6 @@ jobs:
|
||||
const claim = require('./scripts/claim/claim.js');
|
||||
if (context.eventName === 'issue_comment') {
|
||||
await claim.handleComment({ github, context });
|
||||
} else if (context.eventName === 'pull_request_target') {
|
||||
const claimed = await claim.handlePull({ github, context });
|
||||
core.info(`Claimed: ${claimed.join(', ') || 'none'}`);
|
||||
} else {
|
||||
const released = await claim.sweep({ github, context });
|
||||
core.info(`Released: ${released.join(', ') || 'none'}`);
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
# The web app as a container image on GitHub's container registry, for self-hosting
|
||||
# (packaging/docker; the book's "Self-hosting with Docker"):
|
||||
#
|
||||
# ghcr.io/<owner>/solidcraft-web:latest the latest published release
|
||||
# ghcr.io/<owner>/solidcraft-web:0.14.0 a release (also 0.14); pre-releases get only their version
|
||||
# ghcr.io/<owner>/solidcraft-web:edge main
|
||||
#
|
||||
# For linux/amd64 and linux/arm64. The site is built here, as Pages and Release build it, and
|
||||
# passed to the Dockerfile as its `site` stage. Pull requests that touch the image build and
|
||||
# smoke-test it without publishing. Releases published by an agent or maintainer trigger this;
|
||||
# the nightly pre-releases (made with GITHUB_TOKEN) do not.
|
||||
name: Docker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
release:
|
||||
types: [published]
|
||||
pull_request:
|
||||
paths: ["packaging/docker/**", ".github/workflows/docker.yml"]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: docker-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
TRUNK_VERSION: v0.21.14
|
||||
|
||||
jobs:
|
||||
image:
|
||||
if: ${{ !github.event.repository.private }}
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: wasm32-unknown-unknown
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: docker-web
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: Install trunk
|
||||
run: |
|
||||
mkdir -p "$HOME/.local/bin"
|
||||
curl -fsSL "https://github.com/trunk-rs/trunk/releases/download/${TRUNK_VERSION}/trunk-x86_64-unknown-linux-gnu.tar.gz" | tar -xz -C "$HOME/.local/bin"
|
||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||
- name: Build the web app
|
||||
working-directory: apps/solidcraft-web
|
||||
run: trunk build --release
|
||||
- name: Image name
|
||||
id: image
|
||||
# Registry names are lower case; a fork's owner may not be.
|
||||
run: echo "name=ghcr.io/${GITHUB_REPOSITORY_OWNER,,}/solidcraft-web" >> "$GITHUB_OUTPUT"
|
||||
- uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
||||
id: meta
|
||||
with:
|
||||
images: ${{ steps.image.outputs.name }}
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=edge,branch=main
|
||||
type=ref,event=pr
|
||||
labels: |
|
||||
org.opencontainers.image.title=SolidCraft (web)
|
||||
org.opencontainers.image.description=SolidCraft, open-source parametric CAD, in the browser: the web app served by nginx on port 8080
|
||||
org.opencontainers.image.licenses=MIT OR Apache-2.0
|
||||
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||
- name: Build for the smoke test
|
||||
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
|
||||
with:
|
||||
context: .
|
||||
file: packaging/docker/Dockerfile
|
||||
build-contexts: site=dist/web
|
||||
platforms: linux/amd64
|
||||
load: true
|
||||
tags: solidcraft-web:test
|
||||
- name: Smoke test
|
||||
run: bash packaging/docker/smoke-test.sh solidcraft-web:test
|
||||
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build and publish
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
|
||||
with:
|
||||
context: .
|
||||
file: packaging/docker/Dockerfile
|
||||
build-contexts: site=dist/web
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
annotations: ${{ steps.meta.outputs.annotations }}
|
||||
@@ -21,7 +21,7 @@ concurrency:
|
||||
jobs:
|
||||
publish:
|
||||
name: Request the combined docs, web, and Flatpak deployment
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-24.04
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RELEASE_TAG: ${{ github.event.release.tag_name || inputs.release_tag }}
|
||||
|
||||
@@ -18,7 +18,7 @@ permissions:
|
||||
jobs:
|
||||
regenerate:
|
||||
name: Regenerate FreeCAD expected values
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
@@ -19,7 +19,7 @@ concurrency:
|
||||
jobs:
|
||||
regenerate:
|
||||
name: Regenerate and commit
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
# Always main itself, never pull request code, since this job can push.
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
# Files many pull requests add to are kept so that additions don't conflict: one file per change
|
||||
# or one sorted line per item (CONTRIBUTING.md, "Lists many changes add to"). Fast, no build.
|
||||
# Pull requests get the same checks in CI's "Format and repository checks" job (one runner slot
|
||||
# per PR instead of three); this copy keeps main honest.
|
||||
name: Lists
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
@@ -14,7 +13,7 @@ permissions:
|
||||
jobs:
|
||||
format-changes:
|
||||
name: Design format changes merge cleanly
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
@@ -22,7 +21,7 @@ jobs:
|
||||
|
||||
sorted:
|
||||
name: Lists are sorted one item per line
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
@@ -8,7 +8,7 @@ on:
|
||||
branches: [main]
|
||||
paths: [".github/workflows/**", "packaging/**", "assets/**"]
|
||||
pull_request:
|
||||
paths: [".github/workflows/**", "packaging/**", "assets/platforms/**"]
|
||||
paths: [".github/workflows/**", "packaging/**", "assets/**"]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
@@ -20,7 +20,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
ACTIONLINT_VERSION: 1.7.12
|
||||
steps:
|
||||
|
||||
@@ -31,7 +31,7 @@ env:
|
||||
jobs:
|
||||
build:
|
||||
if: ${{ !github.event.repository.private }}
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
@@ -82,7 +82,7 @@ jobs:
|
||||
|
||||
deploy:
|
||||
needs: build
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
|
||||
@@ -45,7 +45,7 @@ env:
|
||||
|
||||
jobs:
|
||||
version:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
build: ${{ steps.changed.outputs.build }}
|
||||
version: ${{ steps.v.outputs.version }}
|
||||
@@ -172,8 +172,8 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- { arch: x86_64, runner: blacksmith-4vcpu-ubuntu-2404 }
|
||||
- { arch: aarch64, runner: blacksmith-4vcpu-ubuntu-2404-arm }
|
||||
- { arch: x86_64, runner: ubuntu-24.04 }
|
||||
- { arch: aarch64, runner: ubuntu-24.04-arm }
|
||||
env:
|
||||
SOLIDCRAFT_VERSION: ${{ needs.version.outputs.version }}
|
||||
SOLIDCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
|
||||
@@ -210,8 +210,8 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- { arch: x86_64, runner: blacksmith-4vcpu-ubuntu-2404 }
|
||||
- { arch: aarch64, runner: blacksmith-4vcpu-ubuntu-2404-arm }
|
||||
- { arch: x86_64, runner: ubuntu-24.04 }
|
||||
- { arch: aarch64, runner: ubuntu-24.04-arm }
|
||||
env:
|
||||
SOLIDCRAFT_VERSION: ${{ needs.version.outputs.version }}
|
||||
SOLIDCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
|
||||
@@ -257,7 +257,7 @@ jobs:
|
||||
needs: version
|
||||
if: needs.version.outputs.build == 'true'
|
||||
# GitHub has no FreeBSD runners: build in a FreeBSD 14.3 VM.
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 180
|
||||
env:
|
||||
SOLIDCRAFT_VERSION: ${{ needs.version.outputs.version }}
|
||||
@@ -297,7 +297,7 @@ jobs:
|
||||
name: macOS universal
|
||||
needs: version
|
||||
if: needs.version.outputs.build == 'true'
|
||||
runs-on: blacksmith-6vcpu-macos-15
|
||||
runs-on: macos-14
|
||||
env:
|
||||
SOLIDCRAFT_VERSION: ${{ needs.version.outputs.version }}
|
||||
SOLIDCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
|
||||
@@ -364,7 +364,7 @@ jobs:
|
||||
name: Web (wasm)
|
||||
needs: version
|
||||
if: needs.version.outputs.build == 'true'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
SOLIDCRAFT_VERSION: ${{ needs.version.outputs.version }}
|
||||
TRUNK_VERSION: 0.21.14
|
||||
@@ -392,7 +392,7 @@ jobs:
|
||||
name: GitHub Release or dry-run artifacts
|
||||
needs: [version, linux, flatpak, snap, freebsd, macos, windows, web]
|
||||
if: needs.version.outputs.build == 'true'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
env:
|
||||
|
||||
@@ -17,7 +17,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
promote:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: creds
|
||||
name: Check for Snap Store credentials
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
# Labels new issues and PRs from fixed rules, `/triage` for maintainers, asks short bug reports
|
||||
# for reproduction steps (`needs-info`, closed after 14 days unanswered), and a manual backfill
|
||||
# (CONTRIBUTING.md, "Labels and triage"). The logic is in scripts/triage/ (tested in ci.yml).
|
||||
name: Triage
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, labeled, edited]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
# pull_request_target so PRs from forks can be labelled. It runs the base branch's
|
||||
# scripts/triage and only lists the PR's changed paths; the PR's code is never checked out.
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
schedule:
|
||||
- cron: '41 5 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
description: 'Only report what would be labelled'
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
permissions:
|
||||
contents: read # to check out scripts/triage
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: triage-${{ github.event.issue.number || github.event.pull_request.number || 'repo' }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
name: Triage bot
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 30
|
||||
# Filtered here so the common events (every comment, every label) start no runner.
|
||||
if: >-
|
||||
(github.event_name == 'issue_comment' &&
|
||||
github.event.comment.user.type != 'Bot' && startsWith(github.event.comment.body, '/triage')) ||
|
||||
((github.event_name == 'issue_comment' || github.event.action == 'edited') &&
|
||||
contains(github.event.issue.labels.*.name, 'needs-info') &&
|
||||
github.event.sender.login == github.event.issue.user.login) ||
|
||||
(github.event.action == 'labeled' && startsWith(github.event.label.name, 'priority:') &&
|
||||
contains(github.event.issue.labels.*.name, 'needs-triage')) ||
|
||||
(github.event_name != 'issue_comment' && github.event.action != 'labeled' && github.event.action != 'edited')
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
sparse-checkout: scripts/triage
|
||||
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
env:
|
||||
DRY_RUN: ${{ inputs.dry_run }}
|
||||
with:
|
||||
script: |
|
||||
const triage = require('./scripts/triage/triage.js');
|
||||
const info = require('./scripts/triage/needs-info.js');
|
||||
const event = context.eventName;
|
||||
const action = context.payload.action;
|
||||
const esc = (t) => String(t).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c]);
|
||||
const th = (...names) => names.map((data) => ({ data, header: true }));
|
||||
if (event === 'issue_comment') {
|
||||
core.info(await triage.handleComment({ github, context }));
|
||||
await info.handleReply({ github, context });
|
||||
} else if (event === 'issues' && action === 'labeled') {
|
||||
await triage.handleLabeled({ github, context });
|
||||
} else if (event === 'issues' && action === 'edited') {
|
||||
await info.handleReply({ github, context });
|
||||
} else if (event === 'schedule') {
|
||||
const closed = await info.sweep({ github, context });
|
||||
core.info(`Closed for missing information: ${closed.join(', ') || 'none'}`);
|
||||
} else if (event === 'workflow_dispatch') {
|
||||
const dryRun = process.env.DRY_RUN !== 'false';
|
||||
const plan = await triage.backfill({ github, context, dryRun });
|
||||
const asks = await info.backfill({ github, context, dryRun });
|
||||
await core.summary
|
||||
.addHeading(dryRun ? `Backfill dry run: ${plan.length} unlabelled` : `Backfill: ${plan.length} labelled`)
|
||||
.addTable([th('Item', 'Kind', 'Labels', 'Title'), ...plan.map((p) => [`#${p.number}`, p.pull ? 'PR' : 'issue', esc(p.labels.join(', ') || '—'), esc(p.title)])])
|
||||
.addHeading(dryRun ? `needs-info dry run: ${asks.length} would be asked` : `needs-info: ${asks.length} asked`)
|
||||
.addTable([th('Issue', 'Asks', 'Title'), ...asks.map((p) => [`#${p.number}`, esc(p.asks.join(', ')), esc(p.title)])])
|
||||
.write();
|
||||
} else {
|
||||
core.info(`Labelled: ${(await triage.handleOpened({ github, context })).join(', ') || 'nothing'}`);
|
||||
if (event === 'issues') await info.handleOpened({ github, context });
|
||||
}
|
||||
@@ -29,7 +29,7 @@ env:
|
||||
|
||||
jobs:
|
||||
submit:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: check
|
||||
name: Check for the token and the package
|
||||
|
||||
@@ -7,6 +7,3 @@
|
||||
/.claude/worktrees/
|
||||
/target-snapshot
|
||||
__pycache__/
|
||||
|
||||
# cargo-mutants output (docs/mutation-testing.md)
|
||||
mutants.out*/
|
||||
|
||||
@@ -132,13 +132,9 @@ People trust SolidCraft with their designs; a crash loses their work. **This out
|
||||
- Before working on issue #N, check nobody has it: `gh pr list -R bherbruck/solidcraft --state open --search "#N"`.
|
||||
- If none, branch from the latest `origin/main`, `git commit --allow-empty -m "claim: #N"`, push,
|
||||
and open a **draft** PR titled `<summary> (#N)` right away. **The lowest-numbered open PR for an
|
||||
issue wins**; if yours lost, close it. A comment or file edit alone does not replace the draft PR.
|
||||
- After opening the draft PR, comment `/claim` on the issue so the bot records the claimant and
|
||||
adds the `in progress` label. **Do both:** the draft PR establishes ownership and `/claim` marks
|
||||
the issue as taken. Reference `#N` in the PR so the bot can find it.
|
||||
issue wins**; if yours lost, close it. Don't claim by commenting or by editing files.
|
||||
- Push work in progress at least daily. A draft with no pushes for 72 hours may be taken over:
|
||||
comment on it first, then open your own claim (you may start from its branch).
|
||||
This takeover policy is separate from the bot's automatic cleanup deadlines in CONTRIBUTING.md.
|
||||
- On a shared build machine, check the load before a big build (`cut -d' ' -f1 /proc/loadavg`)
|
||||
and wait while it is over 24; build with `nice`.
|
||||
|
||||
|
||||
+1
-17
@@ -32,13 +32,6 @@ Editable drawing code, native projects and shipped exports are collected under `
|
||||
| docs/screenshots/demo-0.8.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.8 English desktop UI modelling an original Raspberry Pi 4 case (no logos or branding; the engraved text is set in the system's DejaVu Sans Bold), generated with examples/demo/record_ui.sh and its original animated pointer overlay (9× playback) | MIT OR Apache-2.0 |
|
||||
| docs/screenshots/rover-build.gif | 0xlillith (model); SolidCraft contributors (recording) | screen recording of the actual SolidCraft desktop UI replaying the timeline of the SELENE rover (`Rover-recorded.solidcraft` from https://github.com/0xlillith/solidcraft-rover, an original model by 0xlillith built through SolidCraft MCP); the model's lettering follows Ubuntu Light outlines (Canonical Ltd., Ubuntu Font Licence 1.0, as noted in that repository); the emblem is the SolidCraft project icon (2× playback) | MIT OR Apache-2.0 |
|
||||
| docs/screenshots/rover-build.mp4 | 0xlillith (model); SolidCraft contributors (recording) | screen recording of the actual SolidCraft desktop UI replaying the timeline of the SELENE rover (`Rover-recorded.solidcraft` from https://github.com/0xlillith/solidcraft-rover, an original model by 0xlillith built through SolidCraft MCP); the model's lettering follows Ubuntu Light outlines (Canonical Ltd., Ubuntu Font Licence 1.0, as noted in that repository); the emblem is the SolidCraft project icon | MIT OR Apache-2.0 |
|
||||
| docs/design/ux-redesign/model-floating.png | SolidCraft contributors | original UX mockup (Model tab, floating layout) rendered from an HTML mockup that uses the original Technical Accent icon set; no third-party artwork, and not a screenshot of any other CAD product | MIT OR Apache-2.0 |
|
||||
| docs/design/ux-redesign/model-compact.png | SolidCraft contributors | original UX mockup (Model tab, compact docked layout) rendered from an HTML mockup that uses the original Technical Accent icon set; no third-party artwork, and not a screenshot of any other CAD product | MIT OR Apache-2.0 |
|
||||
| docs/design/ux-redesign/sketch.png | SolidCraft contributors | original UX mockup (Sketch tab) rendered from an HTML mockup that uses the original Technical Accent icon set; no third-party artwork, and not a screenshot of any other CAD product | MIT OR Apache-2.0 |
|
||||
| docs/design/ux-redesign/sheet-metal.png | SolidCraft contributors | original UX mockup (Sheet Metal contextual tab) rendered from an HTML mockup that uses the original Technical Accent icon set; no third-party artwork, and not a screenshot of any other CAD product | MIT OR Apache-2.0 |
|
||||
| docs/design/ux-redesign/start-page.png | SolidCraft contributors | original UX mockup (start page (File view)) rendered from an HTML mockup that uses the original Technical Accent icon set; no third-party artwork, and not a screenshot of any other CAD product | MIT OR Apache-2.0 |
|
||||
| docs/design/ux-redesign/workspaces.png | SolidCraft contributors | original UX mockup (workspace switcher) rendered from an HTML mockup that uses the original Technical Accent icon set; no third-party artwork, and not a screenshot of any other CAD product | MIT OR Apache-2.0 |
|
||||
| docs/design/ux-redesign/command-map.png | SolidCraft contributors | original UX mockup (ribbon command map) rendered from an HTML mockup that uses the original Technical Accent icon set; no third-party artwork, and not a screenshot of any other CAD product | MIT OR Apache-2.0 |
|
||||
| assets/branding/web/source/og.png | SolidCraft contributors | original 1280 × 640 social preview card (the SolidCraft mark, name and tagline), served by the docs site for link previews | MIT OR Apache-2.0 |
|
||||
| docs/book/src/generated/sample-plate.png | SolidCraft contributors | copy of docs/screenshots/sample-plate.png made by `cargo xtask book` (gitignored) | MIT OR Apache-2.0 |
|
||||
| docs/book/src/generated/bracket.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot examples/bracket.json`; gitignored) | MIT OR Apache-2.0 |
|
||||
@@ -93,11 +86,8 @@ Editable drawing code, native projects and shipped exports are collected under `
|
||||
| assets/locales/es.json | jawler | Spanish (Spain) translation of the English catalog, contributed upstream | MIT OR Apache-2.0 |
|
||||
| assets/locales/fr.json | rlartot | French translation of the English catalog, contributed upstream | MIT OR Apache-2.0 |
|
||||
| assets/locales/gl.json | Pablo López (pablopeza) | Galician translation of the English catalog, contributed upstream | MIT OR Apache-2.0 |
|
||||
| assets/locales/it.json | SolidCraft contributors | Original Italian translation of SolidCraft's English catalog; no commercial CAD text used | MIT OR Apache-2.0 |
|
||||
| assets/locales/ja.json | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
| assets/locales/ko.json | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
| assets/locales/ms.json | A.Hakim (a-hakim) | Malay translation of the English message catalog | MIT OR Apache-2.0 |
|
||||
| assets/locales/pl.json | cyryllo | Polish translation of the English catalog, contributed upstream | MIT OR Apache-2.0 |
|
||||
| assets/locales/ru.json | Alex Pro (del000) | Russian translation of the English catalog, contributed upstream | MIT OR Apache-2.0 |
|
||||
| assets/locales/pt-BR.json | DuBearte | Brazilian Portuguese translation of the English catalog, contributed upstream | MIT OR Apache-2.0 |
|
||||
| assets/locales/tr.json | macrulz54 | Turkish translation of the English catalog, contributed upstream | MIT OR Apache-2.0 |
|
||||
@@ -113,12 +103,6 @@ Editable drawing code, native projects and shipped exports are collected under `
|
||||
| assets/platforms/windows/solidcraft.wxs | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
|
||||
| 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/web/loading-logo.svg | SolidCraft contributors | Animation of the SolidCraft mark, built from the paths and gradients of assets/icons/app/macos/source/SolidCraft.icon/Assets/03-solid-blue-cutout.svg; original animation | MIT OR Apache-2.0 |
|
||||
| assets/styles/web/loading.css | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
| crates/io/src/native_cad/fixtures/block.FCStd | SolidCraft contributors | original 10 × 20 × 30 mm test box saved by FreeCAD 1.1.4 run as an external program (`make_block_fcstd.py` beside it); no third-party geometry | MIT OR Apache-2.0 |
|
||||
@@ -471,7 +455,7 @@ copied into SolidCraft.
|
||||
| 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 (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/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 (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
|
||||
|
||||
@@ -27,10 +27,8 @@ new uses of it anywhere else. Public docs/web/Flatpak deployments use project Pa
|
||||
## Never crash
|
||||
People trust SolidCraft with their designs; a crash loses their work. **This outranks feature work.**
|
||||
- **No panics in non-test code:** no `unwrap()`, `expect()`, `panic!`, `unreachable!`, `todo!`,
|
||||
`unimplemented!`; no `unsafe` (`unsafe_code = "forbid"`). The exceptions, in the app crate and in
|
||||
`crates/threedx-client` (which deny it instead), have no safe API: the macOS-only
|
||||
`crates/threedx-client/src/macos.rs` (loads the 3Dconnexion driver's client framework at run time
|
||||
and calls through its function pointers), two functions in
|
||||
`unimplemented!`; no `unsafe` (`unsafe_code = "forbid"`). The three exceptions, in the app crate
|
||||
(which denies it instead), have no safe API: two functions in
|
||||
`apps/solidcraft/src/open_documents.rs` (macOS hands Finder files only to an Objective-C
|
||||
method), the Windows-only `apps/solidcraft/src/native_crash.rs` (the exception filter that
|
||||
records crashes inside native code such as graphics drivers) and the Windows-only
|
||||
@@ -129,8 +127,7 @@ People trust SolidCraft with their designs; a crash loses their work. **This out
|
||||
- Before working on issue #N, check nobody has it: `gh pr list -R bherbruck/solidcraft --state open --search "#N"`.
|
||||
- If none, branch from the latest `origin/main`, `git commit --allow-empty -m "claim: #N"`, push,
|
||||
and open a **draft** PR titled `<summary> (#N)` right away. **The lowest-numbered open PR for an
|
||||
issue wins**; if yours lost, close it. Opening the PR is the claim: the bot assigns you and
|
||||
labels the issue `in progress` (or says on your PR who holds it). Don't claim by editing files.
|
||||
issue wins**; if yours lost, close it. Don't claim by commenting or by editing files.
|
||||
- Push work in progress at least daily. A draft with no pushes for 72 hours may be taken over:
|
||||
comment on it first, then open your own claim (you may start from its branch).
|
||||
- On a shared build machine, check the load before a big build (`cut -d' ' -f1 /proc/loadavg`)
|
||||
|
||||
+31
-88
@@ -10,10 +10,7 @@ Help is welcome, whether that's a one-line fix or owning a whole area. This page
|
||||
build it, how the code is laid out, the few rules that aren't negotiable, and where help is
|
||||
needed most.
|
||||
|
||||
Before choosing a task, check the [command inventory](docs/features.md) and
|
||||
[roadmap](ROADMAP.md) for implemented capabilities and current priorities.
|
||||
|
||||
Questions, ideas or want to say what you're working on? Join the [Discord](https://discord.gg/FtU4NQNVDH).
|
||||
Questions, ideas or want to say what you're working on? Join the [Discord](https://solidcraft.io/discord).
|
||||
|
||||
## Get it running (about 10 minutes on a fast machine)
|
||||
|
||||
@@ -113,10 +110,9 @@ The [README](README.md#architecture) has the layer table. The short version:
|
||||
`cargo xtask assets` enforces this.
|
||||
4. **Never crash.** People trust a CAD app with their work.
|
||||
- Non-test code never uses `unwrap`, `expect`, `panic!` or indexing on input-derived data.
|
||||
There's no `unsafe` outside `apps/solidcraft/src/open_documents.rs` (macOS file-open events),
|
||||
`apps/solidcraft/src/native_crash.rs` (Windows native crash capture) and
|
||||
`crates/threedx-client/src/macos.rs` (the 3Dconnexion driver's client API on macOS), and the
|
||||
compiler denies it.
|
||||
There's no `unsafe` outside `apps/solidcraft/src/open_documents.rs` (macOS file-open events)
|
||||
and `apps/solidcraft/src/native_crash.rs` (Windows native crash capture), and the compiler
|
||||
denies it.
|
||||
- An unsupported case returns an error ("not supported yet: …"); it never panics.
|
||||
- Every call into truck goes through `solidcraft_kernel::guard`.
|
||||
- Every crash fix comes with a regression test.
|
||||
@@ -130,10 +126,6 @@ The [README](README.md#architecture) has the layer table. The short version:
|
||||
`navigation::handle`; never read drags, the wheel or pinch to move the camera yourself. The
|
||||
`hand_rolled_camera_input_is_rejected` test fails if a page does.
|
||||
|
||||
Labels, parameter docs, messages and docs use the terms in the
|
||||
[glossary](https://docs.solidcraft.io/reference/glossary.html): one term for each idea. A change that adds a new
|
||||
idea adds its term there.
|
||||
|
||||
[AGENTS.md](AGENTS.md) has the long form of these rules. It's written for AI agents, but it's
|
||||
the most complete description of the house style.
|
||||
|
||||
@@ -145,12 +137,6 @@ 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) |
|
||||
|
||||
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.
|
||||
|
||||
**UI scenarios** drive the real app headless: start a tool, click a plane or a sketch point,
|
||||
press keys, then assert on the result.
|
||||
|
||||
@@ -180,28 +166,25 @@ in the PR. `cargo xtask test` builds the tests with it and then runs every test
|
||||
`cargo xtask test -- shell`. Tests run beside each other on a busy machine, so a test must not
|
||||
depend on wall-clock timing.
|
||||
|
||||
**Ready pull requests test what they touch.** CI maps changed files to workspace packages and
|
||||
runs Clippy, the test build and tests for those and their dependents. The
|
||||
[package selector](scripts/ci/affected.py) and [CI workflow](.github/workflows/ci.yml) define
|
||||
which changes require native checks; documentation, packaging and bot-script-only PRs can run
|
||||
repository checks only. Workspace-wide changes, merge-queue groups, nightly runs and Run
|
||||
workflow select the full workspace. A push to `main` reuses a successful merge-queue run for
|
||||
the same commit; otherwise it runs the full suite.
|
||||
**PRs test what they touch; main and nightly test everything.** On a pull request, CI maps the
|
||||
changed files to workspace packages and runs Clippy, the test build and the tests for those and
|
||||
the packages that depend on them (`scripts/ci/affected.py`, self-tested in the `checks` job). A
|
||||
change to `Cargo.lock`, the root `Cargo.toml`, `.cargo/`, `.config/`, `.github/`, `vendor/`,
|
||||
`tests/`, `scripts/` or any file outside a package runs the whole workspace, as does every push
|
||||
to `main`, the nightly run and Run workflow.
|
||||
|
||||
**CI shards the tests.** One job builds them into a cargo-nextest archive; two shards run on
|
||||
pull requests and four during full validation. Every test runs in its own process: don't rely
|
||||
on state left by another test.
|
||||
Pull requests skip a few heavy tests (one to twelve minutes each), which run in full validation
|
||||
(the merge queue, nightly, manual runs and pushes to `main` not already tested by the queue).
|
||||
Check that validation after merging. They are listed in
|
||||
**CI shards the tests.** One job builds them into a cargo-nextest archive and four jobs run a
|
||||
quarter each, so every test runs in its own process: don't rely on state left by another test.
|
||||
Pull requests skip a few heavy tests (one to four minutes each), which run on every push to
|
||||
`main` and nightly; check the `main` run 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` and
|
||||
the seeds of `tests::robustness_goldens` (`seed_00` … `seed_95`; pull requests matching the workflow's geometry paths run all but the slowest, `SLOW_SEEDS` in `ci.yml`),
|
||||
the seeds of `tests::robustness_goldens` (`seed_00` … `seed_95`; pull requests that touch `crates/kernel` run every 8th),
|
||||
`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
|
||||
- `solidcraft-kernel`: `tests::boolean_identity_on_tangent_primitives`, `tspline::tests::hostile_tsplines_never_panic`, and the `boolean_props` property tests
|
||||
- `solidcraft-kernel`: `tests::boolean_identity_on_tangent_primitives`, and the `boolean_props` property tests
|
||||
(these run only in the nightly "Property tests" job: their inputs are random, so main skips them too)
|
||||
- `solidcraft-ui-egui` scenarios: `acceptance_sensor_enclosure`
|
||||
|
||||
@@ -210,16 +193,15 @@ corpus of designs against a checked-in ratchet ([docs/action-matrix.md](docs/act
|
||||
A pull request that makes an action pass should record it with `cargo xtask action-matrix --bless`;
|
||||
one that makes a passing action fail fails the sample every CI run checks.
|
||||
|
||||
**The reference tests** live in [`tests/oracle/`](tests/oracle/), each a `recipe.json`
|
||||
**The reference tests** are 76 parts in [`tests/oracle/`](tests/oracle/), each a `recipe.json`
|
||||
in our own format with an `expected.json` beside it. SolidCraft rebuilds the recipe and compares
|
||||
body count, volume, area, centre of mass and bounding box (and, where there is a `reference.stl`,
|
||||
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; 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.
|
||||
program (`tools/freecad-oracle/`); each file says which. [docs/oracle.md](docs/oracle.md) has
|
||||
the current results, and CI runs `cargo xtask oracle` on every pull request. 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.
|
||||
|
||||
## Changing the design file format
|
||||
|
||||
@@ -275,28 +257,19 @@ and a fresh CI run:
|
||||
open a **draft** PR titled `<summary> (#N)`, e.g. `Fillet: variable radius along a chain (#123)`.
|
||||
- **The lowest-numbered open PR for an issue wins.** If yours lost, close it (or offer to help
|
||||
on the other one).
|
||||
- The bot records the claim when the PR is opened: it assigns you, adds `in progress` and
|
||||
says so on the issue. It reads `#N` in the PR title and `Fixes #N` in the body. If someone
|
||||
already holds the issue, or an older open PR references it, it says so on your PR instead.
|
||||
- Want to stake a claim before you have a branch? Comment `/claim` on the issue (below).
|
||||
- Push work in progress at least daily. A draft with no pushes for 72 hours may be taken over:
|
||||
comment on it first, then open your own claim.
|
||||
- Mark the PR ready for review when it is done.
|
||||
2. Branch from `main` and keep the PR to one topic.
|
||||
3. Run `cargo xtask ci` before pushing. It runs fmt, the asset, licence and layer checks,
|
||||
clippy (`-D warnings`), then the tests with the wasm build check alongside. A PR has to pass it.
|
||||
Heavy CI runs after review: a pull request gets the formatting and repository checks and a
|
||||
quick compile (`cargo check`) when you push. Once a maintainer has reviewed it they add the
|
||||
`approved-for-ci` label, which runs the full CI (build, tests, goldens, Clippy); every push
|
||||
after that runs the full set again.
|
||||
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).
|
||||
|
||||
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
|
||||
without pushes. These automatic cleanup deadlines are separate from the daily progress and
|
||||
72-hour takeover policy above. A draft PR that references the issue claims it the same way.
|
||||
`/unclaim` releases the bot claim early (only you or a maintainer can). Issues a maintainer has
|
||||
Comment `/claim` on an issue to take it: you are assigned, it gets the `in progress` label, and
|
||||
the bot replies with the deadline. Open a draft PR referencing the issue within 72 hours, or the
|
||||
claim lapses and the issue is freed again; a claim with a PR is freed after 14 days without
|
||||
pushes. `/unclaim` releases it early (only you or a maintainer can). Issues a maintainer has
|
||||
taken are never freed automatically.
|
||||
|
||||
Help reviewers see what the words mean. For observable CAD, UI or model behavior, please
|
||||
@@ -325,27 +298,6 @@ owning one area, with a human setting direction and reporting bugs. Their workin
|
||||
[AGENTS.md](AGENTS.md). Human contributors are just as welcome. Use whatever tools you like, as
|
||||
long as the change meets the rules above.
|
||||
|
||||
## Labels and triage
|
||||
|
||||
The triage bot labels every new issue and PR (`.github/workflows/triage.yml`). It only adds labels;
|
||||
correct them freely and it won't put them back.
|
||||
|
||||
- **PRs** get `area:*` from the paths they change (`crates/kernel` and `crates/geom` →
|
||||
`area:kernel`, `crates/ui-egui` and the apps → `area:ui`, `.github/`, `scripts/`, `docs/` →
|
||||
`area:infra`, and so on).
|
||||
- **Issues** get `bug` (in-app reports and the bug template), `enhancement` (feature requests),
|
||||
`priority:P1` for crashes, and an `area:*` from the title or the reported command. Until it has
|
||||
a priority, an issue carries `needs-triage`; setting a priority removes it.
|
||||
- **Maintainers** comment `/triage area:kernel P1` (any existing label; `P0`–`P3` for a
|
||||
priority, which replaces the old one; `-label` removes one) to sort an issue or PR.
|
||||
- **Short bug reports without steps to reproduce** get `needs-info` and one comment asking for
|
||||
the steps (and the version, when it's missing). An app report that arrived empty is asked to
|
||||
paste the report the app copied to the clipboard. The reporter's reply, or an edit that adds the
|
||||
steps, removes the label; an issue that stays in `needs-info` for 14 days is closed, and can be
|
||||
reopened any time. Feature requests, app reports with their details, and maintainers' issues
|
||||
are never asked.
|
||||
- `needs-triage` is the queue: [open issues with it](https://github.com/tangency-labs/solidcraft/issues?q=is%3Aopen+label%3Aneeds-triage).
|
||||
|
||||
## Where help is wanted most
|
||||
|
||||
**Kernel speed (slow fillets, booleans, STEP):** please don't rewrite the current kernel code for speed; it is being replaced by [solidcore](https://github.com/bherbruck/solidcore) ([progress](https://tangency-labs.github.io/solidcore/)). Report slow cases with a file under the `kernel-perf` label, and put kernel effort into solidcore instead. See [#1597](https://github.com/tangency-labs/solidcraft/issues/1597).
|
||||
@@ -360,23 +312,14 @@ In order of how much they'd help users:
|
||||
faces, and fillets that consume neighbouring faces.
|
||||
2. **Boolean robustness.** A random-design fuzzer currently gets about half of its designs
|
||||
through without a kernel error. Each failing seed is a reproducible bug.
|
||||
3. **Drawings, CAM and mesh tools.** These are out of scope for now (see
|
||||
3. **Y-up / Z-up.** SolidCraft is Z-up only, so most STEP files (from SolidWorks, Onshape, or
|
||||
Fusion's default) open lying on their backs. Fusion solves this with a "default modeling
|
||||
orientation" preference.
|
||||
4. **Imported appearances.** Colours from STEP files are lost when a feature changes the
|
||||
imported body. They should become design appearances on import.
|
||||
5. **Drawings, CAM and mesh tools.** These are out of scope for now (see
|
||||
[ROADMAP.md](ROADMAP.md)), but anyone who wants to own one is welcome.
|
||||
|
||||
## Updating the generated feature list
|
||||
|
||||
After adding or renaming commands, run `cargo xtask features`. The registry is the
|
||||
source of truth for `docs/features.md`; do not hand-edit the generated rows.
|
||||
The generator sorts commands by ID and omits global and per-section counters,
|
||||
so additions to unrelated areas no longer rewrite shared summary lines.
|
||||
|
||||
If two branches still conflict in this file (for example, adjacent additions in
|
||||
the same area), first resolve the command registry and source conflicts, then run
|
||||
`cargo xtask features` again and stage the regenerated file. Do not use Git's
|
||||
`union` merge driver: it can silently retain stale or duplicate command rows.
|
||||
The generator tests cover stable ordering and additions that leave existing lines
|
||||
unchanged. The initial switch to sorted rows is a one-time larger diff.
|
||||
|
||||
## Licence
|
||||
|
||||
By contributing you agree that your contribution is licensed as MIT OR Apache-2.0, like the rest
|
||||
|
||||
Generated
+16
-51
@@ -865,12 +865,6 @@ dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "color_quant"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.8"
|
||||
@@ -1671,16 +1665,6 @@ dependencies = [
|
||||
"rand_core 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gif"
|
||||
version = "0.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159"
|
||||
dependencies = [
|
||||
"color_quant",
|
||||
"weezl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gl_generator"
|
||||
version = "0.14.0"
|
||||
@@ -2093,9 +2077,6 @@ checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder-lite",
|
||||
"color_quant",
|
||||
"gif",
|
||||
"image-webp",
|
||||
"moxcms",
|
||||
"num-traits",
|
||||
"png",
|
||||
@@ -2104,16 +2085,6 @@ dependencies = [
|
||||
"zune-jpeg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image-webp"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3"
|
||||
dependencies = [
|
||||
"byteorder-lite",
|
||||
"quick-error",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.2"
|
||||
@@ -4078,7 +4049,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft"
|
||||
version = "0.16.2"
|
||||
version = "0.15.0"
|
||||
dependencies = [
|
||||
"block2 0.6.2",
|
||||
"eframe",
|
||||
@@ -4098,7 +4069,6 @@ dependencies = [
|
||||
"ron",
|
||||
"rustls",
|
||||
"serde_json",
|
||||
"solidcraft-3dx-client",
|
||||
"solidcraft-engine",
|
||||
"solidcraft-space-mouse",
|
||||
"solidcraft-ui-egui",
|
||||
@@ -4107,13 +4077,9 @@ dependencies = [
|
||||
"winit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-3dx-client"
|
||||
version = "0.16.2"
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-cli"
|
||||
version = "0.16.2"
|
||||
version = "0.15.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"serde_json",
|
||||
@@ -4124,7 +4090,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-derive"
|
||||
version = "0.16.2"
|
||||
version = "0.15.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -4133,7 +4099,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-doc"
|
||||
version = "0.16.2"
|
||||
version = "0.15.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
@@ -4150,7 +4116,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-engine"
|
||||
version = "0.16.2"
|
||||
version = "0.15.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
@@ -4167,7 +4133,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-geom"
|
||||
version = "0.16.2"
|
||||
version = "0.15.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -4176,7 +4142,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-gpu"
|
||||
version = "0.16.2"
|
||||
version = "0.15.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"naga",
|
||||
@@ -4193,7 +4159,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-io"
|
||||
version = "0.16.2"
|
||||
version = "0.15.0"
|
||||
dependencies = [
|
||||
"cadmpeg-core",
|
||||
"cadmpeg-ir",
|
||||
@@ -4213,7 +4179,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-kernel"
|
||||
version = "0.16.2"
|
||||
version = "0.15.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"proptest",
|
||||
@@ -4233,7 +4199,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-mcp"
|
||||
version = "0.16.2"
|
||||
version = "0.15.0"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"solidcraft-engine",
|
||||
@@ -4242,7 +4208,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-render"
|
||||
version = "0.16.2"
|
||||
version = "0.15.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"moxcms",
|
||||
@@ -4256,7 +4222,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-sketch"
|
||||
version = "0.16.2"
|
||||
version = "0.15.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -4267,13 +4233,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-space-mouse"
|
||||
version = "0.16.2"
|
||||
version = "0.15.0"
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-ui-egui"
|
||||
version = "0.16.2"
|
||||
version = "0.15.0"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"egui",
|
||||
"egui-wgpu",
|
||||
"image",
|
||||
@@ -4296,7 +4261,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-web"
|
||||
version = "0.16.2"
|
||||
version = "0.15.0"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"egui",
|
||||
@@ -5826,7 +5791,7 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.16.2"
|
||||
version = "0.15.0"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"sha2 0.10.9",
|
||||
|
||||
+1
-11
@@ -5,7 +5,7 @@ members = ["crates/*", "apps/*", "xtask"]
|
||||
exclude = ["vendor"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.16.2"
|
||||
version = "0.15.0"
|
||||
edition = "2024"
|
||||
license = "MIT OR Apache-2.0"
|
||||
rust-version = "1.95"
|
||||
@@ -26,7 +26,6 @@ solidcraft-ui-egui = { path = "crates/ui-egui" }
|
||||
solidcraft-space-mouse = { path = "crates/space-mouse" }
|
||||
solidcraft-mcp = { path = "crates/mcp" }
|
||||
solidcraft-derive = { path = "crates/derive" }
|
||||
solidcraft-3dx-client = { path = "crates/threedx-client" }
|
||||
|
||||
# foundation
|
||||
thiserror = "2"
|
||||
@@ -123,9 +122,6 @@ incremental = false
|
||||
|
||||
[profile.ci.package."*"]
|
||||
opt-level = 2
|
||||
# No line tables for dependencies: they were over a third of the test archive every CI shard
|
||||
# downloads. Our own crates keep theirs, so a backtrace still names file:line in them.
|
||||
debug = false
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
@@ -145,9 +141,3 @@ matext4cgmath = { path = "vendor/matext4cgmath" }
|
||||
|
||||
# Keep OpenGL pipelines, resolve textures and CAD callbacks at the same safe sample count.
|
||||
egui-wgpu = { path = "vendor/egui-wgpu" }
|
||||
|
||||
# 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]
|
||||
inherits = "ci"
|
||||
incremental = true
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<a href="https://solidcraft.io/download">Download</a> ·
|
||||
<a href="https://solidcraft.io/app">Try it in the browser</a> ·
|
||||
<a href="https://docs.solidcraft.io/">Documentation</a> ·
|
||||
<a href="https://discord.gg/FtU4NQNVDH">Discord</a> ·
|
||||
<a href="https://solidcraft.io/discord">Discord</a> ·
|
||||
<a href="https://github.com/bherbruck/solidcore">SolidCore</a>
|
||||
</p>
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
<a href="https://github.com/tangency-labs/solidcraft/releases/latest"><img src="https://img.shields.io/github/v/release/tangency-labs/solidcraft?label=release" alt="Latest release" /></a>
|
||||
<a href="https://github.com/tangency-labs/solidcraft/releases"><img src="https://img.shields.io/github/downloads/tangency-labs/solidcraft/total?label=downloads" alt="Downloads" /></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/tangency-labs/solidcraft" alt="Licence" /></a>
|
||||
<a href="https://discord.gg/FtU4NQNVDH"><img src="https://img.shields.io/badge/Discord-join-5865F2?logo=discord&logoColor=white" alt="Discord" /></a>
|
||||
<a href="https://solidcraft.io/discord"><img src="https://img.shields.io/badge/Discord-join-5865F2?logo=discord&logoColor=white" alt="Discord" /></a>
|
||||
<a href="https://remade.wiki/projects/solidcraft/"><img src="https://remade.wiki/badges/listed-on-remade-classic.svg" alt="Listed on Remade" /></a>
|
||||
<a href="https://github.com/tangency-labs/solidcraft/stargazers"><img src="https://img.shields.io/github/stars/tangency-labs/solidcraft?style=flat" alt="Stars" /></a>
|
||||
</p>
|
||||
@@ -63,10 +63,6 @@
|
||||
The downloads aren't signed yet. On Windows, if SmartScreen warns you, choose **More info**, then
|
||||
**Run anyway**. On macOS, the first time, right-click SolidCraft in Applications and choose **Open**.
|
||||
|
||||
**On SolidCraft 0.11 to 0.14?** Its **Update now** can't install 0.15.0 or later, because the
|
||||
releases moved to a new address. Download the new version once from the links above; after that,
|
||||
updating from inside SolidCraft works again ([details](https://docs.solidcraft.io/guide/updates.html#updating-from-011-to-014)).
|
||||
|
||||
Older versions, checksums and other formats are on the [releases page](https://github.com/tangency-labs/solidcraft/releases).
|
||||
Install details, upgrading from earlier builds and building from source are in
|
||||
[Getting started](https://docs.solidcraft.io/guide/getting-started.html).
|
||||
@@ -94,7 +90,7 @@ Install details, upgrading from earlier builds and building from source are in
|
||||
**Documentation:** the [SolidCraft book](https://docs.solidcraft.io/) (user guide,
|
||||
developer guide and command reference; sources in [docs/book](docs/book)).
|
||||
|
||||
**Community:** chat with users and contributors on [Discord](https://discord.gg/FtU4NQNVDH).
|
||||
**Community:** chat with users and contributors on [Discord](https://solidcraft.io/discord).
|
||||
|
||||
**Status: early but usable for simple parts.** 75 of 76 public reference parts
|
||||
match their analytic or FreeCAD-computed volume, area, centre of mass and bounding box
|
||||
@@ -219,7 +215,7 @@ unaccepted artwork Open; run `cargo xtask icons` to check the collection.
|
||||
|
||||
Windows builds: `cargo xwin build --release --target x86_64-pc-windows-msvc -p solidcraft`.
|
||||
|
||||
On Windows, macOS and Linux, SolidCraft detects a connected 3D mouse through its HID multi-axis
|
||||
On Windows and Linux, SolidCraft detects a connected 3D mouse through its HID multi-axis
|
||||
controller interface: a 3Dconnexion SpaceNavigator or SpaceMouse, or a homemade device that
|
||||
declares itself one. Move the cap to pan and zoom, and tilt or twist it to rotate the 3D view.
|
||||
While editing a sketch, the cap pans and zooms without changing the sketch view. The Navigation
|
||||
@@ -257,13 +253,6 @@ lays them out: translation and rotation in separate reports (3Dconnexion's wired
|
||||
in one (its wireless ones and most homemade devices), after the buttons, or narrower than 16 bits.
|
||||
Each axis is scaled from the range the descriptor declares.
|
||||
|
||||
On macOS SolidCraft talks to the 3Dconnexion driver (3DxWare) when it is installed, through the
|
||||
driver's client API (`crates/threedx-client`; the framework is loaded at run time, never bundled).
|
||||
The driver keeps the device to itself, and sends it only to a program it can identify: SolidCraft
|
||||
as an `.app` (the packaged app). Without the driver, or with its helper not running, SolidCraft reads the device through IOKit like on Windows.
|
||||
|
||||
SolidCraft does not follow 3DxWare's profile on macOS. Its default is 3DxWare's object mode.
|
||||
|
||||
In the browser: `cd apps/solidcraft-web && trunk build --release --public-url ./` (WebGPU, or
|
||||
WebGL2 with `?webgl`; `?sample` opens the sample part).
|
||||
|
||||
@@ -289,8 +278,8 @@ A script is a JSON list of commands, the same ones the UI runs:
|
||||
stdio. Headless it runs its own design session; with `--connect 127.0.0.1:PORT` it drives the app
|
||||
started with `solidcraft --control PORT`, so you can watch the agent model. Tools:
|
||||
`list_commands`, `execute`, `batch`, `inspect_design`, `measure`, `body_topology`,
|
||||
`set_parameter`, `screenshot` (returns the image), `export`, `crash_reports`, `undo`, `redo`,
|
||||
`new_design`, `open`, `save`. Tool calls reach files only in the open design's folder (when it lies inside your home
|
||||
`set_parameter`, `screenshot` (returns the image), `export`, `undo`, `redo`, `new_design`, `open`,
|
||||
`save`. Tool calls reach files only in the open design's folder (when it lies inside your home
|
||||
folder, never home itself or a hidden folder), SolidCraft's recovery folder and folders given with
|
||||
`--allow-path DIR`. Claude Code: `claude mcp add solidcraft -- solidcraft-cli mcp`. Details, client
|
||||
configuration and an example session: [docs/mcp.md](docs/mcp.md).
|
||||
@@ -311,7 +300,7 @@ configuration and an example session: [docs/mcp.md](docs/mcp.md).
|
||||
|
||||
## Contributing
|
||||
|
||||
SolidCraft uses a familiar parametric CAD workflow: constrained
|
||||
Coming from Autodesk Fusion? SolidCraft uses familiar parametric CAD workflows: constrained
|
||||
sketches, solid features, an editable timeline, parameters and assemblies. See the
|
||||
[roadmap](ROADMAP.md) for supported operations and current limitations.
|
||||
|
||||
|
||||
+4
-34
@@ -9,15 +9,11 @@ features → parametric timeline) as an open, pure-Rust, agent-drivable applicat
|
||||
|---|---|
|
||||
| Where are we? | **M0 and M13 done; M1, M2, M5–M7, M9, M10, M14 in progress.** Sketch with solver and the full SKETCH create/modify set; extrude, revolve (two sides, symmetric, to object), fillet/chamfer (variable radius, chord length, a radius per edge, corner patches), holes, threads (cosmetic and modelled), patterns, mirror, shell (inside/outside/both), draft, loft, sweep, coil, split body and split face, delete face, construction planes, axes and points; persistent naming of faces and edges; parameters and configurations; components, joints, motion studies, exploded views; sheet metal and a plastic enclosure subset; STEP/IGES/3MF/STL/OBJ/DXF; FreeCAD/Rhino import; autosave and crash recovery; desktop and browser (wasm) app, CLI, MCP server. |
|
||||
| Features | Every registered command by area — [docs/features.md](docs/features.md) (`cargo xtask features`); registry breadth does not establish workflow or geometric depth |
|
||||
| Reference tests | [Current case count and results](docs/oracle.md): public cases in `tests/oracle/` with analytic or FreeCAD-computed expected values, checked by the reference-test job in [CI](.github/workflows/ci.yml). |
|
||||
| Reference tests | **82 / 82 parts match** — [docs/oracle.md](docs/oracle.md): public cases in `tests/oracle/` with analytic (61) or FreeCAD-computed (21) expected values, run by CI on every PR. |
|
||||
| Tests | Solver, profiles, kernel booleans/blends/measures, expressions, timeline, persistent naming, file formats and migrations, assemblies, sheet metal, engine end-to-end, hostile-input fuzz over every command and headless UI scenarios. `cargo xtask ci` reports the current passing counts. |
|
||||
| Gates | `cargo xtask ci`: fmt, clippy -D warnings, tests, asset attribution, layering, wasm32 build — green |
|
||||
| Time to a useful alpha (M0–M6) | ≈ **82 agent hours** remain (sum of the M1–M6 estimates below) |
|
||||
|
||||
Optional GPU viewport ambient occlusion is available in View settings and Graphics preferences,
|
||||
with persistent settings, Performance/Quality graphics presets (Quality combines AO, Fine tessellation and adaptive 2× supersampling; manual combinations show Custom), and the
|
||||
`view.ambient_occlusion` and `view.graphics_preset` commands. See [display settings](docs/ambient-occlusion.md).
|
||||
|
||||
## Scope (owner decision, 2026-10-07)
|
||||
|
||||
The issue batch adds preservation of blind pockets in through cuts, associative projected
|
||||
@@ -55,11 +51,11 @@ PCB / Package, Render, Animation, Simulation, Manufacture/CAM, Drawings.
|
||||
| M3 | General fillets and chamfers (curved edges, chains, vertex blends); saddle rolling-ball surfaces now use continuous seam sections and arc-length fitting with shared contact boundaries and bounded accuracy checks | in progress | 30 |
|
||||
| M4 | Files: STEP import (done — [docs/step-import.md](docs/step-import.md)), FreeCAD/Rhino solids through cadmpeg (done, desktop and CLI), 3MF export and 3MF/STL import as mesh bodies (done), 3MF with B-rep face tags ([docs/formats/3mf-face-tags.md](docs/formats/3mf-face-tags.md), done), constant-section parametric mesh reconstruction, DXF sketches, OBJ import | in progress | 6 |
|
||||
| M5 | Persistent naming of faces and edges (docs/book/src/dev/naming.md): faces named from their exact kernel-reported history (sketch curve sides, caps, revolve sections, primitive sides, blends, shell inner walls, pattern/mirror copies; split pieces told apart by the faces around them, lumps of a cut body named after their faces), edges by their faces; fillets/chamfers, sketches on faces, shell/draft/offset/replace faces, hole placements, joint origins and face appearances resolve by name first and warn when the entity split or vanished: done. sheet metal edges by base sketch line and flange side, shell inner faces, sweep and loft roles: done. kernel-reported face history: done (merged faces answer to every name; operations without history yet fall back to geometric names, flagged unverified; format 14 keeps older designs' geometric names). Left: history through sheet metal, plastic, surface and emboss features; FaceSource certificates through history | in progress | 20 |
|
||||
| M6 | UI depth: selection feedback and origin (done), timeline editing (done: hover and selection highlight modifier faces, including fillets and chamfers; edit feature, history marker drag, dependency-checked single and multi-item reorder, suppress, rename, delete with dependents, re-resolved references; redefining selections refreshes persistent edge/face references while retaining feature identity and dimension names; optional compact named left feature history with rollback bar, multi-selection and persistent folders above the parts browser), right-docked dialogs (done), live previews for every feature dialog (done), keyboard-first commands with on-canvas value boxes and sketch inline dimensions (done), drag manipulators with automatic join/cut (done: extrude incl. two sides, fillet, shell, hole depth, move, pattern spacing, revolve rotator, extrude taper), dark theme (done), right-click menu with Repeat (done), measure tool (done), section analysis (done), selection filter (done), drag-solve of sketch points (done); appearance controls now preserve body/face/component targeting, named transparent looks, reuse/search and override removal; preferences | in progress | 6 |
|
||||
| M6 | UI depth: selection feedback and origin (done), timeline editing (done: hover and selection highlight modifier faces, including fillets and chamfers; edit feature, history marker drag, dependency-checked single and multi-item reorder, suppress, rename, delete with dependents, re-resolved references; redefining selections refreshes persistent edge/face references while retaining feature identity and dimension names; optional compact named left feature history with rollback bar, multi-selection and persistent folders above the parts browser), right-docked dialogs (done), live previews for every feature dialog (done), keyboard-first commands with on-canvas value boxes and sketch inline dimensions (done), drag manipulators with automatic join/cut (done: extrude incl. two sides, fillet, shell, hole depth, move, pattern spacing, revolve rotator; taper handle open), dark theme (done), right-click menu with Repeat (done), measure tool (done), section analysis (done), selection filter (done), drag-solve of sketch points (done); appearance controls now preserve body/face/component targeting, named transparent looks, reuse/search and override removal; preferences | in progress | 6 |
|
||||
| M7 | Thin extrude (walls along open or closed curves: centre, inside, outside or two sides; mitred corners, crossing walls joined; #721), sweep, loft (incl. to a point), rib and web (to next / depth), emboss (planar faces and exact radial emboss/deboss of bounded orthogonal profiles on complete exterior cylinders/sleeves), thread (cosmetic, and modelled M3–M20 outside and inside: the ISO profile screwed in as a faceted cut), coil (smooth B-spline helical walls), pipe, pattern on path, patterns of bodies and components, replace face (parallel), align, remove, feature copy/paste: done. Unique eligible cylindrical targets are inferred for new Emboss features. Left: general curved-face emboss and exact (unfaceted) modelled threads | in progress | 12 |
|
||||
| M8 | Surface workspace (#772). Done: Patch (sketch regions; edge loops filled G0, plus `crown`), Stitch, Thicken (planes and cylinders), Extend (straight edges of planar surfaces), Trim (by a plane or a face), Delete Face (healing), Boundary Fill, zebra/draft/curvature-map analysis, Surface Extrude/Revolve/Sweep/Loft from open and closed curves, Offset, Unstitch and Reverse Normal; the continuity-driven core (`kernel/src/bridge.rs`, G0–G3 Hermite rows) with **Blend** (`surface.blend`, #764), **Match** (`surface.match`), **Boundary** (`surface.boundary`, #763 — two sketch curves exact with G0–G3, three or four as the flat patch of their loop, and curve networks: Coons and Gordon surfaces through sketch, 3D-sketch or edge curves, a point for a three-sided side, G1/G2 against the faces along one direction's edges, joined into their body on request; G1/G2 on two sides that meet at a corner waits for the Gregory-type corner), **Fair** (`surface.fair`, CV fairing), **continuity analysis** (`inspect.continuity`, #770; a toolbar tool with edge markers and a legend, and curvature combs along face isocurves) and a multi-section **loft** with per-end continuity and crown. Also in the Class-A pass: G1…G4 blend curves in the sketch, chord and variable-radius fillets on curved edges (with honest refusals where the fit cannot hold), and a G2 corner patch where filleted edges meet (kernel; the command refuses until the patch's sections can be sewn to the blends' contacts). Left: bias/form-factor fillet sections (design recorded), a Gregory-type corner, surface Fillet between separate surface bodies (#769), Ruled (#762), Untrim (#761), curved-face Extend (#760), loft guide rails (#201; sweep guide rails and two-rail sweeps are done, #771), and every remaining #757–#772 item | deferred | — |
|
||||
| M8 | Surface workspace (#772). Done: Patch (sketch regions; edge loops filled G0, plus `crown`), Stitch, Thicken (planes and cylinders), Extend (straight edges of planar surfaces), Trim (by a plane or a face), Delete Face (healing), Boundary Fill, zebra/draft/curvature-map analysis, Surface Extrude/Revolve/Sweep/Loft from open and closed curves, Offset, Unstitch and Reverse Normal; the continuity-driven core (`kernel/src/bridge.rs`, G0–G3 Hermite rows) with **Blend** (`surface.blend`, #764), **Match** (`surface.match`), **Boundary** (`surface.boundary`, #763 — two boundaries exact, three/four refused pending a Gregory-type corner), **Fair** (`surface.fair`, CV fairing), **continuity analysis** (`inspect.continuity`, #770; a toolbar tool with edge markers and a legend, and curvature combs along face isocurves) and a multi-section **loft** with per-end continuity and crown. Also in the Class-A pass: G1…G4 blend curves in the sketch, chord and variable-radius fillets on curved edges (with honest refusals where the fit cannot hold), and a G2 corner patch where filleted edges meet (kernel; the command refuses until the patch's sections can be sewn to the blends' contacts). Left: bias/form-factor fillet sections (design recorded), a Gregory-type corner, surface Fillet between separate surface bodies (#769), Ruled (#762), Untrim (#761), curved-face Extend (#760), loft guide rails (#201; sweep guide rails and two-rail sweeps are done, #771), and every remaining #757–#772 item | deferred | — |
|
||||
| M9 | Components and assemblies: New Component / Create Components from Bodies, active component, per-component origin/bodies/sketches/timeline entries, occurrences with transforms, Copy/Paste vs Paste New, nested Browser tree with visibility and isolate; Ground, Move/Copy of occurrences, joints (rigid, revolute, slider, cylindrical, pin-slot, planar, ball) by joint origins with limits and Drive Joints, closed kinematic loops (four-bar, slider-crank: the loop's other joints follow the driven one on its branch, and driving stops where a loop can no longer close), cams (`joint.cam`: a revolute cam drives a slider or revolute follower along a closed sketch profile or the cam's edges, knife, roller or flat followers, worked out again when the profile changes; drives, studies, closed loops, contact sets), As-built Joint, Rigid Group, Contact Sets (driving and motion studies stop where bodies in a set would pass through each other), Motion Study (joint values keyed along a timeline, playback, export of occurrence positions as JSON/CSV), motion analysis (joint values and a point's path charted over a study as value, velocity and acceleration; the parts that collide along a study, with the steps where they do), Exploded views (automatic or by hand, shown without changing placements); interference, per-component physical properties; STEP with assembly structure; URDF export for robot simulators (links with meshes, mass and inertia; joint chains whose values match Drive Joints; docs/urdf.md). Physical properties now measure material-aware component/occurrence placements, descendants and repeated instances, including pending moves and body reassignment offsets; contact geometry and automatic exploded centres respect those offsets. Groundwork done: component tree, occurrences with transforms (instances, Paste New), world placement, active component as authoring frame, ground, Capture/Revert Position, moving bodies and sketches. UI: ASSEMBLE panel dialogs for New Component, Joint (snaps on faces, edges, circles and vertices; animated motion preview; offsets, flip, limits), As-built Joint, Joint Origin, Rigid Group, Drive Joints (live slider), Motion Link, Cam and Interference (overlaps shown red); joints in the timeline and the Browser | in progress | 32 |
|
||||
| M10 | Sheet metal: flange (base, edge, contour), hem, unfold/refold, convert, flat pattern + DXF, rules: done. PLASTIC enclosure subset: Boss, Rib, Web, Lip/Groove, Snap Fit, Rest, Plastic Rules (library, edit, assign; rule draft and clearance as feature defaults): done, tested against hand-computed geometry. UI: SHEET METAL and PLASTIC tabs with dialogs (live preview, Edit Feature, pre-selection) for Flange, Hem, Unfold/Refold, Convert, Sheet Metal Rules, Create Flat Pattern (flat view with bend lines, bend table CSV/JSON and DXF export with exact circle/arc/line cutouts), Boss, Lip/Groove, Snap Fit, Rest, Manage and Assign Plastic Rules; Fold (bend along a sketch line); flanges meeting at a corner closed at any angle with the rule's gap; edge flanges with their own angle and height per edge in one feature; corner reliefs (notch, round, rectangular, tear) chosen by the rule; Convert to Sheet Metal from a solid or shelled body by its bend edges (flat faces, sharp bends) | in progress | 36 |
|
||||
| M10 | Sheet metal: flange (base, edge, contour), hem, unfold/refold, convert, flat pattern + DXF, rules: done. PLASTIC enclosure subset: Boss, Rib, Web, Lip/Groove, Snap Fit, Rest, Plastic Rules (library, edit, assign; rule draft and clearance as feature defaults): done, tested against hand-computed geometry. UI: SHEET METAL and PLASTIC tabs in Fusion's layout with dialogs (live preview, Edit Feature, pre-selection) for Flange, Hem, Unfold/Refold, Convert, Sheet Metal Rules, Create Flat Pattern (flat view with bend lines, bend table CSV/JSON and DXF export with exact circle/arc/line cutouts), Boss, Lip/Groove, Snap Fit, Rest, Manage and Assign Plastic Rules; Fold (bend along a sketch line); flanges meeting at a corner closed at any angle with the rule's gap; edge flanges with their own angle and height per edge in one feature; corner reliefs (notch, round, rectangular, tear) chosen by the rule; Convert to Sheet Metal from a solid or shelled body by its bend edges (flat faces, sharp bends) | in progress | 36 |
|
||||
| M11 | 2D drawings | deferred | — |
|
||||
| M12 | Release: cross-platform packages, verification and deliberate publication implemented; original current-UI demo refreshed. Trusted distribution signing and broader performance work remain | in progress | 16 |
|
||||
| M14 | Configurations: a table of rows (parameter expressions and feature suppressions per row) switched with config.activate (undoable, all or nothing), kept through parameter renames and feature deletes (config.configure, config.table, config.column/row/activate). Per-row body materials and appearances (library looks, custom colour/opacity and clearing overrides), undoable activation and save/reopen: done. Ordered typed configuration rules, atomic validation and structured rule editing: done. Component appearance and certified semantic face style columns, atomic preview/apply and independent-copy remapping: done. Exact constructed sketch-side and immutable imported-face lineage now preserve legacy names and reject retargeting. Left: ambiguous shifted/two-sided caps | in progress | 4 |
|
||||
@@ -77,11 +73,6 @@ hierarchies are shared; bounded desktop preparation and occlusion use up to eigh
|
||||
Curves are approximated; drawing sheets,
|
||||
annotations and exact projected curves remain deferred. See [view DXF](docs/view-dxf.md).
|
||||
|
||||
Drawing foundations: [RFC #860](https://github.com/tangency-labs/solidcraft/issues/860)
|
||||
and the [foundation ledger](docs/drawing-foundations.md) track the bounded implementation
|
||||
packages. The first geometry increment defines validated section records in a plane-local
|
||||
frame; the ledger separates that contract from section producers and the Drawing workspace.
|
||||
|
||||
Desktop view DXF export runs in the file-operation worker with five-stage progress and
|
||||
cooperative cancellation. Curve tolerance (0.0001–1 mm, default 0.01 mm) is remembered alongside
|
||||
the hidden-edge setting. Cancelled exports preserve the model and the previous destination;
|
||||
@@ -283,25 +274,6 @@ completed drawings publish atomically. The browser still exports synchronously.
|
||||
|
||||
## 0.2.1 critical fixes
|
||||
|
||||
- Separated multi-profile Join/Cut extrusions into one solid target batch their tools using
|
||||
conservative construction bounds, avoiding a Boolean rebuild for every profile. Live
|
||||
extrusion previews build only their tools; confirmation validates the final Boolean.
|
||||
- Coplanar selected body faces share one linked sketch and one extrusion. Automatic Join/Cut
|
||||
keeps the evaluator's participant scope, including parts of an empty imported component. Desktop OK/Enter
|
||||
computes on a scratch session in the background, with atomic acceptance and cancellation.
|
||||
Planar section stitching projects lines/arcs analytically and removes duplicate endpoints.
|
||||
- Imported rotated face groups can check all full start-face seams in one guarded
|
||||
construction. Separated tools may batch a Join when each meets the same single body;
|
||||
nearby uninvolved bodies retain the original contact decisions. Regression comparisons
|
||||
cover all four extrusion operations.
|
||||
- Inward multi-face cuts retain every eligible target and subtract separated tools together.
|
||||
Safe interior classification handles roundoff at shared imported line/arc endpoints.
|
||||
Straight extrusion feedback caches existing face/profile triangles and updates the display
|
||||
directly, including a newly opened STEP's single identity component placement; other
|
||||
extents, moved or repeated components and curved sketch profiles keep the Engine preview.
|
||||
- The extrusion fixes retain tracked Boolean history and the current `solidcraft` packages.
|
||||
Confirmation and tool previews use main's shared background calculation. Main's collapsed
|
||||
blend-wall refusal and its tests are retained, including smaller valid radii and mirrored parts.
|
||||
- Axis view and navigation controls stay anchored to the viewport when feature dialogs or
|
||||
the command palette open, at normal and enlarged display scales. Fit still accounts for
|
||||
dialog coverage; covered controls do not receive clicks through dialogs.
|
||||
@@ -349,8 +321,6 @@ completed drawings publish atomically. The browser still exports synchronously.
|
||||
rounds next to earlier rounds of another radius, and smooth corner patches where three rounds
|
||||
meet (a sphere for equal radii); perpendicular corner fillets after a closed ring of earlier
|
||||
rounds preserve the ring's radii and the enclosed planar face (#1557);
|
||||
mitres between separate rounds of the same radius keep their endpoint tangent, so a later
|
||||
fillet can select an upright and its adjoining bottom mitre as one chain (#2422);
|
||||
straight edges (convex or concave) between planar faces with perpendicular
|
||||
planar end faces (equal-distance chamfers also meet sloped planar end faces, including inner
|
||||
basin rims within the rim width); convex fillets also meet sloped planar ends and reflex
|
||||
|
||||
+25
-80
@@ -1,6 +1,6 @@
|
||||
Third-party software in SolidCraft
|
||||
|
||||
SolidCraft (the desktop app, solidcraft-cli and the web app) is built from the 552 crates below.
|
||||
SolidCraft (the desktop app, solidcraft-cli and the web app) is built from the 549 crates below.
|
||||
Generated by `cargo xtask licences` from Cargo.lock and the crates' own licence files.
|
||||
SolidCraft's own licence is in LICENSE-MIT and LICENSE-APACHE, and NOTICE.
|
||||
|
||||
@@ -80,7 +80,6 @@ cipher 0.4.4 (MIT OR Apache-2.0)
|
||||
clipboard-win 5.4.1 (BSL-1.0)
|
||||
codespan-reporting 0.13.1 (Apache-2.0)
|
||||
color 0.3.3 (Apache-2.0 OR MIT)
|
||||
color_quant 1.1.0 (MIT)
|
||||
combine 4.6.8 (MIT)
|
||||
concurrent-queue 2.5.0 (Apache-2.0 OR MIT)
|
||||
const-oid 0.10.2 (Apache-2.0 OR MIT)
|
||||
@@ -157,7 +156,6 @@ gethostname 1.1.0 (Apache-2.0)
|
||||
getrandom 0.2.17 (MIT OR Apache-2.0)
|
||||
getrandom 0.3.4 (MIT OR Apache-2.0)
|
||||
getrandom 0.4.3 (MIT OR Apache-2.0)
|
||||
gif 0.14.2 (MIT OR Apache-2.0)
|
||||
glifo 0.2.0 (Apache-2.0 OR MIT)
|
||||
glow 0.17.0 (MIT OR Apache-2.0 OR Zlib)
|
||||
gltf 1.4.1 (MIT OR Apache-2.0)
|
||||
@@ -194,7 +192,6 @@ icu_provider 2.3.1 (Unicode-3.0)
|
||||
idna 1.1.0 (MIT OR Apache-2.0)
|
||||
idna_adapter 1.2.2 (Apache-2.0 OR MIT)
|
||||
image 0.25.10 (MIT OR Apache-2.0)
|
||||
image-webp 0.2.4 (MIT OR Apache-2.0)
|
||||
indexmap 2.14.2 (Apache-2.0 OR MIT)
|
||||
inflections 1.1.1 (MIT)
|
||||
inout 0.1.4 (MIT OR Apache-2.0)
|
||||
@@ -1101,23 +1098,23 @@ ahash 0.8.12, arrayvec 0.7.8, async-channel 2.5.0, async-executor 1.14.0, async-
|
||||
1.0.0, crossbeam-deque 0.8.8, crossbeam-epoch 0.9.21, crossbeam-utils 0.8.23, dbus-secret-service
|
||||
4.1.0, displaydoc 0.2.7, either 1.19.0, equivalent 1.0.2, errno 0.3.14, euclid 0.22.14,
|
||||
event-listener 5.4.2, event-listener-strategy 0.5.4, fastrand 2.5.0, flate2 1.1.10, form_urlencoded
|
||||
1.2.2, futures-lite 2.6.1, gethostname 1.1.0, gif 0.14.2, glow 0.17.0, gltf 1.4.1, gltf-derive
|
||||
1.4.1, gltf-json 1.4.1, hashbrown 0.16.1, hashbrown 0.17.1, hermit-abi 0.5.3, httparse 1.10.1, idna
|
||||
1.1.0, idna_adapter 1.2.2, indexmap 2.14.2, itertools 0.13.0, itertools 0.15.0, js-sys 0.3.106,
|
||||
khronos-egl 6.0.0, lazy_static 1.5.1, linux-raw-sys 0.12.1, linux-raw-sys 0.4.15, lock_api 0.4.14,
|
||||
log 0.4.34, num 0.4.3, num-bigint 0.4.8, num-complex 0.4.6, num-integer 0.1.47, num-iter 0.1.46,
|
||||
num-rational 0.4.2, num-traits 0.2.19, once_cell 1.21.4, openssl-probe 0.2.1, ordered-stream 0.2.0,
|
||||
parking 2.2.1, parking_lot 0.12.5, parking_lot_core 0.9.12, percent-encoding 2.3.2, piper 0.2.5,
|
||||
plain 0.2.3, png 0.18.1, polling 3.11.0, pollster 0.4.0, pollster 1.0.1, rayon 1.12.0, rayon-core
|
||||
1.13.0, renderdoc-sys 1.1.0, robust 1.2.0, ron 0.12.2, roxmltree 0.21.1, rustc-hash 1.1.0, rustix
|
||||
0.38.44, rustix 1.1.5, rustls 0.23.46, rustls-native-certs 0.8.5, scoped-tls 1.0.1, scopeguard
|
||||
1.2.0, secret-service 4.0.0, security-framework 2.11.1, security-framework 3.7.0,
|
||||
security-framework-sys 2.17.0, signal-hook-registry 1.4.8, simd_cesu8 1.2.0, smallvec 1.16.2,
|
||||
smol_str 0.2.2, stable_deref_trait 1.2.1, syn 1.0.109, tempfile 3.27.0, ttf-parser 0.25.1,
|
||||
unicode-segmentation 1.13.3, unicode-width 0.2.2, url 2.5.8, uuid 1.27.0, wasi
|
||||
0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12, wasm-bindgen 0.2.129, wasm-bindgen-futures
|
||||
0.4.79, wasm-bindgen-macro 0.2.129, wasm-bindgen-macro-support 0.2.129, wasm-bindgen-shared 0.2.129,
|
||||
web-sys 0.3.106, weezl 0.1.12, wit-bindgen 0.57.1
|
||||
1.2.2, futures-lite 2.6.1, gethostname 1.1.0, glow 0.17.0, gltf 1.4.1, gltf-derive 1.4.1, gltf-json
|
||||
1.4.1, hashbrown 0.16.1, hashbrown 0.17.1, hermit-abi 0.5.3, httparse 1.10.1, idna 1.1.0,
|
||||
idna_adapter 1.2.2, indexmap 2.14.2, itertools 0.13.0, itertools 0.15.0, js-sys 0.3.106, khronos-egl
|
||||
6.0.0, lazy_static 1.5.1, linux-raw-sys 0.12.1, linux-raw-sys 0.4.15, lock_api 0.4.14, log 0.4.34,
|
||||
num 0.4.3, num-bigint 0.4.8, num-complex 0.4.6, num-integer 0.1.47, num-iter 0.1.46, num-rational
|
||||
0.4.2, num-traits 0.2.19, once_cell 1.21.4, openssl-probe 0.2.1, ordered-stream 0.2.0, parking
|
||||
2.2.1, parking_lot 0.12.5, parking_lot_core 0.9.12, percent-encoding 2.3.2, piper 0.2.5, plain
|
||||
0.2.3, png 0.18.1, polling 3.11.0, pollster 0.4.0, pollster 1.0.1, rayon 1.12.0, rayon-core 1.13.0,
|
||||
renderdoc-sys 1.1.0, robust 1.2.0, ron 0.12.2, roxmltree 0.21.1, rustc-hash 1.1.0, rustix 0.38.44,
|
||||
rustix 1.1.5, rustls 0.23.46, rustls-native-certs 0.8.5, scoped-tls 1.0.1, scopeguard 1.2.0,
|
||||
secret-service 4.0.0, security-framework 2.11.1, security-framework 3.7.0, security-framework-sys
|
||||
2.17.0, signal-hook-registry 1.4.8, simd_cesu8 1.2.0, smallvec 1.16.2, smol_str 0.2.2,
|
||||
stable_deref_trait 1.2.1, syn 1.0.109, tempfile 3.27.0, ttf-parser 0.25.1, unicode-segmentation
|
||||
1.13.3, unicode-width 0.2.2, url 2.5.8, uuid 1.27.0, wasi 0.11.1+wasi-snapshot-preview1, wasip2
|
||||
1.0.4+wasi-0.2.12, wasm-bindgen 0.2.129, wasm-bindgen-futures 0.4.79, wasm-bindgen-macro 0.2.129,
|
||||
wasm-bindgen-macro-support 0.2.129, wasm-bindgen-shared 0.2.129, web-sys 0.3.106, weezl 0.1.12,
|
||||
wit-bindgen 0.57.1
|
||||
================================================================================
|
||||
|
||||
Apache License
|
||||
@@ -1539,12 +1536,12 @@ at your option.
|
||||
|
||||
================================================================================
|
||||
android-activity 0.6.1, arboard 3.6.1, color 0.3.3, fdeflate 0.3.7, fearless_simd 0.4.1, half 2.7.1,
|
||||
image 0.25.10, image-webp 0.2.4, linebender_resource_handle 0.1.1, miniz_oxide 0.8.9, miniz_oxide
|
||||
0.9.1, naga 30.0.1, naga-types 30.0.1, pin-project 1.1.13, pin-project-internal 1.1.13,
|
||||
pin-project-lite 0.2.17, portable-atomic 1.15.0, portable-atomic-util 0.2.8, range-alloc 0.1.5,
|
||||
raw-window-handle 0.6.2, simdutf8 0.1.5, truck-geometry 0.5.0, truck-meshalgo 0.4.0, truck-shapeops
|
||||
0.4.0, truck-stepio 0.3.0, vello_common 0.1.0, vello_cpu 0.1.0, wgpu 30.0.1, wgpu-core 30.0.1,
|
||||
wgpu-core-deps-apple 30.0.1, wgpu-core-deps-emscripten 30.0.1, wgpu-core-deps-wasm 30.0.1,
|
||||
image 0.25.10, linebender_resource_handle 0.1.1, miniz_oxide 0.8.9, miniz_oxide 0.9.1, naga 30.0.1,
|
||||
naga-types 30.0.1, pin-project 1.1.13, pin-project-internal 1.1.13, pin-project-lite 0.2.17,
|
||||
portable-atomic 1.15.0, portable-atomic-util 0.2.8, range-alloc 0.1.5, raw-window-handle 0.6.2,
|
||||
simdutf8 0.1.5, truck-geometry 0.5.0, truck-meshalgo 0.4.0, truck-shapeops 0.4.0, truck-stepio
|
||||
0.3.0, vello_common 0.1.0, vello_cpu 0.1.0, wgpu 30.0.1, wgpu-core 30.0.1, wgpu-core-deps-apple
|
||||
30.0.1, wgpu-core-deps-emscripten 30.0.1, wgpu-core-deps-wasm 30.0.1,
|
||||
wgpu-core-deps-windows-linux-android 30.0.1, wgpu-hal 30.0.1, wgpu-naga-bridge 30.0.1, wgpu-types
|
||||
30.0.1
|
||||
================================================================================
|
||||
@@ -3598,32 +3595,6 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
================================================================================
|
||||
color_quant 1.1.0
|
||||
================================================================================
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 PistonDevelopers
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
================================================================================
|
||||
combine 4.6.8
|
||||
================================================================================
|
||||
@@ -5347,7 +5318,7 @@ The above copyright notice and this permission notice shall be included in all c
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
================================================================================
|
||||
fdeflate 0.3.7, image 0.25.10, image-webp 0.2.4
|
||||
fdeflate 0.3.7, image 0.25.10
|
||||
================================================================================
|
||||
|
||||
MIT License
|
||||
@@ -6181,32 +6152,6 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
================================================================================
|
||||
gif 0.14.2
|
||||
================================================================================
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 nwin
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
================================================================================
|
||||
glifo 0.2.0
|
||||
================================================================================
|
||||
|
||||
@@ -423,24 +423,6 @@ pub fn to_script(recipe: &Value) -> Result<Value, String> {
|
||||
"sketch": profile_sketch(f), "profiles": profiles_of(f), "path_sketch": path.get("sketch"), "path": path.get("curves"),
|
||||
"operation": op(f), "name": name, "body_names": bodies}}));
|
||||
}
|
||||
// A boundary surface: a loop of one sketch's curves (`sketch`, `curves`), or a network
|
||||
// (`u`, `v`), as `surface.boundary` takes them.
|
||||
Some("boundary_surface") => {
|
||||
let mut p = json!({"name": name});
|
||||
for k in ["sketch", "curves", "continuity", "u", "v", "operation"] {
|
||||
if let Some(v) = f.get(k) {
|
||||
p[k] = v.clone();
|
||||
}
|
||||
}
|
||||
if let Some(b) = bodies.as_array().and_then(|a| a.first()) {
|
||||
p["body_name"] = b.clone();
|
||||
}
|
||||
out.push(json!({"command": "surface.boundary", "params": p}));
|
||||
}
|
||||
Some("thicken") => out.push(json!({"command": "surface.thicken", "params": {
|
||||
"bodies": f.get("bodies"), "thickness": f.get("thickness"), "symmetric": f.get("symmetric").cloned().unwrap_or(json!(false)),
|
||||
"operation": op(f), "name": name}})),
|
||||
Some("remove") => out.push(json!({"command": "solid.remove", "params": {"bodies": f.get("bodies"), "name": name}})),
|
||||
Some("shell") => {
|
||||
let faces: Vec<Value> = f.get("faces_removed").and_then(Value::as_array).into_iter().flatten().filter_map(|x| x.get("point").cloned()).collect();
|
||||
out.push(json!({"command": "solid.shell", "params": {"faces": faces, "thickness": f.get("inside_thickness"), "name": name}}));
|
||||
|
||||
@@ -350,7 +350,7 @@ fn actions(s: &Session) -> Vec<Action> {
|
||||
// The surface's normal at `p` (either way round): exact on analytic faces.
|
||||
let normal_at = |f: u32, p: Vec3| -> Option<Vec3> {
|
||||
match face_surface(&b.body, f as usize) {
|
||||
Some(FaceSurface::Plane { normal, .. }) => Some(*normal),
|
||||
Some(FaceSurface::Plane { normal, .. }) => Some(normal),
|
||||
Some(FaceSurface::Cylinder { origin, axis, .. }) => {
|
||||
let v = p - origin;
|
||||
(v - axis * v.dot(axis)).normalized()
|
||||
|
||||
@@ -27,7 +27,7 @@ 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",
|
||||
"partial revolves turn toward the sketch normal since ac43365 (as Fusion's 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"),
|
||||
];
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
//! The reference tests (tests/oracle, docs/oracle.md) as a `cargo test`, so tools that drive
|
||||
//! `cargo test` (cargo-mutants) can use them: every case with expected values and no
|
||||
//! `known_failure` mark must pass, as `cargo xtask oracle` requires.
|
||||
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
#[test]
|
||||
#[ignore = "run by cargo-mutants (.cargo/mutants.toml); CI replays the oracle through cargo xtask oracle"]
|
||||
fn oracle_suite() {
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
let dir = root.join("tests/oracle");
|
||||
let mut cases: Vec<String> = std::fs::read_dir(&dir)
|
||||
.unwrap()
|
||||
.flatten()
|
||||
.map(|e| e.path())
|
||||
.filter(|p| p.join("recipe.json").is_file())
|
||||
.map(|p| format!("tests/oracle/{}", p.file_name().unwrap().to_string_lossy()))
|
||||
.collect();
|
||||
cases.sort();
|
||||
assert!(cases.len() >= 50, "oracle cases in {}", dir.display());
|
||||
// One process per core, cases dealt round-robin so the slow ones spread out.
|
||||
let n = std::thread::available_parallelism().map_or(4, |n| n.get()).min(cases.len());
|
||||
let children: Vec<_> = (0..n)
|
||||
.map(|i| {
|
||||
let chunk: Vec<&String> = cases.iter().skip(i).step_by(n).collect();
|
||||
Command::new(env!("CARGO_BIN_EXE_solidcraft-cli"))
|
||||
.current_dir(&root)
|
||||
.arg("oracle")
|
||||
.arg("--json")
|
||||
.args(chunk)
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.spawn()
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
let mut results: Vec<Value> = Vec::new();
|
||||
for child in children {
|
||||
let out = child.wait_with_output().unwrap();
|
||||
let part: Vec<Value> =
|
||||
serde_json::from_slice(&out.stdout).unwrap_or_else(|e| panic!("oracle JSON: {e}\n{}", String::from_utf8_lossy(&out.stderr)));
|
||||
results.extend(part);
|
||||
}
|
||||
assert_eq!(results.len(), cases.len());
|
||||
let mut failed = Vec::new();
|
||||
for r in &results {
|
||||
let case = r["case"].as_str().unwrap_or("");
|
||||
let want: Value =
|
||||
std::fs::read_to_string(dir.join(case).join("expected.json")).ok().and_then(|t| serde_json::from_str(&t).ok()).unwrap_or_default();
|
||||
if want.is_null() || want["known_failure"].is_string() || r["pass"].as_bool().unwrap_or(false) {
|
||||
continue;
|
||||
}
|
||||
let checks: Vec<String> = r["checks"]
|
||||
.as_array()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter(|c| !c["ok"].as_bool().unwrap_or(false))
|
||||
.map(|c| format!("{} got {} want {}", c["check"].as_str().unwrap_or(""), c["got"], c["want"]))
|
||||
.collect();
|
||||
failed.push(format!("{case}: {} {}", r["error"].as_str().unwrap_or(""), checks.join("; ")));
|
||||
}
|
||||
assert!(failed.is_empty(), "oracle cases failed:\n{}", failed.join("\n"));
|
||||
}
|
||||
@@ -13,10 +13,8 @@
|
||||
// unchanged, the kept build starts; when a new build is deployed, it is downloaded first
|
||||
// (waiting at most UPDATE_TIMEOUT_MS; the download goes on in the background after that) and
|
||||
// started. When the site has no `build.json` (the app was removed), launches go to the network,
|
||||
// the kept build answering when that fails. So they do when `build.json` is redirected or
|
||||
// refused (a sign-in in front of the app): the browser then shows the sign-in. Each page then
|
||||
// gets its files from the build it started (remembered in PAGES), never from another build
|
||||
// with a file of the same name.
|
||||
// the kept build answering when that fails. Each page then gets its files from the build it
|
||||
// started (remembered in PAGES), never from another build with a file of the same name.
|
||||
//
|
||||
// A cache is only deleted when nothing uses it: a download holds its lock (Web Locks, shared,
|
||||
// in any tab or worker; the browser releases it when the download ends or its worker stops), and
|
||||
@@ -160,14 +158,9 @@ function within(ms, promise) {
|
||||
/** No app is deployed here any more (the server says `build.json` doesn't exist). */
|
||||
const GONE = "gone";
|
||||
|
||||
/** The site wants a sign-in first (`build.json` is redirected, often to another origin, or refused). */
|
||||
const SIGN_IN = "sign-in";
|
||||
|
||||
/** The deployed build's version and files, GONE, SIGN_IN, or null (unknown: offline, failing, invalid). */
|
||||
/** The deployed build's version and files, GONE, or null (unknown: offline, failing, invalid). */
|
||||
async function deployed() {
|
||||
// `manual`: a redirect to another origin would only fail (CORS), looking like being offline.
|
||||
const response = await fetch(url("build.json"), { cache: "no-cache", redirect: "manual" });
|
||||
if (response.type === "opaqueredirect" || response.status === 401 || response.status === 403) return SIGN_IN;
|
||||
const response = await fetch(url("build.json"), { cache: "no-cache" });
|
||||
if (response.status === 404 || response.status === 410) return GONE;
|
||||
const build = response.ok ? await response.json() : null;
|
||||
const valid = build && typeof build.version === "string" && Array.isArray(build.files);
|
||||
@@ -189,7 +182,6 @@ async function launch(event) {
|
||||
if (!kept) return [await fetch(event.request), NETWORK]; // Nothing kept yet (the first visit).
|
||||
extend(event, sweep());
|
||||
const build = await within(NAVIGATE_TIMEOUT_MS, deployed());
|
||||
if (build === SIGN_IN) return [await fetch(event.request), NETWORK];
|
||||
if (build === GONE) {
|
||||
const page = await fetch(event.request).catch(() => null);
|
||||
return page && page.ok ? [page, NETWORK] : [kept, CACHE];
|
||||
|
||||
@@ -41,7 +41,7 @@ rustls = { version = "0.23", default-features = false, features = ["ring", "std"
|
||||
# Keychain on macOS, Credential Manager on Windows, the Secret Service (pure-Rust D-Bus) elsewhere.
|
||||
keyring = { version = "3.6", default-features = false, features = ["apple-native", "windows-native", "async-secret-service", "async-io", "crypto-rust"] }
|
||||
|
||||
[target.'cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))'.dependencies]
|
||||
[target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies]
|
||||
quick-xml = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
@@ -65,11 +65,6 @@ windows-sys = { version = "0.61", features = [
|
||||
] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
# The 3D mouse through the 3Dconnexion driver, which keeps the device to itself (src/space_mouse.rs).
|
||||
solidcraft-3dx-client = { workspace = true }
|
||||
# The 3D mouse (src/space_mouse.rs), in the shared-device open mode so that a running 3DxWare
|
||||
# driver does not shut the app out.
|
||||
hidapi = { version = "2.6.7", default-features = false, features = ["macos-shared-device"] }
|
||||
# Files opened from Finder and the Dock (src/open_documents.rs); all already come with eframe and rfd.
|
||||
objc2 = "0.6"
|
||||
objc2-app-kit = { version = "0.3", default-features = false, features = ["std", "NSApplication", "NSResponder"] }
|
||||
|
||||
@@ -292,16 +292,13 @@ mod tests {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
let (tx, rx) = channel::<ControlRequest>();
|
||||
// The slow request is held until the test has seen it time out, so the test does not
|
||||
// race the clock on a loaded machine.
|
||||
let (gate_tx, gate_rx) = channel::<()>();
|
||||
let ran = std::sync::Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
|
||||
let seen = ran.clone();
|
||||
std::thread::spawn(move || {
|
||||
for req in rx {
|
||||
seen.lock().unwrap().push(req.method.clone());
|
||||
if req.method == "slow" {
|
||||
let _ = gate_rx.recv();
|
||||
std::thread::sleep(Duration::from_millis(400));
|
||||
}
|
||||
let _ = req.reply.send(json!({"ok": true, "result": req.method}));
|
||||
}
|
||||
@@ -328,15 +325,8 @@ mod tests {
|
||||
assert!(r["busy"] == true && r["id"] == 2, "{r}");
|
||||
let r = ask(json!({"id": 3, "method": "control.pending"}));
|
||||
assert_eq!((&r["result"]["pending"], &r["result"]["done"]), (&json!(1), &json!(false)), "{r}");
|
||||
gate_tx.send(()).unwrap();
|
||||
let deadline = std::time::Instant::now() + Duration::from_secs(10);
|
||||
let r = loop {
|
||||
let r = ask(json!({"id": 4, "method": "control.pending"}));
|
||||
if r["result"]["done"] == true || std::time::Instant::now() > deadline {
|
||||
break r;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
};
|
||||
std::thread::sleep(Duration::from_millis(600));
|
||||
let r = ask(json!({"id": 4, "method": "control.pending"}));
|
||||
assert_eq!(
|
||||
(&r["result"]["pending"], &r["result"]["done"], &r["result"]["reply"]["result"]),
|
||||
(&json!(1), &json!(true), &json!("slow")),
|
||||
|
||||
@@ -25,7 +25,7 @@ mod native_crash;
|
||||
mod native_frame;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod open_documents;
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
mod space_mouse;
|
||||
mod startup;
|
||||
|
||||
@@ -38,7 +38,7 @@ struct App {
|
||||
starting: Option<std::path::PathBuf>,
|
||||
/// Frames begun; the second one means the first was drawn (startup is over).
|
||||
frames: u32,
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
space_mouse: space_mouse::SpaceMouse,
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ impl eframe::App for App {
|
||||
self.solve.open_path_async(&f);
|
||||
}
|
||||
self.solve.logic(ctx);
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
if !self.solve.file_picker_busy() {
|
||||
self.space_mouse.tick(&mut self.solve, ctx);
|
||||
}
|
||||
@@ -78,7 +78,7 @@ impl eframe::App for App {
|
||||
}
|
||||
fn raw_input_hook(&mut self, _ctx: &egui::Context, raw: &mut egui::RawInput) {
|
||||
// Before the app's own hook, which adds the control channel's events.
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
self.space_mouse.raw_input_hook(self.solve.preferences.space_mouse_enabled, raw);
|
||||
self.solve.raw_input_hook(raw);
|
||||
}
|
||||
@@ -116,7 +116,6 @@ fn services(cc: &eframe::CreationContext<'_>) -> Services {
|
||||
let parent = rfd::AsyncFileDialog::new().set_parent(cc);
|
||||
let open_parent = parent.clone();
|
||||
let program_parent = parent.clone();
|
||||
let file_parent = parent.clone();
|
||||
Services {
|
||||
pick_open: Some(Box::new(move |first: &[&str]| {
|
||||
// The file type the command reads comes first, so the picker opens on it (#1801).
|
||||
@@ -167,22 +166,6 @@ fn services(cc: &eframe::CreationContext<'_>) -> Services {
|
||||
.pick_file();
|
||||
Box::pin(async move { future.await.map(|p| p.path().to_string_lossy().into_owned()) })
|
||||
})),
|
||||
// One kind of file (the agent chat's attachments): only the types it names, as given and in
|
||||
// lower and upper case (Linux dialogs match case; macOS allows just the listed types).
|
||||
pick_file: Some(Box::new(move |label: &str, exts: &[&str]| {
|
||||
let mut all: Vec<String> = Vec::new();
|
||||
for e in exts {
|
||||
for v in [e.to_string(), e.to_ascii_lowercase(), e.to_ascii_uppercase()] {
|
||||
if !all.contains(&v) {
|
||||
all.push(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
let all: Vec<&str> = all.iter().map(String::as_str).collect();
|
||||
// Any other file attaches by reference, so the list is not the only choice.
|
||||
let future = file_parent.clone().add_filter(label, &all).add_filter("All files", &["*"]).pick_file();
|
||||
Box::pin(async move { future.await.map(|p| p.path().to_string_lossy().into_owned()) })
|
||||
})),
|
||||
pick_program: Some(Box::new(move || {
|
||||
let dialog = program_parent.clone().set_title("Choose your slicer");
|
||||
let dialog = if cfg!(target_os = "windows") { dialog.add_filter("Programs", &["exe"]) } else { dialog };
|
||||
@@ -672,14 +655,14 @@ fn main() -> eframe::Result {
|
||||
if !notes.is_empty() {
|
||||
app.set_status(notes.join(" "), true);
|
||||
}
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
let space_mouse = space_mouse::SpaceMouse::start(&cc.egui_ctx);
|
||||
solidcraft_engine::crash::phase("first frame");
|
||||
Ok(Box::new(App {
|
||||
solve: app,
|
||||
starting,
|
||||
frames: 0,
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
space_mouse,
|
||||
}))
|
||||
}),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! 3D mouse navigation on Windows and macOS (through hidapi) and Linux (through spacenavd when it runs, else
|
||||
//! 3D mouse navigation on Windows (through hidapi) and Linux (through spacenavd when it runs, else
|
||||
//! hidraw). Read directly, any HID multi-axis controller works: 3Dconnexion's and homemade ones.
|
||||
//! The device reader never touches the document or UI; it publishes the latest axes, and the egui
|
||||
//! thread applies them once per frame.
|
||||
@@ -8,12 +8,11 @@ mod hidraw;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod spacenavd;
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use egui::Context;
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
#[cfg(target_os = "windows")]
|
||||
use hidapi::{DeviceInfo, HidApi};
|
||||
use quick_xml::Reader;
|
||||
use quick_xml::events::Event;
|
||||
@@ -22,14 +21,6 @@ use solidcraft_ui_egui::SolveApp;
|
||||
use solidcraft_ui_egui::space_mouse::{Axes, AxisRule, DriverMapping, WheelGuard, navigate_frame};
|
||||
|
||||
const STALE_AFTER: Duration = Duration::from_millis(150);
|
||||
/// Whether the 3Dconnexion driver (macOS) has the device: it then reports the cap, and reading
|
||||
/// the HID device would fail or report it twice.
|
||||
static DRIVER_CONNECTED: AtomicBool = AtomicBool::new(false);
|
||||
/// Whether this app registered with the 3Dconnexion driver (macOS). The driver then has the device
|
||||
/// whether or not it has said so yet, so a failure to open it by HID is no news.
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
static DRIVER_REGISTERED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
const RETRY_AFTER: Duration = Duration::from_secs(2);
|
||||
const MAX_PROFILE_BYTES: u64 = 1024 * 1024;
|
||||
/// Room for one input report: the 4096 bytes `solidcraft_space_mouse` places fields in, plus the
|
||||
@@ -197,7 +188,7 @@ impl Reading {
|
||||
|
||||
/// The Generic Desktop "Multi-Axis Controller" usage names a 3D mouse's motion interface, whoever
|
||||
/// made it, and excludes ordinary mice and the keyboard interfaces of 3D mice.
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
#[cfg(target_os = "windows")]
|
||||
fn is_3d_mouse(info: &DeviceInfo) -> bool {
|
||||
info.usage_page() == 0x01 && info.usage() == 0x08
|
||||
}
|
||||
@@ -219,9 +210,6 @@ fn decode_report(report: &[u8], layout: Layout, reading: &mut Reading, now: Inst
|
||||
|
||||
/// Publishes one input report from the device to the egui thread.
|
||||
fn publish(ctx: &Context, shared: &Mutex<Reading>, layout: Layout, report: &[u8]) {
|
||||
if DRIVER_CONNECTED.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
if let Ok(mut reading) = shared.lock()
|
||||
&& decode_report(report, layout, &mut reading, Instant::now())
|
||||
{
|
||||
@@ -246,7 +234,7 @@ fn reset(ctx: &Context, shared: &Mutex<Reading>) {
|
||||
ctx.request_repaint();
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
#[cfg(target_os = "windows")]
|
||||
fn reader(ctx: Context, shared: Arc<Mutex<Reading>>) {
|
||||
let Ok(mut api) = HidApi::new() else {
|
||||
log::warn!("3D mouse: HID initialization failed");
|
||||
@@ -256,16 +244,7 @@ fn reader(ctx: Context, shared: Arc<Mutex<Reading>>) {
|
||||
if let Err(error) = api.refresh_devices() {
|
||||
log::warn!("3D mouse: device scan failed: {error}");
|
||||
} else if let Some((device, layout)) = api.device_list().filter(|info| is_3d_mouse(info)).find_map(|info| {
|
||||
let device = info
|
||||
.open_device(&api)
|
||||
.inspect_err(|error| {
|
||||
// On macOS: missing Input Monitoring permission, or the 3DxWare helper holding the device.
|
||||
// The driver having the device is the reason it cannot be opened, not a problem.
|
||||
if !DRIVER_CONNECTED.load(Ordering::Relaxed) && !DRIVER_REGISTERED.load(Ordering::Relaxed) {
|
||||
log::warn!("3D mouse: cannot open the device: {error}");
|
||||
}
|
||||
})
|
||||
.ok()?;
|
||||
let device = info.open_device(&api).ok()?;
|
||||
let layout = device_layout(&device, info.vendor_id())?;
|
||||
Some((device, layout))
|
||||
}) {
|
||||
@@ -291,7 +270,7 @@ fn reader(ctx: Context, shared: Arc<Mutex<Reading>>) {
|
||||
/// read, or it's empty), a 3Dconnexion device (Logitech's id for the old SpaceNavigator,
|
||||
/// 3Dconnexion's own for the rest) gets 3Dconnexion's layout, and any other device is left alone
|
||||
/// rather than guessed at.
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
#[cfg(target_os = "windows")]
|
||||
fn device_layout(device: &hidapi::HidDevice, vendor_id: u16) -> Option<Layout> {
|
||||
let mut descriptor = [0_u8; hidapi::MAX_REPORT_DESCRIPTOR_SIZE];
|
||||
let read = device.get_report_descriptor(&mut descriptor);
|
||||
@@ -356,40 +335,6 @@ fn reader(ctx: Context, shared: Arc<Mutex<Reading>>) {
|
||||
}
|
||||
}
|
||||
|
||||
/// macOS: the 3Dconnexion driver gives the device to its own clients only, so SolidCraft registers
|
||||
/// as one. Without the driver (or with its helper not running) the HID reader above takes over.
|
||||
#[cfg(target_os = "macos")]
|
||||
fn start_driver_client(ctx: &Context, shared: &Arc<Mutex<Reading>>) {
|
||||
use solidcraft_3dx_client::Event;
|
||||
let (ctx, shared) = (ctx.clone(), Arc::clone(shared));
|
||||
let started = solidcraft_3dx_client::start(
|
||||
"SolidCraft",
|
||||
move |event| match event {
|
||||
Event::DeviceAdded => DRIVER_CONNECTED.store(true, Ordering::Relaxed),
|
||||
Event::DeviceRemoved => {
|
||||
DRIVER_CONNECTED.store(false, Ordering::Relaxed);
|
||||
reset(&ctx, &shared);
|
||||
}
|
||||
Event::Axes { translation, rotation } => {
|
||||
if let Ok(mut reading) = shared.lock() {
|
||||
let now = Instant::now();
|
||||
reading.axes = Axes { translation, rotation };
|
||||
reading.at = [now; 6];
|
||||
ctx.request_repaint();
|
||||
}
|
||||
}
|
||||
Event::Buttons(_) => {}
|
||||
},
|
||||
|connected| match connected {
|
||||
Ok(()) => DRIVER_REGISTERED.store(true, Ordering::Relaxed),
|
||||
Err(error) => log::info!("3D mouse: no driver client: {error}"),
|
||||
},
|
||||
);
|
||||
if let Err(error) = started {
|
||||
log::info!("3D mouse: no driver client: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SpaceMouse {
|
||||
shared: Arc<Mutex<Reading>>,
|
||||
last_frame: Instant,
|
||||
@@ -409,8 +354,6 @@ impl SpaceMouse {
|
||||
if let Err(error) = std::thread::Builder::new().name("solidcraft-3d-mouse".into()).spawn(move || reader(worker_ctx, worker_state)) {
|
||||
log::warn!("3D mouse: reader could not start: {error}");
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
start_driver_client(ctx, &shared);
|
||||
Self {
|
||||
shared,
|
||||
last_frame: now,
|
||||
@@ -625,10 +568,7 @@ mod tests {
|
||||
wheel: WheelGuard::default(),
|
||||
started: now,
|
||||
};
|
||||
// A window that has run a frame with focus, as the app's always has by now.
|
||||
let ctx = Context::default();
|
||||
let _ = ctx.run_ui(egui::RawInput::default(), |_| {});
|
||||
mouse.tick_at(&mut app, &ctx, now);
|
||||
mouse.tick_at(&mut app, &Context::default(), now);
|
||||
let DeviceMotion { motion, seconds } = app.viewport.device_motion.expect("the frame hands the page its motion");
|
||||
assert_eq!(motion, Motion { translation: [0.0; 3], rotation: [0.0, 0.0, -1.0] });
|
||||
assert!(seconds > 0.0);
|
||||
|
||||
@@ -89,15 +89,6 @@ pub(crate) fn pin_icon(p: &egui::Painter, r: Rect, on: bool, col: Color32) {
|
||||
p.line_segment([at(0.0, -2.0), at(0.0, 7.0)], stroke);
|
||||
}
|
||||
|
||||
/// A breadcrumb separator ("Sketch › Create"): a thin chevron about 3 by 6 px, centred on `c`.
|
||||
/// Drawn rather than typed, because a font's "›" sits on the baseline in some languages
|
||||
/// (Japanese), where it reads as a comma.
|
||||
pub(crate) fn breadcrumb(p: &egui::Painter, c: egui::Pos2, col: Color32) {
|
||||
let s = Stroke::new(1.0, col);
|
||||
p.line_segment([pos2(c.x - 1.5, c.y - 3.0), pos2(c.x + 1.5, c.y)], s);
|
||||
p.line_segment([pos2(c.x + 1.5, c.y), pos2(c.x - 1.5, c.y + 3.0)], s);
|
||||
}
|
||||
|
||||
pub(crate) fn caret(p: &egui::Painter, c: egui::Pos2, col: Color32) {
|
||||
p.add(egui::Shape::convex_polygon(vec![pos2(c.x - 3.5, c.y - 2.0), pos2(c.x + 3.5, c.y - 2.0), pos2(c.x, c.y + 2.5)], col, Stroke::NONE));
|
||||
}
|
||||
|
||||
+1
-16
@@ -82,7 +82,6 @@
|
||||
"Density": "Hustota",
|
||||
"Designs you open or save appear here.": "Zde se zobrazí návrhy, které otevřete nebo uložíte.",
|
||||
"Determined by To Object. Choose Angle to edit the angular extent.": "Určeno volbou K objektu. Pro úpravu úhlového rozsahu zvolte Úhel.",
|
||||
"Direction": "Směr",
|
||||
"Discard": "Zahodit",
|
||||
"Do not export hidden edges": "Neexportovat skryté hrany",
|
||||
"Double-click to rename": "Dvojklikem přejmenujete",
|
||||
@@ -113,6 +112,7 @@
|
||||
"Follow the camera (Look At sketches)": "Sledovat kameru (náčrty natočené k pohledu)",
|
||||
"Font": "Písmo",
|
||||
"Full Length": "Celá délka",
|
||||
"Gap": "Mezera",
|
||||
"Grid": "Mřížka",
|
||||
"Height": "Výška",
|
||||
"Hidden edges": "Skryté hrany",
|
||||
@@ -121,7 +121,6 @@
|
||||
"Home view": "Výchozí pohled",
|
||||
"In This Design": "V tomto návrhu",
|
||||
"In place (surfaces)": "Na místě (plochy)",
|
||||
"In plane": "V rovině",
|
||||
"Inside": "Uvnitř",
|
||||
"Interferes": "Koliduje",
|
||||
"Issue title": "Název hlášení",
|
||||
@@ -155,7 +154,6 @@
|
||||
"No matching appearances": "Žádné odpovídající vzhledy",
|
||||
"No visible bodies to export.": "Žádná viditelná tělesa k exportu.",
|
||||
"None": "Žádné",
|
||||
"Normal to sketch": "Kolmo ke skice",
|
||||
"Not available yet": "Zatím není k dispozici",
|
||||
"Notch": "Zářez",
|
||||
"Opacity": "Neprůhlednost",
|
||||
@@ -687,15 +685,6 @@
|
||||
"joint.from_bodies_hint": "Každé vybrané těleso se přesune do vlastní nové komponenty a zůstane na místě.",
|
||||
"joint.one_body": "Oba výběry jsou na {body}. Druhý vyberte na jiném dílu.",
|
||||
"joint.one_component": "Tyto díly jsou tělesy jedné komponenty: {bodies}. Kloub pohybuje jednou komponentou vůči druhé, takže každý díl potřebuje vlastní komponentu.",
|
||||
"messages.ago_h": "před {n} h",
|
||||
"messages.ago_min": "před {n} min",
|
||||
"messages.ago_s": "před {n} s",
|
||||
"messages.chip_hint": "Klikněte pro všechny zprávy této relace",
|
||||
"messages.clear": "Vymazat",
|
||||
"messages.copy_all": "Kopírovat vše",
|
||||
"messages.menu": "Zprávy…",
|
||||
"messages.none": "Zatím žádné zprávy.",
|
||||
"messages.title": "Zprávy",
|
||||
"nav.touchpad.help": "Podržením Shift a pohybem ukazatele posouváte; podržením Alt/Option a pohybem otáčíte. Tlačítko myši není potřeba. Svislé posouvání přibližuje.",
|
||||
"nav.touchpad.label": "Touchpad",
|
||||
"nav.touchpad.orbit": "Otáčení (podržte Alt/Option a pohybujte; bez tlačítka myši)",
|
||||
@@ -1039,7 +1028,6 @@
|
||||
"ui.construct": "Konstrukce",
|
||||
"ui.construction": "Konstrukční",
|
||||
"ui.context_menu": "Místní nabídka",
|
||||
"ui.continuity_gap": "Mezera",
|
||||
"ui.copy": "Kopírovat",
|
||||
"ui.corner": "Roh",
|
||||
"ui.corners": "Rohy",
|
||||
@@ -1346,8 +1334,6 @@
|
||||
"ui.save_mesh_pick_bodies": "Vyberte tělesa k uložení",
|
||||
"ui.save_mesh_temp_failed": "Nelze vytvořit {folder}: {error}",
|
||||
"ui.saved_to": "Uloženo {path}",
|
||||
"ui.sbox_pin": "Připnout nahoru ve vyhledávání S",
|
||||
"ui.sbox_unpin": "Odepnout",
|
||||
"ui.scale": "Měřítko",
|
||||
"ui.section_analysis": "Analýza řezu",
|
||||
"ui.select": "Vybrat",
|
||||
@@ -1358,7 +1344,6 @@
|
||||
"ui.send": "Odeslat",
|
||||
"ui.send_to_slicer": "Odeslat do sliceru",
|
||||
"ui.sent_to_slicer": "Odesláno do {slicer}",
|
||||
"ui.sheet_gap": "Mezera",
|
||||
"ui.shell": "Skořepina",
|
||||
"ui.show": "Zobrazit",
|
||||
"ui.showAll": "Zobrazit vše",
|
||||
|
||||
+1
-23
@@ -26,7 +26,6 @@
|
||||
"Current orthographic view (1:1 mm)": "Aktuelle orthogonale Ansicht (1:1 mm)",
|
||||
"Current perspective view": "Aktuelle perspektivische Ansicht",
|
||||
"Curvature (G2)": "Krümmung (G2)",
|
||||
"Direction": "Richtung",
|
||||
"Do not export hidden edges": "Verdeckte Kanten nicht exportieren",
|
||||
"Don't ask again": "Nicht mehr fragen",
|
||||
"Drive Joint": "Gelenk antreiben",
|
||||
@@ -43,11 +42,11 @@
|
||||
"Faces": "Flächen",
|
||||
"Find in Browser": "Im Browser suchen",
|
||||
"Flat": "Flach",
|
||||
"Gap": "Spalt",
|
||||
"Hidden edges": "Verdeckte Kanten",
|
||||
"Holes": "Löcher",
|
||||
"Holes (edges on them)": "Löcher (ihre Kanten)",
|
||||
"In place (surfaces)": "An Ort und Stelle (Flächen)",
|
||||
"In plane": "In der Ebene",
|
||||
"Inside": "Innen",
|
||||
"Length": "Länge",
|
||||
"Max": "Max",
|
||||
@@ -56,7 +55,6 @@
|
||||
"New study": "Neue Untersuchung",
|
||||
"No visible bodies to export.": "Keine sichtbaren Körper zum Exportieren.",
|
||||
"None": "Keine",
|
||||
"Normal to sketch": "Normal zur Skizze",
|
||||
"Notch": "Ausklinkung",
|
||||
"Outside": "Außen",
|
||||
"Overall length": "Gesamtlänge",
|
||||
@@ -425,10 +423,8 @@
|
||||
"cmd.timeline_roll_to": "Verlaufsmarkierung verschieben",
|
||||
"cmd.timeline_rollback": "Zeitleistenmarkierung verschieben",
|
||||
"cmd.timeline_suppress": "Merkmal unterdrücken",
|
||||
"cmd.view_ambient_occlusion": "Umgebungsverdeckung",
|
||||
"cmd.view_apply": "Benannte Ansicht wiederherstellen",
|
||||
"cmd.view_delete": "Benannte Ansicht löschen",
|
||||
"cmd.view_graphics_preset": "Grafikprofil",
|
||||
"cmd.view_list": "Benannte Ansichten auflisten",
|
||||
"cmd.view_navigate": "Ansicht navigieren",
|
||||
"cmd.view_rename": "Benannte Ansicht umbenennen",
|
||||
@@ -449,12 +445,7 @@
|
||||
"coupling.rack": "Zahnstange",
|
||||
"coupling.radius_1": "Radius 1",
|
||||
"coupling.radius_2": "Radius 2",
|
||||
"dialog.bodies_count": "{count} Körper",
|
||||
"dialog.clearance_invalid": "der Abstand muss eine Länge von 0 oder mehr sein",
|
||||
"dialog.compute": "Berechnen",
|
||||
"dialog.compute_again": "Erneut berechnen",
|
||||
"dialog.computing": "Wird berechnet… {seconds} s",
|
||||
"dialog.could_not_be_checked": "nicht prüfbar",
|
||||
"dialog.custom_axis": "↖ Eigene Achse",
|
||||
"dialog.edit_title": "{title} bearbeiten",
|
||||
"dialog.select": "↖ Auswählen",
|
||||
@@ -758,8 +749,6 @@
|
||||
"ui.ai_toolbar_button_note": "Die KI-Schaltfläche am rechten Ende der Werkzeugleiste. Strg+Umschalt+A und das Ansicht-Menü öffnen das Panel weiterhin.",
|
||||
"ui.ai_undo_tip": "Alles, was der Agent für diese Nachricht geändert hat, in einem Schritt rückgängig machen",
|
||||
"ui.all": "Alle",
|
||||
"ui.ambient_occlusion": "Umgebungsverdeckung",
|
||||
"ui.ambient_occlusion_help": "Sanfte Schattierung an Kontaktstellen und in Vertiefungen undurchsichtiger Flächen. Erfordert die GPU-Ansicht.",
|
||||
"ui.analysis": "Analyse",
|
||||
"ui.angle": "Winkel",
|
||||
"ui.angle_decimals": "Winkel-Dezimalstellen",
|
||||
@@ -777,7 +766,6 @@
|
||||
"ui.base_point": "Basispunkt",
|
||||
"ui.blocked": "Verdeckt",
|
||||
"ui.bodies": "Körper",
|
||||
"ui.bodies_all_if_none": "Körper (alle, wenn keine)",
|
||||
"ui.box": "Block",
|
||||
"ui.break": "Unterbrechen",
|
||||
"ui.calibrate": "Kalibrieren",
|
||||
@@ -828,7 +816,6 @@
|
||||
"ui.continuity_g0": "G0 Knick",
|
||||
"ui.continuity_g1": "G1 Tangente",
|
||||
"ui.continuity_g2": "G2 Krümmung",
|
||||
"ui.continuity_gap": "Lücke",
|
||||
"ui.copy": "Kopieren",
|
||||
"ui.corner": "Ecke",
|
||||
"ui.corners": "Ecken",
|
||||
@@ -914,11 +901,6 @@
|
||||
"ui.fully_constrained": "Vollständig bestimmt",
|
||||
"ui.general": "Allgemein",
|
||||
"ui.graphics": "Grafik",
|
||||
"ui.graphics_custom": "Benutzerdefiniert",
|
||||
"ui.graphics_performance": "Leistung",
|
||||
"ui.graphics_preset": "Grafikprofil",
|
||||
"ui.graphics_preset_help": "Qualität: Umgebungsverdeckung, feine Anzeigequalität und adaptives 2× Supersampling. Leistung: keine Umgebungsverdeckung, normale Anzeigequalität und kein Supersampling. Manuelle Änderungen: Benutzerdefiniert. MSAA bleibt unverändert.",
|
||||
"ui.graphics_quality": "Qualität",
|
||||
"ui.groupSelected": "Auswahl gruppieren",
|
||||
"ui.heading_assemble": "ZUSAMMENBAU",
|
||||
"ui.heading_box": "BLOCK",
|
||||
@@ -943,7 +925,6 @@
|
||||
"ui.heading_home": "START",
|
||||
"ui.heading_insert": "EINFÜGEN",
|
||||
"ui.heading_inspect": "PRÜFEN",
|
||||
"ui.heading_interference": "EINGRIFF",
|
||||
"ui.heading_loft": "LOFT",
|
||||
"ui.heading_measure": "Messen",
|
||||
"ui.heading_mesh": "MESH",
|
||||
@@ -1127,8 +1108,6 @@
|
||||
"ui.save_mesh_pick_bodies": "Wähle die Körper zum Speichern aus",
|
||||
"ui.save_mesh_temp_failed": "{folder} konnte nicht angelegt werden: {error}",
|
||||
"ui.saved_to": "Gespeichert: {path}",
|
||||
"ui.sbox_pin": "Oben im S-Feld anheften",
|
||||
"ui.sbox_unpin": "Lösen",
|
||||
"ui.scale": "Skalieren",
|
||||
"ui.second_rail": "Zweite Leitkurve",
|
||||
"ui.section_analysis": "Querschnittsanalyse",
|
||||
@@ -1139,7 +1118,6 @@
|
||||
"ui.send": "Senden",
|
||||
"ui.send_to_slicer": "An Slicer senden",
|
||||
"ui.sent_to_slicer": "An {slicer} gesendet",
|
||||
"ui.sheet_gap": "Spalt",
|
||||
"ui.shell": "Hülle",
|
||||
"ui.show": "Einblenden",
|
||||
"ui.showAll": "Alle einblenden",
|
||||
|
||||
+1
-84
@@ -31,7 +31,6 @@
|
||||
"Arc length": "Arc length",
|
||||
"Area": "Area",
|
||||
"Attachment": "Attachment",
|
||||
"BOUNDARY SURFACE": "BOUNDARY SURFACE",
|
||||
"BROWSER": "BROWSER",
|
||||
"Bend Line Position": "Bend Line Position",
|
||||
"Bend Position": "Bend Position",
|
||||
@@ -74,7 +73,6 @@
|
||||
"Counterbore depth": "Counterbore depth",
|
||||
"Countersink angle": "Countersink angle",
|
||||
"Create Issue — review draft": "Create Issue — review draft",
|
||||
"Cross curves": "Cross curves",
|
||||
"Cube": "Cube",
|
||||
"Current orthographic view (1:1 mm)": "Current orthographic view (1:1 mm)",
|
||||
"Current perspective view": "Current perspective view",
|
||||
@@ -88,7 +86,6 @@
|
||||
"Density": "Density",
|
||||
"Designs you open or save appear here.": "Designs you open or save appear here.",
|
||||
"Determined by To Object. Choose Angle to edit the angular extent.": "Determined by To Object. Choose Angle to edit the angular extent.",
|
||||
"Direction": "Direction",
|
||||
"Discard": "Discard",
|
||||
"Do not export hidden edges": "Do not export hidden edges",
|
||||
"Don't ask again": "Don't ask again",
|
||||
@@ -122,6 +119,7 @@
|
||||
"Follow the camera (Look At sketches)": "Follow the camera (Look At sketches)",
|
||||
"Font": "Font",
|
||||
"Full Length": "Full Length",
|
||||
"Gap": "Gap",
|
||||
"Grid": "Grid",
|
||||
"Height": "Height",
|
||||
"Hidden edges": "Hidden edges",
|
||||
@@ -130,7 +128,6 @@
|
||||
"Home view": "Home view",
|
||||
"In This Design": "In This Design",
|
||||
"In place (surfaces)": "In place (surfaces)",
|
||||
"In plane": "In plane",
|
||||
"Inside": "Inside",
|
||||
"Interferes": "Interferes",
|
||||
"Issue title": "Issue title",
|
||||
@@ -166,7 +163,6 @@
|
||||
"No matching appearances": "No matching appearances",
|
||||
"No visible bodies to export.": "No visible bodies to export.",
|
||||
"None": "None",
|
||||
"Normal to sketch": "Normal to sketch",
|
||||
"Not available yet": "Not available yet",
|
||||
"Notch": "Notch",
|
||||
"Opacity": "Opacity",
|
||||
@@ -350,7 +346,6 @@
|
||||
"cmd.component_move_sketches": "Move Sketches to Component",
|
||||
"cmd.component_paste_new": "Paste New",
|
||||
"cmd.component_properties": "Component Physical Properties",
|
||||
"cmd.component_relink": "Relink Source",
|
||||
"cmd.component_rename": "Rename Component",
|
||||
"cmd.component_revert_position": "Revert Position",
|
||||
"cmd.component_unlock": "Unlock Contents",
|
||||
@@ -560,8 +555,6 @@
|
||||
"cmd.sketch_isoparametric_curve": "Isoparametric Curve",
|
||||
"cmd.sketch_line3d": "3D Line",
|
||||
"cmd.sketch_line_midpoint": "Midpoint Line",
|
||||
"cmd.sketch_loose_ends": "Loose Ends",
|
||||
"cmd.sketch_loose_ends_tie": "Tie Loose Ends",
|
||||
"cmd.sketch_move": "Move/Copy Sketch Entities",
|
||||
"cmd.sketch_move3d": "Move 3D Point",
|
||||
"cmd.sketch_move_point": "Drag Sketch Point",
|
||||
@@ -633,10 +626,8 @@
|
||||
"cmd.timeline_roll_to": "Roll History Marker",
|
||||
"cmd.timeline_rollback": "Move Timeline Marker",
|
||||
"cmd.timeline_suppress": "Suppress Feature",
|
||||
"cmd.view_ambient_occlusion": "Ambient Occlusion",
|
||||
"cmd.view_apply": "Restore Named View",
|
||||
"cmd.view_delete": "Delete Named View",
|
||||
"cmd.view_graphics_preset": "Graphics Preset",
|
||||
"cmd.view_list": "List Named Views",
|
||||
"cmd.view_navigate": "Navigate View",
|
||||
"cmd.view_rename": "Rename Named View",
|
||||
@@ -658,12 +649,7 @@
|
||||
"coupling.rack": "Rack",
|
||||
"coupling.radius_1": "Radius 1",
|
||||
"coupling.radius_2": "Radius 2",
|
||||
"dialog.bodies_count": "{count} bodies",
|
||||
"dialog.clearance_invalid": "the clearance must be a length of 0 or more",
|
||||
"dialog.compute": "Compute",
|
||||
"dialog.compute_again": "Compute again",
|
||||
"dialog.computing": "Computing… {seconds} s",
|
||||
"dialog.could_not_be_checked": "could not be checked",
|
||||
"dialog.custom_axis": "↖ Custom Axis",
|
||||
"dialog.edit_title": "EDIT {title}",
|
||||
"dialog.select": "↖ Select",
|
||||
@@ -719,15 +705,6 @@
|
||||
"joint.from_bodies_hint": "Each picked body moves into a new component of its own and stays where it is.",
|
||||
"joint.one_body": "Both picks are on {body}. Pick the second one on another part.",
|
||||
"joint.one_component": "These parts are bodies of one component: {bodies}. A joint moves one component against another, so each part needs a component of its own.",
|
||||
"messages.ago_h": "{n} h ago",
|
||||
"messages.ago_min": "{n} min ago",
|
||||
"messages.ago_s": "{n} s ago",
|
||||
"messages.chip_hint": "Click for all messages of this session",
|
||||
"messages.clear": "Clear",
|
||||
"messages.copy_all": "Copy all",
|
||||
"messages.menu": "Messages…",
|
||||
"messages.none": "No messages yet.",
|
||||
"messages.title": "Messages",
|
||||
"motion.checked_to": "Checked to step {step} only: the check ran out of its work budget",
|
||||
"motion.collision": "{a} · {b}: steps {steps}",
|
||||
"motion.duration_hint": "seconds (empty: per step)",
|
||||
@@ -742,11 +719,9 @@
|
||||
"new configuration": "new configuration",
|
||||
"new name": "new name",
|
||||
"new parameter": "new parameter",
|
||||
"no hole": "no hole",
|
||||
"no interference": "no interference",
|
||||
"no joint that moves yet": "no joint that moves yet",
|
||||
"no picture": "no picture",
|
||||
"none": "none",
|
||||
"pattern.alignment_2": "Alignment 2",
|
||||
"pattern.alignment_hint": "Counts include the original. Center balances copies; Flip chooses the extra side for even totals.",
|
||||
"pattern.direction_1": "Direction 1",
|
||||
@@ -819,8 +794,6 @@
|
||||
"sketch.degrees_of_freedom": "{count} degrees of freedom",
|
||||
"sketch.draw_curves": "Draw as {linetype}",
|
||||
"sketch.make_selection": "Convert the selection to {linetype}",
|
||||
"sketch.open_ends": "Open {count}",
|
||||
"sketch.untied_ends": "Untied {count}",
|
||||
"sketch_text.align_center": "Center",
|
||||
"sketch_text.align_left": "Left",
|
||||
"sketch_text.align_right": "Right",
|
||||
@@ -833,7 +806,6 @@
|
||||
"sketch_text.path_clear": "Take the text off its curve",
|
||||
"sketch_text.path_hint": "A curve of the sketch the text follows: click here, then the curve",
|
||||
"sketch_text.use_font_file": "Use the font file {path}",
|
||||
"solid": "solid",
|
||||
"the parts library is not available": "the parts library is not available",
|
||||
"thread.automatic_size": "Automatic size",
|
||||
"thread.designation": "Designation",
|
||||
@@ -845,7 +817,6 @@
|
||||
"timeline.rename_group": "Rename Feature Folder",
|
||||
"timeline.reorder_many": "Reorder Features",
|
||||
"timeline.ungroup": "Remove Feature Folders",
|
||||
"to the face": "to the face",
|
||||
"to the next face": "to the next face",
|
||||
"toolbar.image": "IMAGE",
|
||||
"toolbar.render": "RENDER",
|
||||
@@ -861,15 +832,12 @@
|
||||
"ui.activate": "Activate",
|
||||
"ui.active_component": "Active component",
|
||||
"ui.agent_chat": "Agent chat",
|
||||
"ui.agent_chat_add": "Attach files",
|
||||
"ui.agent_chat_add_file": "Choose a file…",
|
||||
"ui.agent_chat_allow": "Allow",
|
||||
"ui.agent_chat_allowed": "Allowed",
|
||||
"ui.agent_chat_answered": "Answer: {option}",
|
||||
"ui.agent_chat_approval": "Allow the agent to do this?",
|
||||
"ui.agent_chat_attach_view": "Attach view",
|
||||
"ui.agent_chat_attach_view_tip": "Send a picture of the current view with the message",
|
||||
"ui.agent_chat_attachable": "Pictures, PDFs, text and CAD files",
|
||||
"ui.agent_chat_auth_agent": "Sign in with {name}",
|
||||
"ui.agent_chat_auth_env": "Set {vars} in your environment and restart SolidCraft.",
|
||||
"ui.agent_chat_auth_needed": "The agent asks you to sign in. You sign in with the agent itself; SolidCraft never sees your password or keys.",
|
||||
@@ -889,32 +857,11 @@
|
||||
"ui.agent_chat_deny": "Deny",
|
||||
"ui.agent_chat_desktop_only": "The agent chat needs the desktop app.",
|
||||
"ui.agent_chat_detecting": "Looking for agents…",
|
||||
"ui.agent_chat_dock": "Dock in the side panel",
|
||||
"ui.agent_chat_dock_left": "Dock on the left",
|
||||
"ui.agent_chat_dock_right": "Dock on the right",
|
||||
"ui.agent_chat_drop_hint": "Drop to attach to your next message",
|
||||
"ui.agent_chat_edit": "Edit",
|
||||
"ui.agent_chat_exited": "The agent stopped.",
|
||||
"ui.agent_chat_failed_start": "The agent could not start: {error}",
|
||||
"ui.agent_chat_file_cad_not_read": "{name}: a CAD file is imported into the design, not read",
|
||||
"ui.agent_chat_file_empty_picture": "{name}: an empty picture",
|
||||
"ui.agent_chat_file_error": "{name}: {error}",
|
||||
"ui.agent_chat_file_heic": "{name}: HEIC pictures can't be read here; export it as JPEG or PNG first",
|
||||
"ui.agent_chat_file_importing": "Importing into the design…",
|
||||
"ui.agent_chat_file_loading": "Reading the file…",
|
||||
"ui.agent_chat_file_not_a_file": "{name}: not a file",
|
||||
"ui.agent_chat_file_not_a_picture": "{name}: not a picture",
|
||||
"ui.agent_chat_file_not_utf8": "{name}: not UTF-8 text",
|
||||
"ui.agent_chat_file_picture_too_big": "{name}: a picture can be at most {max} MB",
|
||||
"ui.agent_chat_file_text_too_big": "{name}: a text file can be at most {max} KB",
|
||||
"ui.agent_chat_file_too_large": "{name}: too large",
|
||||
"ui.agent_chat_file_too_large_to_send": "{name}: too large to send, even scaled down",
|
||||
"ui.agent_chat_file_unreadable_picture": "{name}: not a picture this can read",
|
||||
"ui.agent_chat_files": "Files: {files}",
|
||||
"ui.agent_chat_files_left_out": "At most {max} files go with one message; left out: {files}",
|
||||
"ui.agent_chat_files_loading": "Waiting for the files to be read",
|
||||
"ui.agent_chat_files_max": "At most {max} files on one message",
|
||||
"ui.agent_chat_import_failed": "{name}: the import failed: {error}",
|
||||
"ui.agent_chat_intro": "Chat with a coding agent you already use, or a model endpoint you choose. It works on the open design through SolidCraft's tools: its changes show as it makes them and undo as one step per message. What it may do without asking is set in Preferences > AI.",
|
||||
"ui.agent_chat_key_in_env": "key set",
|
||||
"ui.agent_chat_left": "Left",
|
||||
@@ -927,18 +874,14 @@
|
||||
"ui.agent_chat_no_cli": "solidcraft-cli was not found next to the app, so the agent can't reach the design.",
|
||||
"ui.agent_chat_no_control": "The agent chat's endpoint did not start, so the agent can't reach the design.",
|
||||
"ui.agent_chat_no_images": "This agent doesn't take pictures",
|
||||
"ui.agent_chat_no_picture": "There is no picture on the clipboard",
|
||||
"ui.agent_chat_none_found": "No coding agent found. Install one of these, then press Refresh:",
|
||||
"ui.agent_chat_none_found_short": "No agent found",
|
||||
"ui.agent_chat_not_installed": "Not installed",
|
||||
"ui.agent_chat_not_signed_in": "not signed in",
|
||||
"ui.agent_chat_own_window": "Own window",
|
||||
"ui.agent_chat_paste_picture": "Paste a picture",
|
||||
"ui.agent_chat_permission": "The agent asks to run: {title}",
|
||||
"ui.agent_chat_pick": "Agent",
|
||||
"ui.agent_chat_placeholder": "Ask the agent to change the design…",
|
||||
"ui.agent_chat_placeholder_busy": "Queue a message for when the agent is done (Ctrl+Enter: send now)",
|
||||
"ui.agent_chat_pop_out": "Open in its own window, which you can move to another screen",
|
||||
"ui.agent_chat_privacy": "Nothing goes anywhere except to the agent you run, under your own account.",
|
||||
"ui.agent_chat_privacy_note": "SolidCraft sends nothing anywhere: the agent you run talks to its own service under your own account. SolidCraft never sees or stores a key or password.",
|
||||
"ui.agent_chat_queue": "Queue",
|
||||
@@ -948,8 +891,6 @@
|
||||
"ui.agent_chat_refresh": "Refresh",
|
||||
"ui.agent_chat_refused": "Not allowed for the agent: {reason}",
|
||||
"ui.agent_chat_remove": "Remove from the queue",
|
||||
"ui.agent_chat_remove_file": "Take it off this message",
|
||||
"ui.agent_chat_remove_imported": "Take it off this message; the part stays in the design (Undo removes it)",
|
||||
"ui.agent_chat_right": "Right",
|
||||
"ui.agent_chat_save": "Save",
|
||||
"ui.agent_chat_send": "Send",
|
||||
@@ -1047,8 +988,6 @@
|
||||
"ui.ai_toolbar_button_note": "The AI button at the right end of the toolbar. Ctrl+Shift+A and the View menu still open the panel.",
|
||||
"ui.ai_undo_tip": "Undo everything the agent changed for this message, as one step",
|
||||
"ui.all": "All",
|
||||
"ui.ambient_occlusion": "Ambient occlusion",
|
||||
"ui.ambient_occlusion_help": "Soft shading where opaque surfaces meet. Requires the GPU viewport.",
|
||||
"ui.analysis": "Analysis",
|
||||
"ui.angle": "Angle",
|
||||
"ui.angle_decimals": "Angle decimals",
|
||||
@@ -1066,7 +1005,6 @@
|
||||
"ui.base_point": "Base point",
|
||||
"ui.blocked": "Blocked",
|
||||
"ui.bodies": "Bodies",
|
||||
"ui.bodies_all_if_none": "Bodies (all if none)",
|
||||
"ui.box": "Box",
|
||||
"ui.break": "Break",
|
||||
"ui.calibrate": "Calibrate",
|
||||
@@ -1121,7 +1059,6 @@
|
||||
"ui.continuity_g0": "G0 crease",
|
||||
"ui.continuity_g1": "G1 tangent",
|
||||
"ui.continuity_g2": "G2 curvature",
|
||||
"ui.continuity_gap": "Gap",
|
||||
"ui.copy": "Copy",
|
||||
"ui.corner": "Corner",
|
||||
"ui.corners": "Corners",
|
||||
@@ -1209,11 +1146,6 @@
|
||||
"ui.fully_constrained": "Fully constrained",
|
||||
"ui.general": "General",
|
||||
"ui.graphics": "Graphics",
|
||||
"ui.graphics_custom": "Custom",
|
||||
"ui.graphics_performance": "Performance",
|
||||
"ui.graphics_preset": "Graphics Preset",
|
||||
"ui.graphics_preset_help": "Quality: ambient occlusion, Fine display quality and adaptive 2× supersampling. Performance: no ambient occlusion, Normal display quality and supersampling off. Manual changes show Custom. MSAA stays unchanged.",
|
||||
"ui.graphics_quality": "Quality",
|
||||
"ui.grid_follows_view": "Grid follows the view",
|
||||
"ui.grid_follows_view_hint": "Side views show the grid on the upright plane facing them instead of the ground",
|
||||
"ui.ground_shadow": "Ground shadow",
|
||||
@@ -1242,7 +1174,6 @@
|
||||
"ui.heading_home": "HOME",
|
||||
"ui.heading_insert": "INSERT",
|
||||
"ui.heading_inspect": "INSPECT",
|
||||
"ui.heading_interference": "INTERFERENCE",
|
||||
"ui.heading_loft": "LOFT",
|
||||
"ui.heading_make": "MAKE",
|
||||
"ui.heading_measure": "MEASURE",
|
||||
@@ -1367,7 +1298,6 @@
|
||||
"ui.one_side": "One Side",
|
||||
"ui.open": "Open",
|
||||
"ui.open_ellipsis": "Open...",
|
||||
"ui.open_ends_tip": "Touches nothing",
|
||||
"ui.open_preferences": "Open Preferences",
|
||||
"ui.open_recent": "Open Recent",
|
||||
"ui.operation": "Operation",
|
||||
@@ -1418,11 +1348,6 @@
|
||||
"ui.rectangle": "Rectangle",
|
||||
"ui.rectangular_pattern": "Rectangular Pattern",
|
||||
"ui.redo": "Redo",
|
||||
"ui.relink_design_changed": "The assembly changed while choosing a source. Start Relink Source again.",
|
||||
"ui.relink_done": "Source relinked for all instances.",
|
||||
"ui.relink_not_linked": "This component is not linked to a source design.",
|
||||
"ui.relink_select_one": "Select one linked component or one of its instances first.",
|
||||
"ui.relink_source_all": "Relink Source (All Instances)…",
|
||||
"ui.removeFromToolbar": "Remove from Toolbar",
|
||||
"ui.removeSection": "Remove Section",
|
||||
"ui.render": "Render",
|
||||
@@ -1434,8 +1359,6 @@
|
||||
"ui.reset_the_settings_on_this_page": "Reset the settings on this page",
|
||||
"ui.restore": "Restore",
|
||||
"ui.restore_defaults": "Restore Defaults",
|
||||
"ui.reverse_horizontal_pan_direction": "Reverse horizontal pan direction",
|
||||
"ui.reverse_horizontal_pan_tip": "Horizontal two-finger scrolling and the thumb wheel pan the other way",
|
||||
"ui.reverse_zoom_direction": "Reverse zoom direction",
|
||||
"ui.revolve": "Revolve",
|
||||
"ui.right": "Right",
|
||||
@@ -1446,8 +1369,6 @@
|
||||
"ui.save_mesh_pick_bodies": "Select the bodies to save",
|
||||
"ui.save_mesh_temp_failed": "Couldn't make {folder}: {error}",
|
||||
"ui.saved_to": "Saved {path}",
|
||||
"ui.sbox_pin": "Pin to the top of the S box",
|
||||
"ui.sbox_unpin": "Unpin",
|
||||
"ui.scale": "Scale",
|
||||
"ui.second_rail": "Second Rail",
|
||||
"ui.section_analysis": "Section Analysis",
|
||||
@@ -1459,7 +1380,6 @@
|
||||
"ui.send": "Send",
|
||||
"ui.send_to_slicer": "Send to slicer",
|
||||
"ui.sent_to_slicer": "Sent to {slicer}",
|
||||
"ui.sheet_gap": "Gap",
|
||||
"ui.shell": "Shell",
|
||||
"ui.show": "Show",
|
||||
"ui.showAll": "Show All",
|
||||
@@ -1520,8 +1440,6 @@
|
||||
"ui.theme": "Theme",
|
||||
"ui.thickness": "Thickness",
|
||||
"ui.thread": "Thread",
|
||||
"ui.tie_ends": "Tie",
|
||||
"ui.tie_ends_tip": "Constrain all untied ends",
|
||||
"ui.timeline_bottom": "Bottom timeline",
|
||||
"ui.timeline_left_note": "The left layout shows the rollback bar above the parts and scene browser.",
|
||||
"ui.timeline_left_sidebar": "Left sidebar",
|
||||
@@ -1544,7 +1462,6 @@
|
||||
"ui.units_of_new_designs": "Units of new designs",
|
||||
"ui.unlock": "Unlock",
|
||||
"ui.unsuppress": "Unsuppress",
|
||||
"ui.untied_ends_tip": "Touches by position only",
|
||||
"ui.untitled": "Untitled",
|
||||
"ui.up_axis_note": "Which axis points up in the views (axis view, Home, Look At). Designs are unchanged.",
|
||||
"ui.updateToCurrentView": "Update to Current View",
|
||||
|
||||
+15
-81
@@ -7,7 +7,6 @@
|
||||
"A new configuration, a copy of the active one": "Una configuración nueva, copia de la activa",
|
||||
"AI": "AI",
|
||||
"About SolidCraft": "Acerca de SolidCraft",
|
||||
"Acceleration": "Aceleración",
|
||||
"Active": "Activo",
|
||||
"Active rule": "Regla activa",
|
||||
"Add": "Añadir",
|
||||
@@ -15,10 +14,8 @@
|
||||
"Add Mid Radius": "Añadir radio intermedio",
|
||||
"Add Row": "Añadir fila",
|
||||
"Add condition": "Añadir condición",
|
||||
"Against": "Contra",
|
||||
"Alignment": "Alineación",
|
||||
"All Sketch Points": "Todos los puntos del boceto",
|
||||
"All components": "Todos los componentes",
|
||||
"All open designs": "Todos los diseños abiertos",
|
||||
"Allowed expressions, separated by semicolons": "Expresiones permitidas, separadas por punto y coma",
|
||||
"Also deletes": "También elimina",
|
||||
@@ -60,7 +57,6 @@
|
||||
"Configuration rules": "Reglas de configuración",
|
||||
"Construction": "Construcción",
|
||||
"Contact": "Contacto",
|
||||
"Contact sets": "Conjuntos de contacto",
|
||||
"Continuity": "Continuidad",
|
||||
"Copy Error Message": "Copiar mensaje de error",
|
||||
"Copy Warning Message": "Copiar mensaje de advertencia",
|
||||
@@ -86,21 +82,16 @@
|
||||
"Density": "Densidad",
|
||||
"Designs you open or save appear here.": "Aquí aparecen los diseños que abras o guardes.",
|
||||
"Determined by To Object. Choose Angle to edit the angular extent.": "Determinado por «Hasta objeto». Elige «Ángulo» para modificar la extensión angular.",
|
||||
"Direction": "Dirección",
|
||||
"Discard": "Descartar",
|
||||
"Do not export hidden edges": "No exportar aristas ocultas",
|
||||
"Don't ask again": "No volver a preguntar",
|
||||
"Double-click to rename": "Doble clic para cambiar el nombre",
|
||||
"Double-clicking the middle button in the view: Home (the home view, framing what is shown), Fit (frame what is shown, keeping the orientation) or nothing": "Doble clic con el botón central en la vista: Inicio (vista inicial que encuadra lo visible), Ajustar (encuadrar sin cambiar la orientación) o nada",
|
||||
"Drive Joint": "Accionar unión",
|
||||
"Duration": "Duración",
|
||||
"Edges": "Aristas",
|
||||
"Edit Profile Sketch": "Editar boceto del perfil",
|
||||
"Enable rule": "Activar regla",
|
||||
"Enabled": "Activado",
|
||||
"Explode": "Explosionar",
|
||||
"Explode Text": "Descomponer texto",
|
||||
"Export Animation…": "Exportar animación…",
|
||||
"Export Current View as DXF": "Exportar vista actual como DXF",
|
||||
"Export Current View as DXF…": "Exportar vista actual como DXF…",
|
||||
"Export DXF…": "Exportar DXF…",
|
||||
@@ -120,6 +111,7 @@
|
||||
"Follow the camera (Look At sketches)": "Seguir la cámara (orientar hacia los bocetos)",
|
||||
"Font": "Fuente",
|
||||
"Full Length": "Longitud total",
|
||||
"Gap": "Separación",
|
||||
"Grid": "Cuadrícula",
|
||||
"Height": "Altura",
|
||||
"Hidden edges": "Aristas ocultas",
|
||||
@@ -128,7 +120,6 @@
|
||||
"Home view": "Vista de inicio",
|
||||
"In This Design": "En este diseño",
|
||||
"In place (surfaces)": "En su posición (superficies)",
|
||||
"In plane": "En el plano",
|
||||
"Inside": "Interior",
|
||||
"Interferes": "Interfiere",
|
||||
"Issue title": "Título de la incidencia",
|
||||
@@ -158,13 +149,10 @@
|
||||
"Move earlier": "Mover antes",
|
||||
"Move later": "Mover después",
|
||||
"Name": "Nombre",
|
||||
"Nearest": "Más cercano",
|
||||
"New study": "Nuevo estudio",
|
||||
"No crash reports.": "No hay informes de fallos.",
|
||||
"No matching appearances": "No hay apariencias coincidentes",
|
||||
"No visible bodies to export.": "No hay cuerpos visibles para exportar.",
|
||||
"None": "Ninguno",
|
||||
"Normal to sketch": "Normal al boceto",
|
||||
"Not available yet": "Todavía no disponible",
|
||||
"Notch": "Muesca",
|
||||
"Opacity": "Opacidad",
|
||||
@@ -179,20 +167,16 @@
|
||||
"Per Edge": "Por arista",
|
||||
"Pick the tool bodies": "Selecciona los cuerpos herramienta",
|
||||
"Picked": "Seleccionado",
|
||||
"Picked point": "Punto seleccionado",
|
||||
"Place at the origin instead": "Colocar en el origen",
|
||||
"Playback": "Reproducción",
|
||||
"Plot": "Graficar",
|
||||
"Position": "Posición",
|
||||
"Position (0 to 1)": "Posición (de 0 a 1)",
|
||||
"Position (G0)": "Posición (G0)",
|
||||
"Position along the edge, 0 at the start end, 1 at the other": "Posición a lo largo de la arista: 0 en el extremo inicial y 1 en el otro",
|
||||
"Preferences › Design turns the offer back on": "Preferencias › Diseño vuelve a activar la oferta",
|
||||
"Preview Row": "Previsualizar fila",
|
||||
"Preview leaves the current design unchanged.": "La vista previa no modifica el diseño actual.",
|
||||
"Profiles/Curves": "Perfiles/curvas",
|
||||
"Projection": "Proyección",
|
||||
"Quantity": "Cantidad",
|
||||
"RECENT": "RECIENTES",
|
||||
"RULED SURFACE": "SUPERFICIE REGLADA",
|
||||
"Radius Fit": "Ajuste de radio",
|
||||
@@ -223,11 +207,10 @@
|
||||
"Revert Position": "Revertir posición",
|
||||
"Review the report and screenshot before opening GitHub.": "Revisa el informe y la captura antes de abrir GitHub.",
|
||||
"Rim Edge": "Arista del borde",
|
||||
"Roll History Marker Here": "Desplazar marcador de la línea de tiempo hasta aquí",
|
||||
"Roll History Marker Here": "Desplazar marcador del historial hasta aquí",
|
||||
"Round": "Redondo",
|
||||
"Rule": "Regla",
|
||||
"Save Rule": "Guardar regla",
|
||||
"Save the study as an animated GIF, seen as the viewport shows it": "Guardar el estudio como GIF animado, tal como se ve en el visor",
|
||||
"Saved preview. Geometry is being checked before editing.": "Vista previa guardada. Se está comprobando la geometría antes de editarla.",
|
||||
"Scale factor": "Factor de escala",
|
||||
"Search": "Buscar",
|
||||
@@ -258,7 +241,6 @@
|
||||
"Stand this part up?": "¿Poner esta pieza en vertical?",
|
||||
"Step": "Paso",
|
||||
"Steps": "Pasos",
|
||||
"Study": "Estudio",
|
||||
"Style": "Estilo",
|
||||
"Suppressed": "Suprimido",
|
||||
"Swap Ends": "Intercambiar extremos",
|
||||
@@ -278,7 +260,6 @@
|
||||
"This face cannot provide a durable attachment. Choose another face or explicitly use a fixed plane.": "Esta cara no permite una vinculación persistente. Elige otra cara o utiliza expresamente un plano fijo.",
|
||||
"To Offset": "Hasta desfase",
|
||||
"Tolerance": "Tolerancia",
|
||||
"Too close": "Demasiado cerca",
|
||||
"Turn off section analysis before exporting the view.": "Desactiva el análisis de sección antes de exportar la vista.",
|
||||
"Type": "Tipo",
|
||||
"UNTRIM": "ANULAR RECORTE",
|
||||
@@ -289,7 +270,6 @@
|
||||
"Use selected face as fixed plane": "Usar la cara seleccionada como plano fijo",
|
||||
"VIEW": "VISTA",
|
||||
"Value": "Valor",
|
||||
"Velocity": "Velocidad",
|
||||
"Vertices": "Vértices",
|
||||
"View navigator": "Navegador de vistas",
|
||||
"Visible bodies only. Curves use line segments (0.01 mm tolerance).": "Solo cuerpos visibles. Las curvas utilizan segmentos rectos (tolerancia de 0,01 mm).",
|
||||
@@ -313,13 +293,6 @@
|
||||
"browser.precision": "Precisión: {precision}",
|
||||
"browser.sketch_name": "Boceto{number}",
|
||||
"browser.units": "Unidades: {units}",
|
||||
"cam.flat": "Cara plana",
|
||||
"cam.follower": "Seguidor",
|
||||
"cam.knife": "Arista de cuchilla",
|
||||
"cam.needs_joints": "necesita una unión que gire y otra que deslice o bascule",
|
||||
"cam.roller": "Rodillo",
|
||||
"cam.roller_pick": "Rodillo (opcional)",
|
||||
"cam.tip_pick": "Punta (opcional)",
|
||||
"cmd.appearance_assign": "Apariencia",
|
||||
"cmd.appearance_library": "Biblioteca de apariencias",
|
||||
"cmd.appearance_list": "Listar apariencias",
|
||||
@@ -412,7 +385,6 @@
|
||||
"cmd.file_slicers": "Laminadores instalados",
|
||||
"cmd.inspect_accessibility": "Análisis de accesibilidad",
|
||||
"cmd.inspect_center_of_mass": "Centro de masas",
|
||||
"cmd.inspect_clearance": "Holgura",
|
||||
"cmd.inspect_compare": "Comparar cuerpos",
|
||||
"cmd.inspect_continuity": "Análisis de continuidad",
|
||||
"cmd.inspect_curvature_comb": "Análisis de peine de curvatura",
|
||||
@@ -422,10 +394,8 @@
|
||||
"cmd.inspect_interference": "Interferencia",
|
||||
"cmd.inspect_isocurve": "Análisis de isocurvas",
|
||||
"cmd.inspect_minimum_radius": "Análisis de radio mínimo",
|
||||
"cmd.inspect_silhouette": "Silueta",
|
||||
"cmd.inspect_zebra": "Análisis de franjas cebra",
|
||||
"cmd.joint_as_built": "Unión según posición actual",
|
||||
"cmd.joint_cam": "Leva",
|
||||
"cmd.joint_create": "Unión",
|
||||
"cmd.joint_delete": "Eliminar unión",
|
||||
"cmd.joint_drive": "Accionar uniones",
|
||||
@@ -456,11 +426,8 @@
|
||||
"cmd.model_threads": "Listar roscas",
|
||||
"cmd.motion_delete": "Eliminar estudio de movimiento",
|
||||
"cmd.motion_export": "Exportar estudio de movimiento",
|
||||
"cmd.motion_export_animation": "Exportar animación del movimiento",
|
||||
"cmd.motion_interference": "Interferencia de movimiento",
|
||||
"cmd.motion_list": "Listar estudios de movimiento",
|
||||
"cmd.motion_play": "Reproducir estudio de movimiento",
|
||||
"cmd.motion_plot": "Graficar estudio de movimiento",
|
||||
"cmd.motion_study": "Estudio de movimiento",
|
||||
"cmd.motion_trace": "Trayectoria de movimiento",
|
||||
"cmd.motion_trace_keep": "Conservar trayectoria de movimiento",
|
||||
@@ -486,7 +453,7 @@
|
||||
"cmd.parts_library": "Piezas normalizadas",
|
||||
"cmd.parts_resize": "Cambiar tamaño de pieza",
|
||||
"cmd.plastic_assign_rule": "Asignar regla de plástico",
|
||||
"cmd.plastic_boss": "Saliente",
|
||||
"cmd.plastic_boss": "Resalte",
|
||||
"cmd.plastic_lip": "Labio",
|
||||
"cmd.plastic_manage_rules": "Gestionar reglas de plástico",
|
||||
"cmd.plastic_rest": "Apoyo",
|
||||
@@ -578,7 +545,7 @@
|
||||
"cmd.sketch_slot_center_point": "Ranura por punto central",
|
||||
"cmd.sketch_slot_center_to_center": "Ranura entre centros",
|
||||
"cmd.sketch_slot_overall": "Ranura por longitud total",
|
||||
"cmd.sketch_snap": "Referencia a objetos",
|
||||
"cmd.sketch_snap": "Detección de referencias",
|
||||
"cmd.sketch_solve": "Resolver boceto",
|
||||
"cmd.sketch_spline3d": "Spline 3D",
|
||||
"cmd.sketch_spline_control_point": "Spline por puntos de control",
|
||||
@@ -603,7 +570,7 @@
|
||||
"cmd.surface_extrude": "Extruir superficie",
|
||||
"cmd.surface_fair": "Suavizar superficie",
|
||||
"cmd.surface_fillet": "Redondeo de superficie",
|
||||
"cmd.surface_loft": "Solevación de superficies",
|
||||
"cmd.surface_loft": "Transición de superficies",
|
||||
"cmd.surface_match": "Igualar superficie",
|
||||
"cmd.surface_offset": "Desfasar superficie",
|
||||
"cmd.surface_patch": "Parche",
|
||||
@@ -625,7 +592,7 @@
|
||||
"cmd.timeline_redefine": "Editar operación",
|
||||
"cmd.timeline_rename": "Cambiar nombre",
|
||||
"cmd.timeline_reorder": "Reordenar operación",
|
||||
"cmd.timeline_roll_to": "Desplazar marcador de la línea de tiempo",
|
||||
"cmd.timeline_roll_to": "Desplazar marcador del historial",
|
||||
"cmd.timeline_rollback": "Mover marcador de la línea de tiempo",
|
||||
"cmd.timeline_suppress": "Suprimir operación",
|
||||
"cmd.view_apply": "Restaurar vista guardada",
|
||||
@@ -664,18 +631,6 @@
|
||||
"drive.stopped_by_contact": "Detenido en {value}: {a} atravesaría {b}",
|
||||
"drive.stopped_by_loop": "Detenido en {value}: el bucle que pasa por {joint} no puede cerrarse más",
|
||||
"drive.unconnected": "No mueve nada: {part} no está conectado a ningún elemento fijado",
|
||||
"dxf.cancel_wait": "Esperando a que se detenga la exportación…",
|
||||
"dxf.cancelled": "Exportación DXF cancelada",
|
||||
"dxf.cancelling": "Cancelando la exportación DXF",
|
||||
"dxf.exporting": "Exportando DXF",
|
||||
"dxf.invalid_tolerance": "La tolerancia de curva debe estar entre 0,0001 y 1 mm.",
|
||||
"dxf.progress.index": "Preparando superficies (2/5)",
|
||||
"dxf.progress.meshes": "Preparando mallas (1/5)",
|
||||
"dxf.progress.occlusion": "Resolviendo líneas ocultas (4/5)",
|
||||
"dxf.progress.projection": "Proyectando geometría (3/5)",
|
||||
"dxf.progress.write": "Escribiendo DXF (5/5)",
|
||||
"dxf.tolerance": "Tolerancia de curva",
|
||||
"dxf.tolerance_help": "Solo cuerpos visibles. Las curvas usan segmentos rectos. Una tolerancia menor da curvas más finas y consume más tiempo y memoria.",
|
||||
"expression, e.g. 25 mm": "expresión, p. ej., 25 mm",
|
||||
"fits the face, e.g. M8": "se ajusta a la cara, p. ej., M8",
|
||||
"from the rule": "de la regla",
|
||||
@@ -707,10 +662,6 @@
|
||||
"joint.from_bodies_hint": "Cada cuerpo seleccionado pasa a un componente nuevo propio y conserva su posición.",
|
||||
"joint.one_body": "Las dos selecciones pertenecen a {body}. Selecciona la segunda en otra pieza.",
|
||||
"joint.one_component": "Estas piezas son cuerpos de un mismo componente: {bodies}. Una unión mueve un componente respecto a otro, por lo que cada pieza debe tener su propio componente.",
|
||||
"motion.checked_to": "Comprobado solo hasta el paso {step}: la comprobación agotó su presupuesto de trabajo",
|
||||
"motion.collision": "{a} · {b}: pasos {steps}",
|
||||
"motion.duration_hint": "segundos (vacío: por paso)",
|
||||
"motion.no_collisions": "Sin colisiones durante el estudio",
|
||||
"nav.touchpad.help": "Mantén pulsado Mayús y mueve el puntero para encuadrar; mantén Alt/Opción y muévelo para orbitar. No hace falta pulsar ningún botón del ratón. El desplazamiento vertical hace zoom.",
|
||||
"nav.touchpad.label": "Panel táctil",
|
||||
"nav.touchpad.orbit": "Orbitar (mantén Alt/Opción y mueve el puntero; sin pulsar botones)",
|
||||
@@ -773,17 +724,17 @@
|
||||
"render.use_preview": "Usar vista previa integrada",
|
||||
"sample.bracket.about": "Brida base con pestañas laterales y reborde, según una regla de 2 mm",
|
||||
"sample.bracket.name": "Soporte de chapa",
|
||||
"sample.enclosure.about": "Caja vaciada con labio, salientes para tornillos, apoyo y encaje a presión",
|
||||
"sample.enclosure.about": "Caja vaciada con labio, alojamientos para tornillos, apoyo y cierre a presión",
|
||||
"sample.enclosure.name": "Carcasa de plástico",
|
||||
"sample.flange.about": "Cubo y brida con patrón circular controlado por un parámetro",
|
||||
"sample.flange.name": "Brida atornillada",
|
||||
"sample.hinge.about": "Dos componentes y una unión de revolución accionada hasta 60°",
|
||||
"sample.hinge.name": "Conjunto de bisagra",
|
||||
"sample.plate.about": "Placa paramétrica con redondeos, agujeros y un saliente perforado",
|
||||
"sample.plate.about": "Placa paramétrica con redondeos, agujeros y un resalte perforado",
|
||||
"sample.plate.name": "Placa de ejemplo",
|
||||
"sample.vents.about": "Ranuras en un patrón rectangular: las filas, las columnas y el paso son parámetros",
|
||||
"sample.vents.name": "Placa de ventilación",
|
||||
"settings.display_quality_note": "Los niveles más finos muestran las caras y aristas curvas con mayor suavidad, pero tardan más en generar la malla. Solo cambia el visor, nunca el diseño ni las exportaciones.",
|
||||
"settings.display_quality_note": "Los niveles más finos muestran las caras y aristas curvas con mayor suavidad, pero tardan más en generar la malla. Solo cambia la vista, nunca el diseño ni las exportaciones.",
|
||||
"settings.frame_rate_note": "Muestra los fotogramas por segundo en la esquina inferior derecha de la vista. Mientras esté activado, la vista se redibuja continuamente.",
|
||||
"settings.measure_axis_distances": "Mostrar distancias X/Y/Z",
|
||||
"settings.measure_axis_distances_help": "Mostrar las cotas de cada eje global junto a la distancia total mientras está abierta la herramienta Medir.",
|
||||
@@ -809,10 +760,6 @@
|
||||
"sketch_text.path_hint": "Curva del boceto que seguirá el texto: haz clic aquí y después en la curva",
|
||||
"sketch_text.use_font_file": "Usar el archivo de fuente {path}",
|
||||
"the parts library is not available": "la biblioteca de piezas no está disponible",
|
||||
"thread.automatic_size": "Tamaño automático",
|
||||
"thread.designation": "Designación",
|
||||
"thread.iso_metric": "Métrica ISO",
|
||||
"thread.select_size": "Seleccionar tamaño",
|
||||
"timeline.group": "Nueva carpeta de operaciones",
|
||||
"timeline.move_many": "Mover elementos de la línea de tiempo",
|
||||
"timeline.move_to_group": "Mover operaciones a la carpeta",
|
||||
@@ -1058,10 +1005,6 @@
|
||||
"ui.construct": "Construir",
|
||||
"ui.construction": "Construcción",
|
||||
"ui.context_menu": "Menú contextual",
|
||||
"ui.continuity_g0": "G0 arista",
|
||||
"ui.continuity_g1": "G1 tangencia",
|
||||
"ui.continuity_g2": "G2 curvatura",
|
||||
"ui.continuity_gap": "Separación",
|
||||
"ui.copy": "Copiar",
|
||||
"ui.corner": "Esquina",
|
||||
"ui.corners": "Esquinas",
|
||||
@@ -1123,7 +1066,7 @@
|
||||
"ui.equivalent_radius": "Radio equivalente",
|
||||
"ui.expand": "Expandir",
|
||||
"ui.exportDxf": "Exportar DXF…",
|
||||
"ui.export_ellipsis": "Exportar…",
|
||||
"ui.export_ellipsis": "Exportar...",
|
||||
"ui.export_ellipsis_keymap": "Exportar…",
|
||||
"ui.export_keymap_tip": "Exportar las teclas, los favoritos de la búsqueda S y la barra de herramientas como archivo",
|
||||
"ui.extend": "Extender",
|
||||
@@ -1149,8 +1092,6 @@
|
||||
"ui.fully_constrained": "Totalmente restringido",
|
||||
"ui.general": "General",
|
||||
"ui.graphics": "Gráficos",
|
||||
"ui.grid_follows_view": "La cuadrícula sigue a la vista",
|
||||
"ui.grid_follows_view_hint": "Las vistas laterales muestran la cuadrícula en el plano vertical que tienen enfrente, en lugar del suelo",
|
||||
"ui.ground_shadow": "Sombra en el suelo",
|
||||
"ui.groupSelected": "Agrupar selección",
|
||||
"ui.heading_add_ins": "COMPLEMENTOS",
|
||||
@@ -1177,7 +1118,7 @@
|
||||
"ui.heading_home": "INICIO",
|
||||
"ui.heading_insert": "INSERTAR",
|
||||
"ui.heading_inspect": "INSPECCIONAR",
|
||||
"ui.heading_loft": "SOLEVACIÓN",
|
||||
"ui.heading_loft": "TRANSICIÓN",
|
||||
"ui.heading_make": "FABRICAR",
|
||||
"ui.heading_measure": "MEDIR",
|
||||
"ui.heading_mesh": "MALLA",
|
||||
@@ -1247,7 +1188,7 @@
|
||||
"ui.line": "Línea",
|
||||
"ui.linetype": "Tipo de línea",
|
||||
"ui.lock": "Bloquear",
|
||||
"ui.loft": "Solevación",
|
||||
"ui.loft": "Transición",
|
||||
"ui.look_at": "Mirar hacia",
|
||||
"ui.look_at_anim_desc": "Vista de eje, Mirar hacia, Inicio y Ajustar",
|
||||
"ui.manufacture": "Fabricación",
|
||||
@@ -1300,7 +1241,7 @@
|
||||
"ui.offset_plane": "Plano desfasado",
|
||||
"ui.one_side": "Un lado",
|
||||
"ui.open": "Abrir",
|
||||
"ui.open_ellipsis": "Abrir…",
|
||||
"ui.open_ellipsis": "Abrir...",
|
||||
"ui.open_preferences": "Abrir preferencias",
|
||||
"ui.open_recent": "Abrir reciente",
|
||||
"ui.operation": "Operación",
|
||||
@@ -1345,8 +1286,6 @@
|
||||
"ui.radial_context_menu": "Menú contextual radial",
|
||||
"ui.radius": "Radio",
|
||||
"ui.radius_type": "Tipo de radio",
|
||||
"ui.rail": "Línea guía",
|
||||
"ui.rail_fit": "Ajuste a la guía",
|
||||
"ui.reachable": "Alcanzable",
|
||||
"ui.rectangle": "Rectángulo",
|
||||
"ui.rectangular_pattern": "Patrón rectangular",
|
||||
@@ -1367,15 +1306,12 @@
|
||||
"ui.right": "Derecha",
|
||||
"ui.save": "Guardar",
|
||||
"ui.save_as": "Guardar como",
|
||||
"ui.save_as_ellipsis": "Guardar como…",
|
||||
"ui.save_as_ellipsis": "Guardar como...",
|
||||
"ui.save_mesh_no_slicer": "No se ha encontrado ningún laminador: elige Personalizado… y selecciona tu laminador, o desactiva Enviar al laminador",
|
||||
"ui.save_mesh_pick_bodies": "Selecciona los cuerpos que se van a guardar",
|
||||
"ui.save_mesh_temp_failed": "No se ha podido crear {folder}: {error}",
|
||||
"ui.saved_to": "Guardado en {path}",
|
||||
"ui.sbox_pin": "Fijar arriba en la búsqueda S",
|
||||
"ui.sbox_unpin": "Desfijar",
|
||||
"ui.scale": "Escala",
|
||||
"ui.second_rail": "Segunda línea guía",
|
||||
"ui.section_analysis": "Análisis de sección",
|
||||
"ui.select": "Seleccionar",
|
||||
"ui.selectOther": "Seleccionar otro…",
|
||||
@@ -1385,7 +1321,6 @@
|
||||
"ui.send": "Enviar",
|
||||
"ui.send_to_slicer": "Enviar al laminador",
|
||||
"ui.sent_to_slicer": "Enviado a {slicer}",
|
||||
"ui.sheet_gap": "Separación",
|
||||
"ui.shell": "Vaciado",
|
||||
"ui.show": "Mostrar",
|
||||
"ui.showAll": "Mostrar todo",
|
||||
@@ -1416,7 +1351,7 @@
|
||||
"ui.smooth": "Suave",
|
||||
"ui.smooth_wheel_zoom": "Zoom suave con la rueda",
|
||||
"ui.smooth_wheel_zoom_tip": "Desactívalo para aplicar cada movimiento de la rueda inmediatamente, sin suavizado posterior",
|
||||
"ui.snap": "Referencia a objetos",
|
||||
"ui.snap": "Ajustar a referencias",
|
||||
"ui.space_mouse_hint": "Desplaza y gira la vista a la vez; los bocetos solo permiten desplazamiento y zoom.",
|
||||
"ui.space_mouse_navigate": "Navegar con SpaceNavigator / SpaceMouse",
|
||||
"ui.spacing": "Espaciado",
|
||||
@@ -1427,7 +1362,6 @@
|
||||
"ui.stand_up_tip": "Tras abrir un archivo STEP o IGES con Z hacia arriba, ofrecer un cuarto de vuelta alrededor de X",
|
||||
"ui.start": "Iniciar",
|
||||
"ui.start_radius": "Radio inicial",
|
||||
"ui.stretch": "Estirar",
|
||||
"ui.supersampling": "Supermuestreo (SSAA)",
|
||||
"ui.supersampling_adaptive": "Solo cuando la vista está inmóvil",
|
||||
"ui.supersampling_off": "Desactivado (1×)",
|
||||
|
||||
+1
-33
@@ -86,7 +86,6 @@
|
||||
"Density": "Masse volumique",
|
||||
"Designs you open or save appear here.": "Les conceptions que vous ouvrez ou enregistrez apparaissent ici.",
|
||||
"Determined by To Object. Choose Angle to edit the angular extent.": "Déterminé par Jusqu'à l'objet. Choisir Angle pour modifier l'étendue angulaire.",
|
||||
"Direction": "Direction",
|
||||
"Discard": "Abandonner",
|
||||
"Do not export hidden edges": "Ne pas exporter les arêtes cachées",
|
||||
"Don't ask again": "Ne plus demander",
|
||||
@@ -99,7 +98,6 @@
|
||||
"Enable rule": "Activer la règle",
|
||||
"Enabled": "Activé",
|
||||
"Explode": "Éclater",
|
||||
"Explode Text": "Décomposer le texte",
|
||||
"Export Animation…": "Exporter l'animation…",
|
||||
"Export Current View as DXF": "Exporter la vue actuelle en DXF",
|
||||
"Export Current View as DXF…": "Exporter la vue actuelle en DXF…",
|
||||
@@ -120,6 +118,7 @@
|
||||
"Follow the camera (Look At sketches)": "Suivre la caméra (vue normale aux esquisses)",
|
||||
"Font": "Police",
|
||||
"Full Length": "Longueur totale",
|
||||
"Gap": "Jeu",
|
||||
"Grid": "Grille",
|
||||
"Height": "Hauteur",
|
||||
"Hidden edges": "Arêtes cachées",
|
||||
@@ -128,7 +127,6 @@
|
||||
"Home view": "Vue d'accueil",
|
||||
"In This Design": "Dans cette conception",
|
||||
"In place (surfaces)": "En place (surfaces)",
|
||||
"In plane": "Dans le plan",
|
||||
"Inside": "Intérieur",
|
||||
"Interferes": "Interfère",
|
||||
"Issue title": "Titre du ticket",
|
||||
@@ -158,13 +156,11 @@
|
||||
"Move earlier": "Déplacer plus tôt",
|
||||
"Move later": "Déplacer plus tard",
|
||||
"Name": "Nom",
|
||||
"Nearest": "Corps le plus proche",
|
||||
"New study": "Nouvelle étude",
|
||||
"No crash reports.": "Aucun rapport de plantage.",
|
||||
"No matching appearances": "Aucune apparence correspondante",
|
||||
"No visible bodies to export.": "Aucun corps visible à exporter.",
|
||||
"None": "Aucun",
|
||||
"Normal to sketch": "Normale à l'esquisse",
|
||||
"Not available yet": "Pas encore disponible",
|
||||
"Notch": "Encoche",
|
||||
"Opacity": "Opacité",
|
||||
@@ -278,7 +274,6 @@
|
||||
"This face cannot provide a durable attachment. Choose another face or explicitly use a fixed plane.": "Cette face ne peut pas fournir de rattachement durable. Choisissez une autre face ou utilisez explicitement un plan fixe.",
|
||||
"To Offset": "Jusqu'au décalage",
|
||||
"Tolerance": "Tolérance",
|
||||
"Too close": "Trop proches",
|
||||
"Turn off section analysis before exporting the view.": "Désactivez l'analyse de coupe avant d'exporter la vue.",
|
||||
"Type": "Type",
|
||||
"UNTRIM": "ANNULER L'AJUSTEMENT",
|
||||
@@ -412,7 +407,6 @@
|
||||
"cmd.file_slicers": "Slicers installés",
|
||||
"cmd.inspect_accessibility": "Analyse d'accessibilité",
|
||||
"cmd.inspect_center_of_mass": "Centre de gravité",
|
||||
"cmd.inspect_clearance": "Jeu",
|
||||
"cmd.inspect_compare": "Comparer les corps",
|
||||
"cmd.inspect_continuity": "Analyse de continuité",
|
||||
"cmd.inspect_curvature_comb": "Analyse par peigne de courbure",
|
||||
@@ -422,7 +416,6 @@
|
||||
"cmd.inspect_interference": "Interférence",
|
||||
"cmd.inspect_isocurve": "Analyse des isocourbes",
|
||||
"cmd.inspect_minimum_radius": "Analyse du rayon minimal",
|
||||
"cmd.inspect_silhouette": "Silhouette",
|
||||
"cmd.inspect_zebra": "Analyse zébrée",
|
||||
"cmd.joint_as_built": "Liaison telle que construite",
|
||||
"cmd.joint_cam": "Came",
|
||||
@@ -628,10 +621,8 @@
|
||||
"cmd.timeline_roll_to": "Déplacer le marqueur d'historique",
|
||||
"cmd.timeline_rollback": "Déplacer le marqueur de chronologie",
|
||||
"cmd.timeline_suppress": "Désactiver la fonction",
|
||||
"cmd.view_ambient_occlusion": "Occlusion ambiante",
|
||||
"cmd.view_apply": "Restaurer la vue nommée",
|
||||
"cmd.view_delete": "Supprimer la vue nommée",
|
||||
"cmd.view_graphics_preset": "Préréglage graphique",
|
||||
"cmd.view_list": "Lister les vues nommées",
|
||||
"cmd.view_navigate": "Naviguer dans la vue",
|
||||
"cmd.view_rename": "Renommer la vue nommée",
|
||||
@@ -653,12 +644,7 @@
|
||||
"coupling.rack": "Crémaillère",
|
||||
"coupling.radius_1": "Rayon 1",
|
||||
"coupling.radius_2": "Rayon 2",
|
||||
"dialog.bodies_count": "{count} corps",
|
||||
"dialog.clearance_invalid": "le jeu doit être une longueur positive ou nulle",
|
||||
"dialog.compute": "Calculer",
|
||||
"dialog.compute_again": "Recalculer",
|
||||
"dialog.computing": "Calcul… {seconds} s",
|
||||
"dialog.could_not_be_checked": "non vérifiable",
|
||||
"dialog.custom_axis": "↖ Axe personnalisé",
|
||||
"dialog.edit_title": "MODIFIER {title}",
|
||||
"dialog.select": "↖ Sélectionner",
|
||||
@@ -997,8 +983,6 @@
|
||||
"ui.ai_toolbar_button_note": "Le bouton IA à l'extrémité droite de la barre d'outils. Ctrl+Shift+A et le menu Vue ouvrent toujours le panneau.",
|
||||
"ui.ai_undo_tip": "Annuler tout ce que l'agent a modifié pour ce message, en une seule étape",
|
||||
"ui.all": "Tout",
|
||||
"ui.ambient_occlusion": "Occlusion ambiante",
|
||||
"ui.ambient_occlusion_help": "Ombrage doux aux jonctions des surfaces opaques. Nécessite la vue GPU.",
|
||||
"ui.analysis": "Analyse",
|
||||
"ui.angle": "Angle",
|
||||
"ui.angle_decimals": "Décimales des angles",
|
||||
@@ -1016,7 +1000,6 @@
|
||||
"ui.base_point": "Point de base",
|
||||
"ui.blocked": "Inaccessible",
|
||||
"ui.bodies": "Corps",
|
||||
"ui.bodies_all_if_none": "Corps (tous si aucun)",
|
||||
"ui.box": "Boîte",
|
||||
"ui.break": "Fractionner",
|
||||
"ui.calibrate": "Étalonner",
|
||||
@@ -1068,10 +1051,6 @@
|
||||
"ui.construct": "Construire",
|
||||
"ui.construction": "Construction",
|
||||
"ui.context_menu": "Menu contextuel",
|
||||
"ui.continuity_g0": "G0 angle vif",
|
||||
"ui.continuity_g1": "G1 tangence",
|
||||
"ui.continuity_g2": "G2 courbure",
|
||||
"ui.continuity_gap": "Écart",
|
||||
"ui.copy": "Copier",
|
||||
"ui.corner": "Angle",
|
||||
"ui.corners": "Angles",
|
||||
@@ -1159,13 +1138,6 @@
|
||||
"ui.fully_constrained": "Entièrement contrainte",
|
||||
"ui.general": "Général",
|
||||
"ui.graphics": "Graphisme",
|
||||
"ui.graphics_custom": "Personnalisé",
|
||||
"ui.graphics_performance": "Performances",
|
||||
"ui.graphics_preset": "Préréglage graphique",
|
||||
"ui.graphics_preset_help": "Qualité : occlusion ambiante, tessellation fine et suréchantillonnage adaptatif 2×. Performances : sans occlusion ambiante, tessellation normale et sans suréchantillonnage. Les réglages manuels affichent Personnalisé. MSAA reste inchangé.",
|
||||
"ui.graphics_quality": "Qualité",
|
||||
"ui.grid_follows_view": "La grille suit la vue",
|
||||
"ui.grid_follows_view_hint": "Dans les vues de côté, la grille s'affiche sur le plan vertical qui leur fait face plutôt que sur le sol",
|
||||
"ui.ground_shadow": "Ombre au sol",
|
||||
"ui.groupSelected": "Grouper la sélection",
|
||||
"ui.heading_add_ins": "COMPLÉMENTS",
|
||||
@@ -1192,7 +1164,6 @@
|
||||
"ui.heading_home": "ACCUEIL",
|
||||
"ui.heading_insert": "INSÉRER",
|
||||
"ui.heading_inspect": "INSPECTER",
|
||||
"ui.heading_interference": "INTERFÉRENCE",
|
||||
"ui.heading_loft": "LISSAGE",
|
||||
"ui.heading_make": "FABRIQUER",
|
||||
"ui.heading_measure": "MESURER",
|
||||
@@ -1388,8 +1359,6 @@
|
||||
"ui.save_mesh_pick_bodies": "Sélectionner les corps à enregistrer",
|
||||
"ui.save_mesh_temp_failed": "Impossible de créer {folder} : {error}",
|
||||
"ui.saved_to": "Enregistré : {path}",
|
||||
"ui.sbox_pin": "Épingler en haut de la boîte S",
|
||||
"ui.sbox_unpin": "Désépingler",
|
||||
"ui.scale": "Échelle",
|
||||
"ui.second_rail": "Second rail",
|
||||
"ui.section_analysis": "Analyse de coupe",
|
||||
@@ -1401,7 +1370,6 @@
|
||||
"ui.send": "Envoyer",
|
||||
"ui.send_to_slicer": "Envoyer au slicer",
|
||||
"ui.sent_to_slicer": "Envoyé à {slicer}",
|
||||
"ui.sheet_gap": "Jeu",
|
||||
"ui.shell": "Coque",
|
||||
"ui.show": "Afficher",
|
||||
"ui.showAll": "Tout afficher",
|
||||
|
||||
@@ -82,7 +82,6 @@
|
||||
"Density": "Densidade",
|
||||
"Designs you open or save appear here.": "Os deseños que abras ou gardes aparecen aquí.",
|
||||
"Determined by To Object. Choose Angle to edit the angular extent.": "Determinado por Ata o obxecto. Escolle Ángulo para editar a extensión angular.",
|
||||
"Direction": "Dirección",
|
||||
"Discard": "Descartar",
|
||||
"Do not export hidden edges": "Non exportar as arestas ocultas",
|
||||
"Double-click to rename": "Dobre clic para renomear",
|
||||
@@ -113,6 +112,7 @@
|
||||
"Follow the camera (Look At sketches)": "Seguir a cámara (Mirar cara aos bosquexos)",
|
||||
"Font": "Tipo de letra",
|
||||
"Full Length": "Lonxitude completa",
|
||||
"Gap": "Separación",
|
||||
"Grid": "Grella",
|
||||
"Height": "Altura",
|
||||
"Hidden edges": "Arestas ocultas",
|
||||
@@ -121,7 +121,6 @@
|
||||
"Home view": "Vista de inicio",
|
||||
"In This Design": "Neste deseño",
|
||||
"In place (surfaces)": "No sitio (superficies)",
|
||||
"In plane": "No plano",
|
||||
"Inside": "Dentro",
|
||||
"Interferes": "Interfire",
|
||||
"Issue title": "Título da incidencia",
|
||||
@@ -155,7 +154,6 @@
|
||||
"No matching appearances": "Non hai aparencias coincidentes",
|
||||
"No visible bodies to export.": "Non hai corpos visibles para exportar.",
|
||||
"None": "Ningún",
|
||||
"Normal to sketch": "Normal ao bosquexo",
|
||||
"Not available yet": "Aínda non dispoñible",
|
||||
"Notch": "Entalle",
|
||||
"Opacity": "Opacidade",
|
||||
@@ -1030,7 +1028,6 @@
|
||||
"ui.construct": "Construír",
|
||||
"ui.construction": "Construción",
|
||||
"ui.context_menu": "Menú contextual",
|
||||
"ui.continuity_gap": "Separación",
|
||||
"ui.copy": "Copiar",
|
||||
"ui.corner": "Esquina",
|
||||
"ui.corners": "Esquinas",
|
||||
@@ -1337,8 +1334,6 @@
|
||||
"ui.save_mesh_pick_bodies": "Selecciona os corpos que queres gardar",
|
||||
"ui.save_mesh_temp_failed": "Non se puido crear {folder}: {error}",
|
||||
"ui.saved_to": "Gardado {path}",
|
||||
"ui.sbox_pin": "Fixar arriba na caixa S",
|
||||
"ui.sbox_unpin": "Desfixar",
|
||||
"ui.scale": "Escala",
|
||||
"ui.section_analysis": "Análise de sección",
|
||||
"ui.select": "Seleccionar",
|
||||
@@ -1349,7 +1344,6 @@
|
||||
"ui.send": "Enviar",
|
||||
"ui.send_to_slicer": "Enviar ao laminador",
|
||||
"ui.sent_to_slicer": "Enviado a {slicer}",
|
||||
"ui.sheet_gap": "Separación",
|
||||
"ui.shell": "Baleirado",
|
||||
"ui.show": "Mostrar",
|
||||
"ui.showAll": "Mostrar todo",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+1
-86
@@ -31,7 +31,6 @@
|
||||
"Arc length": "円弧の長さ",
|
||||
"Area": "表面積",
|
||||
"Attachment": "アタッチメント",
|
||||
"BOUNDARY SURFACE": "境界サーフェス",
|
||||
"BROWSER": "ブラウザ",
|
||||
"Bend Line Position": "曲げ線位置",
|
||||
"Bend Position": "曲げ位置",
|
||||
@@ -74,7 +73,6 @@
|
||||
"Counterbore depth": "座ぐり深さ",
|
||||
"Countersink angle": "皿穴角度",
|
||||
"Create Issue — review draft": "Issueを作成 — 下書きの確認",
|
||||
"Cross curves": "交差する曲線",
|
||||
"Cube": "キューブ",
|
||||
"Current orthographic view (1:1 mm)": "現在の平行投影(実寸・mm)",
|
||||
"Current perspective view": "現在の透視投影",
|
||||
@@ -88,7 +86,6 @@
|
||||
"Density": "密度",
|
||||
"Designs you open or save appear here.": "開いたデザインや保存したデザインがここに表示されます。",
|
||||
"Determined by To Object. Choose Angle to edit the angular extent.": "「対象オブジェクトまで」により決定されます。角度範囲を編集するには「角度」を選択してください。",
|
||||
"Direction": "方向",
|
||||
"Discard": "破棄",
|
||||
"Do not export hidden edges": "出力しない",
|
||||
"Don't ask again": "今後確認しない",
|
||||
@@ -122,6 +119,7 @@
|
||||
"Follow the camera (Look At sketches)": "カメラ追従(スケッチを正面視)",
|
||||
"Font": "フォント",
|
||||
"Full Length": "全長",
|
||||
"Gap": "ギャップ",
|
||||
"Grid": "グリッド",
|
||||
"Height": "高さ",
|
||||
"Hidden edges": "隠線",
|
||||
@@ -130,7 +128,6 @@
|
||||
"Home view": "ホームビュー",
|
||||
"In This Design": "このデザイン内",
|
||||
"In place (surfaces)": "その場で(サーフェス)",
|
||||
"In plane": "平面内",
|
||||
"Inside": "内側",
|
||||
"Interferes": "干渉あり",
|
||||
"Issue title": "Issueタイトル",
|
||||
@@ -166,7 +163,6 @@
|
||||
"No matching appearances": "一致する外観がありません",
|
||||
"No visible bodies to export.": "出力する表示中のボディがありません。",
|
||||
"None": "なし",
|
||||
"Normal to sketch": "スケッチに垂直",
|
||||
"Not available yet": "まだ利用できません",
|
||||
"Notch": "ノッチ",
|
||||
"Opacity": "不透明度",
|
||||
@@ -350,7 +346,6 @@
|
||||
"cmd.component_move_sketches": "スケッチをコンポーネントに移動",
|
||||
"cmd.component_paste_new": "新規として貼り付け",
|
||||
"cmd.component_properties": "コンポーネントの物理的プロパティ",
|
||||
"cmd.component_relink": "参照元を再リンク",
|
||||
"cmd.component_rename": "コンポーネントの名前を変更",
|
||||
"cmd.component_revert_position": "位置を元に戻す",
|
||||
"cmd.component_unlock": "内容のロックを解除",
|
||||
@@ -560,8 +555,6 @@
|
||||
"cmd.sketch_isoparametric_curve": "アイソパラメトリック曲線",
|
||||
"cmd.sketch_line3d": "3D 線分",
|
||||
"cmd.sketch_line_midpoint": "中点線分",
|
||||
"cmd.sketch_loose_ends": "端点の診断",
|
||||
"cmd.sketch_loose_ends_tie": "未拘束の接点を拘束",
|
||||
"cmd.sketch_move": "スケッチ要素を移動/コピー",
|
||||
"cmd.sketch_move3d": "3D 点を移動",
|
||||
"cmd.sketch_move_point": "スケッチ点をドラッグ",
|
||||
@@ -633,10 +626,8 @@
|
||||
"cmd.timeline_roll_to": "履歴マーカーを移動",
|
||||
"cmd.timeline_rollback": "タイムライン マーカーを移動",
|
||||
"cmd.timeline_suppress": "フィーチャーを抑制",
|
||||
"cmd.view_ambient_occlusion": "アンビエントオクルージョン",
|
||||
"cmd.view_apply": "名前付きビューを復元",
|
||||
"cmd.view_delete": "名前付きビューを削除",
|
||||
"cmd.view_graphics_preset": "グラフィックスプリセット",
|
||||
"cmd.view_list": "名前付きビューを一覧表示",
|
||||
"cmd.view_navigate": "ビューナビゲーション",
|
||||
"cmd.view_rename": "名前付きビューの名前を変更",
|
||||
@@ -658,12 +649,7 @@
|
||||
"coupling.rack": "ラック",
|
||||
"coupling.radius_1": "半径 1",
|
||||
"coupling.radius_2": "半径 2",
|
||||
"dialog.bodies_count": "{count} ボディ",
|
||||
"dialog.clearance_invalid": "クリアランスは 0 以上の長さにしてください",
|
||||
"dialog.compute": "計算",
|
||||
"dialog.compute_again": "再計算",
|
||||
"dialog.computing": "計算中… {seconds} 秒",
|
||||
"dialog.could_not_be_checked": "チェックできません",
|
||||
"dialog.custom_axis": "↖ カスタム軸",
|
||||
"dialog.edit_title": "{title}を編集",
|
||||
"dialog.select": "↖ 選択",
|
||||
@@ -719,15 +705,6 @@
|
||||
"joint.from_bodies_hint": "選択した各ボディを、位置はそのままで専用の新しいコンポーネントに移動します。",
|
||||
"joint.one_body": "両方の選択が {body} 上にあります。2 つ目は別の部品で選択してください。",
|
||||
"joint.one_component": "これらの部品は 1 つのコンポーネントのボディです: {bodies}。ジョイントはコンポーネント同士を動かすため、部品ごとに専用のコンポーネントが必要です。",
|
||||
"messages.ago_h": "{n} 時間前",
|
||||
"messages.ago_min": "{n} 分前",
|
||||
"messages.ago_s": "{n} 秒前",
|
||||
"messages.chip_hint": "クリックするとこのセッションのすべてのメッセージを表示します",
|
||||
"messages.clear": "クリア",
|
||||
"messages.copy_all": "すべてコピー",
|
||||
"messages.menu": "メッセージ…",
|
||||
"messages.none": "まだメッセージはありません。",
|
||||
"messages.title": "メッセージ",
|
||||
"motion.checked_to": "ステップ {step} までしか確認していません: 確認の計算量の上限に達しました",
|
||||
"motion.collision": "{a} · {b}: ステップ {steps}",
|
||||
"motion.duration_hint": "秒 (空欄: ステップごと)",
|
||||
@@ -742,11 +719,9 @@
|
||||
"new configuration": "新規構成",
|
||||
"new name": "新しい名前",
|
||||
"new parameter": "新規パラメータ",
|
||||
"no hole": "穴なし",
|
||||
"no interference": "干渉なし",
|
||||
"no joint that moves yet": "可動ジョイントがまだありません",
|
||||
"no picture": "画像なし",
|
||||
"none": "なし",
|
||||
"pattern.alignment_2": "配置 2",
|
||||
"pattern.alignment_hint": "個数には元の図形が含まれます。中央配置ではコピーを両側に配置し、反転で偶数個の場合に多く配置する側を切り替えます。",
|
||||
"pattern.direction_1": "方向 1",
|
||||
@@ -819,8 +794,6 @@
|
||||
"sketch.degrees_of_freedom": "自由度 {count}",
|
||||
"sketch.draw_curves": "{linetype}として描く",
|
||||
"sketch.make_selection": "選択を{linetype}にする",
|
||||
"sketch.open_ends": "開放 {count}",
|
||||
"sketch.untied_ends": "未拘束 {count}",
|
||||
"sketch_text.align_center": "中央",
|
||||
"sketch_text.align_left": "左",
|
||||
"sketch_text.align_right": "右",
|
||||
@@ -833,7 +806,6 @@
|
||||
"sketch_text.path_clear": "文字を曲線から外す",
|
||||
"sketch_text.path_hint": "文字が沿うスケッチの曲線: ここをクリックしてから曲線をクリック",
|
||||
"sketch_text.use_font_file": "フォント ファイル {path} を使用",
|
||||
"solid": "中実",
|
||||
"the parts library is not available": "パーツライブラリは利用できません",
|
||||
"thread.automatic_size": "サイズを自動選択",
|
||||
"thread.designation": "ねじの呼び",
|
||||
@@ -845,7 +817,6 @@
|
||||
"timeline.rename_group": "フィーチャーフォルダの名前を変更",
|
||||
"timeline.reorder_many": "フィーチャーの順序を変更",
|
||||
"timeline.ungroup": "フィーチャーフォルダを削除",
|
||||
"to the face": "面まで",
|
||||
"to the next face": "次の面まで",
|
||||
"toolbar.image": "画像",
|
||||
"toolbar.render": "レンダリング",
|
||||
@@ -861,15 +832,12 @@
|
||||
"ui.activate": "アクティブ化",
|
||||
"ui.active_component": "アクティブなコンポーネント",
|
||||
"ui.agent_chat": "エージェントチャット",
|
||||
"ui.agent_chat_add": "ファイルを添付",
|
||||
"ui.agent_chat_add_file": "ファイルを選択…",
|
||||
"ui.agent_chat_allow": "許可",
|
||||
"ui.agent_chat_allowed": "許可しました",
|
||||
"ui.agent_chat_answered": "回答: {option}",
|
||||
"ui.agent_chat_approval": "エージェントにこの操作を許可しますか?",
|
||||
"ui.agent_chat_attach_view": "ビューを添付",
|
||||
"ui.agent_chat_attach_view_tip": "現在のビューの画像をメッセージと一緒に送ります",
|
||||
"ui.agent_chat_attachable": "画像・PDF・テキスト・CAD ファイル",
|
||||
"ui.agent_chat_auth_agent": "{name} でサインイン",
|
||||
"ui.agent_chat_auth_env": "環境変数 {vars} を設定して SolidCraft を再起動してください。",
|
||||
"ui.agent_chat_auth_needed": "エージェントがサインインを求めています。サインインはエージェント自身で行います。SolidCraft がパスワードやキーを見ることはありません。",
|
||||
@@ -889,32 +857,11 @@
|
||||
"ui.agent_chat_deny": "拒否",
|
||||
"ui.agent_chat_desktop_only": "エージェントチャットにはデスクトップアプリが必要です。",
|
||||
"ui.agent_chat_detecting": "エージェントを検索中…",
|
||||
"ui.agent_chat_dock": "サイドパネルに戻す",
|
||||
"ui.agent_chat_dock_left": "左にドッキング",
|
||||
"ui.agent_chat_dock_right": "右にドッキング",
|
||||
"ui.agent_chat_drop_hint": "ドロップして次のメッセージに添付",
|
||||
"ui.agent_chat_edit": "編集",
|
||||
"ui.agent_chat_exited": "エージェントが停止しました。",
|
||||
"ui.agent_chat_failed_start": "エージェントを起動できませんでした: {error}",
|
||||
"ui.agent_chat_file_cad_not_read": "{name}: CAD ファイルは読み込まず、デザインにインポートします",
|
||||
"ui.agent_chat_file_empty_picture": "{name}: 空の画像です",
|
||||
"ui.agent_chat_file_error": "{name}: {error}",
|
||||
"ui.agent_chat_file_heic": "{name}: HEIC 画像はここでは読めません。先に JPEG か PNG に書き出してください",
|
||||
"ui.agent_chat_file_importing": "デザインにインポート中…",
|
||||
"ui.agent_chat_file_loading": "ファイルを読み込み中…",
|
||||
"ui.agent_chat_file_not_a_file": "{name}: ファイルではありません",
|
||||
"ui.agent_chat_file_not_a_picture": "{name}: 画像ではありません",
|
||||
"ui.agent_chat_file_not_utf8": "{name}: UTF-8 のテキストではありません",
|
||||
"ui.agent_chat_file_picture_too_big": "{name}: 画像は {max} MB までです",
|
||||
"ui.agent_chat_file_text_too_big": "{name}: テキストファイルは {max} KB までです",
|
||||
"ui.agent_chat_file_too_large": "{name}: 大きすぎます",
|
||||
"ui.agent_chat_file_too_large_to_send": "{name}: 縮小しても大きすぎて送れません",
|
||||
"ui.agent_chat_file_unreadable_picture": "{name}: 読み込めない画像です",
|
||||
"ui.agent_chat_files": "ファイル: {files}",
|
||||
"ui.agent_chat_files_left_out": "1 つのメッセージで送れるファイルは {max} 個までです。送らなかったファイル: {files}",
|
||||
"ui.agent_chat_files_loading": "ファイルの読み込みを待っています",
|
||||
"ui.agent_chat_files_max": "1 つのメッセージに添付できるのは {max} 個までです",
|
||||
"ui.agent_chat_import_failed": "{name}: インポートに失敗しました: {error}",
|
||||
"ui.agent_chat_intro": "普段使っているコーディングエージェント、または選んだモデルのエンドポイントとチャットできます。SolidCraft のツールで開いているデザインを操作し、変更はその場で表示され、メッセージごとに1回で元に戻せます。確認なしで許可する範囲は「環境設定 > AI」で設定します。",
|
||||
"ui.agent_chat_key_in_env": "キー設定済み",
|
||||
"ui.agent_chat_left": "左",
|
||||
@@ -927,18 +874,14 @@
|
||||
"ui.agent_chat_no_cli": "アプリの隣に solidcraft-cli が見つからないため、エージェントはデザインにアクセスできません。",
|
||||
"ui.agent_chat_no_control": "エージェントチャットのエンドポイントが起動しなかったため、エージェントはデザインにアクセスできません。",
|
||||
"ui.agent_chat_no_images": "このエージェントは画像を受け付けません",
|
||||
"ui.agent_chat_no_picture": "クリップボードに画像がありません",
|
||||
"ui.agent_chat_none_found": "コーディングエージェントが見つかりません。次のいずれかをインストールしてから「再検索」を押してください:",
|
||||
"ui.agent_chat_none_found_short": "エージェントが見つかりません",
|
||||
"ui.agent_chat_not_installed": "未インストール",
|
||||
"ui.agent_chat_not_signed_in": "未サインイン",
|
||||
"ui.agent_chat_own_window": "独立したウィンドウ",
|
||||
"ui.agent_chat_paste_picture": "画像を貼り付け",
|
||||
"ui.agent_chat_permission": "エージェントが実行の許可を求めています: {title}",
|
||||
"ui.agent_chat_pick": "エージェント",
|
||||
"ui.agent_chat_placeholder": "デザインの変更をエージェントに依頼…",
|
||||
"ui.agent_chat_placeholder_busy": "エージェントの作業が終わったら送るメッセージを入力(Ctrl+Enter: 今すぐ送信)",
|
||||
"ui.agent_chat_pop_out": "独立したウィンドウで開く(別の画面に移動できます)",
|
||||
"ui.agent_chat_privacy": "あなたのアカウントで実行するエージェント以外には、何も送信されません。",
|
||||
"ui.agent_chat_privacy_note": "SolidCraft はどこにも何も送信しません。あなたが実行するエージェントが、あなた自身のアカウントでそのサービスと通信します。SolidCraft がキーやパスワードを見たり保存したりすることはありません。",
|
||||
"ui.agent_chat_queue": "待機に追加",
|
||||
@@ -948,8 +891,6 @@
|
||||
"ui.agent_chat_refresh": "再検索",
|
||||
"ui.agent_chat_refused": "エージェントには許可されていません: {reason}",
|
||||
"ui.agent_chat_remove": "待機から削除",
|
||||
"ui.agent_chat_remove_file": "このメッセージから外す",
|
||||
"ui.agent_chat_remove_imported": "このメッセージから外します。パーツはデザインに残ります (元に戻すで削除できます)",
|
||||
"ui.agent_chat_right": "右",
|
||||
"ui.agent_chat_save": "保存",
|
||||
"ui.agent_chat_send": "送信",
|
||||
@@ -1047,8 +988,6 @@
|
||||
"ui.ai_toolbar_button_note": "ツールバー右端の AI ボタン。Ctrl+Shift+A と表示メニューからも引き続きパネルを開けます。",
|
||||
"ui.ai_undo_tip": "このメッセージでエージェントが行った変更を、1回でまとめて元に戻します",
|
||||
"ui.all": "すべて",
|
||||
"ui.ambient_occlusion": "アンビエントオクルージョン",
|
||||
"ui.ambient_occlusion_help": "不透明な面が接する部分に柔らかい陰影を加えます。GPUビューポートが必要です。",
|
||||
"ui.analysis": "解析",
|
||||
"ui.angle": "角度",
|
||||
"ui.angle_decimals": "角度の小数点以下の桁数",
|
||||
@@ -1066,7 +1005,6 @@
|
||||
"ui.base_point": "基点",
|
||||
"ui.blocked": "到達不可",
|
||||
"ui.bodies": "ボディ",
|
||||
"ui.bodies_all_if_none": "ボディ(未選択ならすべて)",
|
||||
"ui.box": "直方体",
|
||||
"ui.break": "分割",
|
||||
"ui.calibrate": "校正",
|
||||
@@ -1121,7 +1059,6 @@
|
||||
"ui.continuity_g0": "G0 折れ",
|
||||
"ui.continuity_g1": "G1 接線",
|
||||
"ui.continuity_g2": "G2 曲率",
|
||||
"ui.continuity_gap": "隙間",
|
||||
"ui.copy": "コピー",
|
||||
"ui.corner": "コーナー",
|
||||
"ui.corners": "コーナー",
|
||||
@@ -1209,13 +1146,6 @@
|
||||
"ui.fully_constrained": "完全拘束",
|
||||
"ui.general": "一般",
|
||||
"ui.graphics": "グラフィックス",
|
||||
"ui.graphics_custom": "カスタム",
|
||||
"ui.graphics_performance": "パフォーマンス",
|
||||
"ui.graphics_preset": "グラフィックスプリセット",
|
||||
"ui.graphics_preset_help": "品質:アンビエントオクルージョン、細かい表示品質、適応型2×スーパーサンプリング。パフォーマンス:AOなし、標準の表示品質、スーパーサンプリングなし。手動変更はカスタムと表示されます。MSAAは変更されません。",
|
||||
"ui.graphics_quality": "品質",
|
||||
"ui.grid_follows_view": "グリッドをビューに追従させる",
|
||||
"ui.grid_follows_view_hint": "側面ビューでは、グリッドを地面ではなく、そのビューに向いた垂直平面に表示します",
|
||||
"ui.ground_shadow": "地面の影",
|
||||
"ui.groupSelected": "選択をグループ化",
|
||||
"ui.heading_add_ins": "アドイン",
|
||||
@@ -1242,7 +1172,6 @@
|
||||
"ui.heading_home": "ホーム",
|
||||
"ui.heading_insert": "挿入",
|
||||
"ui.heading_inspect": "検査",
|
||||
"ui.heading_interference": "干渉",
|
||||
"ui.heading_loft": "ロフト",
|
||||
"ui.heading_make": "製造",
|
||||
"ui.heading_measure": "計測",
|
||||
@@ -1367,7 +1296,6 @@
|
||||
"ui.one_side": "片側",
|
||||
"ui.open": "開く",
|
||||
"ui.open_ellipsis": "開く…",
|
||||
"ui.open_ends_tip": "どこにも接していない",
|
||||
"ui.open_preferences": "環境設定を開く",
|
||||
"ui.open_recent": "最近使った項目を開く",
|
||||
"ui.operation": "操作",
|
||||
@@ -1418,11 +1346,6 @@
|
||||
"ui.rectangle": "長方形",
|
||||
"ui.rectangular_pattern": "矩形パターン",
|
||||
"ui.redo": "やり直す",
|
||||
"ui.relink_design_changed": "参照元の選択中にアセンブリが変更されました。再リンクをやり直してください。",
|
||||
"ui.relink_done": "すべてのインスタンスの参照元を再リンクしました。",
|
||||
"ui.relink_not_linked": "このコンポーネントは参照元の設計にリンクされていません。",
|
||||
"ui.relink_select_one": "リンクされたコンポーネントまたはそのインスタンスを1つ選択してください。",
|
||||
"ui.relink_source_all": "参照元を再リンク(すべてのインスタンス)…",
|
||||
"ui.removeFromToolbar": "ツールバーから削除",
|
||||
"ui.removeSection": "セクションを解除",
|
||||
"ui.render": "レンダリング",
|
||||
@@ -1434,8 +1357,6 @@
|
||||
"ui.reset_the_settings_on_this_page": "このページの設定をリセット",
|
||||
"ui.restore": "復元",
|
||||
"ui.restore_defaults": "初期値に戻す",
|
||||
"ui.reverse_horizontal_pan_direction": "水平パンの方向を反転",
|
||||
"ui.reverse_horizontal_pan_tip": "水平方向の2本指スクロールとサムホイールで、反対方向にパンします",
|
||||
"ui.reverse_zoom_direction": "ズーム方向を反転",
|
||||
"ui.revolve": "回転",
|
||||
"ui.right": "右側面",
|
||||
@@ -1446,8 +1367,6 @@
|
||||
"ui.save_mesh_pick_bodies": "保存するボディを選択してください",
|
||||
"ui.save_mesh_temp_failed": "{folder} を作成できません: {error}",
|
||||
"ui.saved_to": "保存しました: {path}",
|
||||
"ui.sbox_pin": "Sボックスの上部に固定",
|
||||
"ui.sbox_unpin": "固定を解除",
|
||||
"ui.scale": "尺度",
|
||||
"ui.second_rail": "第2レール",
|
||||
"ui.section_analysis": "断面解析",
|
||||
@@ -1459,7 +1378,6 @@
|
||||
"ui.send": "送信",
|
||||
"ui.send_to_slicer": "スライサーに送る",
|
||||
"ui.sent_to_slicer": "{slicer} に送信しました",
|
||||
"ui.sheet_gap": "ギャップ",
|
||||
"ui.shell": "シェル",
|
||||
"ui.show": "表示",
|
||||
"ui.showAll": "すべて表示",
|
||||
@@ -1520,8 +1438,6 @@
|
||||
"ui.theme": "テーマ",
|
||||
"ui.thickness": "厚さ",
|
||||
"ui.thread": "ねじ",
|
||||
"ui.tie_ends": "拘束",
|
||||
"ui.tie_ends_tip": "未拘束をすべて拘束",
|
||||
"ui.timeline_bottom": "下部タイムライン",
|
||||
"ui.timeline_left_note": "左レイアウトでは、ロールバックバーがパーツとシーンブラウザーの上に表示されます。",
|
||||
"ui.timeline_left_sidebar": "左サイドバー",
|
||||
@@ -1544,7 +1460,6 @@
|
||||
"ui.units_of_new_designs": "新規デザインの単位",
|
||||
"ui.unlock": "ロック解除",
|
||||
"ui.unsuppress": "抑制を解除",
|
||||
"ui.untied_ends_tip": "位置だけで接している",
|
||||
"ui.untitled": "無題",
|
||||
"ui.up_axis_note": "ビュー(軸ビュー、ホーム、正面視)で上向きにする軸を指定します。デザインデータ自体は変更されません。",
|
||||
"ui.updateToCurrentView": "現在のビューに更新",
|
||||
|
||||
+1
-23
@@ -26,7 +26,6 @@
|
||||
"Current orthographic view (1:1 mm)": "현재 직교 투영 뷰 (1:1 mm)",
|
||||
"Current perspective view": "현재 원근 투영 뷰",
|
||||
"Curvature (G2)": "곡률 (G2)",
|
||||
"Direction": "방향",
|
||||
"Do not export hidden edges": "숨겨진 모서리 내보내지 않기",
|
||||
"Don't ask again": "다시 묻지 않기",
|
||||
"Drive Joint": "조인트 구동",
|
||||
@@ -43,11 +42,11 @@
|
||||
"Faces": "면",
|
||||
"Find in Browser": "브라우저에서 찾기",
|
||||
"Flat": "플랫",
|
||||
"Gap": "틈",
|
||||
"Hidden edges": "숨겨진 모서리",
|
||||
"Holes": "구멍",
|
||||
"Holes (edges on them)": "구멍 (구멍의 모서리)",
|
||||
"In place (surfaces)": "제자리에서 (서피스)",
|
||||
"In plane": "평면 내",
|
||||
"Inside": "안쪽",
|
||||
"Length": "길이",
|
||||
"Max": "최대",
|
||||
@@ -56,7 +55,6 @@
|
||||
"New study": "새 연구",
|
||||
"No visible bodies to export.": "표시된 바디가 없어 내보낼 수 없습니다.",
|
||||
"None": "없음",
|
||||
"Normal to sketch": "스케치에 수직",
|
||||
"Notch": "노치",
|
||||
"Outside": "바깥쪽",
|
||||
"Overall length": "전체 길이",
|
||||
@@ -425,10 +423,8 @@
|
||||
"cmd.timeline_roll_to": "이력 마커 이동",
|
||||
"cmd.timeline_rollback": "타임라인 마커 이동",
|
||||
"cmd.timeline_suppress": "특성 비활성화",
|
||||
"cmd.view_ambient_occlusion": "앰비언트 오클루전",
|
||||
"cmd.view_apply": "이름 있는 보기 복원",
|
||||
"cmd.view_delete": "이름 있는 보기 삭제",
|
||||
"cmd.view_graphics_preset": "그래픽 프리셋",
|
||||
"cmd.view_list": "이름 있는 보기 목록",
|
||||
"cmd.view_navigate": "보기 탐색",
|
||||
"cmd.view_rename": "이름 있는 보기 이름 변경",
|
||||
@@ -449,12 +445,7 @@
|
||||
"coupling.rack": "랙",
|
||||
"coupling.radius_1": "반지름 1",
|
||||
"coupling.radius_2": "반지름 2",
|
||||
"dialog.bodies_count": "바디 {count}개",
|
||||
"dialog.clearance_invalid": "간격은 0 이상의 길이여야 합니다",
|
||||
"dialog.compute": "계산",
|
||||
"dialog.compute_again": "다시 계산",
|
||||
"dialog.computing": "계산 중… {seconds}초",
|
||||
"dialog.could_not_be_checked": "확인할 수 없음",
|
||||
"dialog.custom_axis": "↖ 사용자 정의 축",
|
||||
"dialog.edit_title": "{title} 편집",
|
||||
"dialog.select": "↖ 선택",
|
||||
@@ -758,8 +749,6 @@
|
||||
"ui.ai_toolbar_button_note": "도구 모음 오른쪽 끝의 AI 버튼. Ctrl+Shift+A와 보기 메뉴로도 패널을 열 수 있습니다.",
|
||||
"ui.ai_undo_tip": "이 메시지에서 에이전트가 바꾼 모든 것을 한 번에 실행 취소",
|
||||
"ui.all": "모두",
|
||||
"ui.ambient_occlusion": "앰비언트 오클루전",
|
||||
"ui.ambient_occlusion_help": "불투명한 표면이 만나는 곳에 부드러운 음영을 추가합니다. GPU 뷰포트가 필요합니다.",
|
||||
"ui.analysis": "분석",
|
||||
"ui.angle": "각도",
|
||||
"ui.angle_decimals": "각도 소수점 자릿수",
|
||||
@@ -777,7 +766,6 @@
|
||||
"ui.base_point": "기준점",
|
||||
"ui.blocked": "막힘",
|
||||
"ui.bodies": "바디",
|
||||
"ui.bodies_all_if_none": "바디 (없으면 전체)",
|
||||
"ui.box": "박스",
|
||||
"ui.break": "끊기",
|
||||
"ui.calibrate": "캘리브레이션",
|
||||
@@ -828,7 +816,6 @@
|
||||
"ui.continuity_g0": "G0 꺾임",
|
||||
"ui.continuity_g1": "G1 접선",
|
||||
"ui.continuity_g2": "G2 곡률",
|
||||
"ui.continuity_gap": "틈",
|
||||
"ui.copy": "복사",
|
||||
"ui.corner": "코너",
|
||||
"ui.corners": "코너",
|
||||
@@ -914,11 +901,6 @@
|
||||
"ui.fully_constrained": "완전 구속",
|
||||
"ui.general": "일반",
|
||||
"ui.graphics": "그래픽",
|
||||
"ui.graphics_custom": "사용자 지정",
|
||||
"ui.graphics_performance": "성능",
|
||||
"ui.graphics_preset": "그래픽 프리셋",
|
||||
"ui.graphics_preset_help": "품질: 앰비언트 오클루전, 세밀한 표시 품질, 적응형 2× 슈퍼샘플링. 성능: 앰비언트 오클루전 끄기, 보통 표시 품질, 슈퍼샘플링 끄기. 수동 변경 시 사용자 지정으로 표시됩니다. MSAA는 유지됩니다.",
|
||||
"ui.graphics_quality": "품질",
|
||||
"ui.groupSelected": "선택 항목 그룹화",
|
||||
"ui.heading_assemble": "조립",
|
||||
"ui.heading_box": "박스",
|
||||
@@ -943,7 +925,6 @@
|
||||
"ui.heading_home": "홈",
|
||||
"ui.heading_insert": "삽입",
|
||||
"ui.heading_inspect": "검사",
|
||||
"ui.heading_interference": "간섭",
|
||||
"ui.heading_loft": "로프트",
|
||||
"ui.heading_measure": "측정",
|
||||
"ui.heading_mesh": "메시",
|
||||
@@ -1125,8 +1106,6 @@
|
||||
"ui.save_mesh_pick_bodies": "저장할 바디를 선택하세요",
|
||||
"ui.save_mesh_temp_failed": "{folder}을(를) 만들 수 없습니다: {error}",
|
||||
"ui.saved_to": "저장됨: {path}",
|
||||
"ui.sbox_pin": "S 상자 맨 위에 고정",
|
||||
"ui.sbox_unpin": "고정 해제",
|
||||
"ui.scale": "축척",
|
||||
"ui.section_analysis": "단면 분석",
|
||||
"ui.select": "선택",
|
||||
@@ -1136,7 +1115,6 @@
|
||||
"ui.send": "보내기",
|
||||
"ui.send_to_slicer": "슬라이서로 보내기",
|
||||
"ui.sent_to_slicer": "{slicer}(으)로 보냈습니다",
|
||||
"ui.sheet_gap": "틈",
|
||||
"ui.shell": "쉘",
|
||||
"ui.show": "표시",
|
||||
"ui.showAll": "모두 표시",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -82,7 +82,6 @@
|
||||
"Density": "Densidade",
|
||||
"Designs you open or save appear here.": "Os projetos que você abrir ou salvar aparecem aqui.",
|
||||
"Determined by To Object. Choose Angle to edit the angular extent.": "Determinado por Até o objeto. Escolha Ângulo para editar a extensão angular.",
|
||||
"Direction": "Direção",
|
||||
"Discard": "Descartar",
|
||||
"Do not export hidden edges": "Não exportar arestas ocultas",
|
||||
"Double-click to rename": "Clique duas vezes para renomear",
|
||||
@@ -113,6 +112,7 @@
|
||||
"Follow the camera (Look At sketches)": "Seguir a câmera (Olhar para os esboços)",
|
||||
"Font": "Fonte",
|
||||
"Full Length": "Comprimento total",
|
||||
"Gap": "Folga",
|
||||
"Grid": "Grade",
|
||||
"Height": "Altura",
|
||||
"Hidden edges": "Arestas ocultas",
|
||||
@@ -121,7 +121,6 @@
|
||||
"Home view": "Vista inicial",
|
||||
"In This Design": "Neste projeto",
|
||||
"In place (surfaces)": "No lugar (superfícies)",
|
||||
"In plane": "No plano",
|
||||
"Inside": "Dentro",
|
||||
"Interferes": "Interfere",
|
||||
"Issue title": "Título da issue",
|
||||
@@ -155,7 +154,6 @@
|
||||
"No matching appearances": "Nenhuma aparência correspondente",
|
||||
"No visible bodies to export.": "Nenhum corpo visível para exportar.",
|
||||
"None": "Nenhum",
|
||||
"Normal to sketch": "Normal ao esboço",
|
||||
"Not available yet": "Ainda não disponível",
|
||||
"Notch": "Entalhe",
|
||||
"Opacity": "Opacidade",
|
||||
@@ -1010,7 +1008,6 @@
|
||||
"ui.construct": "Construir",
|
||||
"ui.construction": "Construção",
|
||||
"ui.context_menu": "Menu de contexto",
|
||||
"ui.continuity_gap": "Folga",
|
||||
"ui.copy": "Copiar",
|
||||
"ui.corner": "Canto",
|
||||
"ui.corners": "Cantos",
|
||||
@@ -1317,8 +1314,6 @@
|
||||
"ui.save_mesh_pick_bodies": "Selecione os corpos a salvar",
|
||||
"ui.save_mesh_temp_failed": "Não foi possível criar {folder}: {error}",
|
||||
"ui.saved_to": "Salvo {path}",
|
||||
"ui.sbox_pin": "Fixar no topo da caixa S",
|
||||
"ui.sbox_unpin": "Desafixar",
|
||||
"ui.scale": "Escala",
|
||||
"ui.section_analysis": "Análise de seção",
|
||||
"ui.select": "Selecionar",
|
||||
@@ -1329,7 +1324,6 @@
|
||||
"ui.send": "Enviar",
|
||||
"ui.send_to_slicer": "Enviar ao fatiador",
|
||||
"ui.sent_to_slicer": "Enviado para {slicer}",
|
||||
"ui.sheet_gap": "Folga",
|
||||
"ui.shell": "Casca",
|
||||
"ui.show": "Mostrar",
|
||||
"ui.showAll": "Mostrar tudo",
|
||||
|
||||
+1
-16
@@ -76,7 +76,6 @@
|
||||
"Density": "Плотность",
|
||||
"Designs you open or save appear here.": "Здесь появляются открытые и сохранённые дизайны.",
|
||||
"Determined by To Object. Choose Angle to edit the angular extent.": "Определяется параметром «До объекта». Выберите «Угол», чтобы задать угловой предел.",
|
||||
"Direction": "Направление",
|
||||
"Discard": "Отменить",
|
||||
"Double-click to rename": "Двойной щелчок — переименовать",
|
||||
"Double-clicking the middle button in the view: Home (the home view, framing what is shown), Fit (frame what is shown, keeping the orientation) or nothing": "Двойной щелчок средней кнопкой в виде: «Главная» (главный вид, кадрирует показанное), «Вписать» (кадрирует, сохраняя ориентацию) или ничего",
|
||||
@@ -102,11 +101,11 @@
|
||||
"Follow the camera (Look At sketches)": "Следовать за камерой (эскизы «Взгляд на»)",
|
||||
"Font": "Шрифт",
|
||||
"Full Length": "Полная длина",
|
||||
"Gap": "Зазор",
|
||||
"Grid": "Сетка",
|
||||
"Height": "Высота",
|
||||
"Home view": "Главный вид",
|
||||
"In This Design": "В этом дизайне",
|
||||
"In plane": "В плоскости",
|
||||
"Interferes": "Пересекается",
|
||||
"Issue title": "Заголовок Issue",
|
||||
"Joint Origin1": "Точка сопряжения1",
|
||||
@@ -135,7 +134,6 @@
|
||||
"New study": "Новое исследование",
|
||||
"No crash reports.": "Нет отчётов о сбоях.",
|
||||
"No matching appearances": "Нет подходящих внешних видов",
|
||||
"Normal to sketch": "По нормали к эскизу",
|
||||
"Not available yet": "Пока недоступно",
|
||||
"Notch": "Вырез",
|
||||
"Opacity": "Непрозрачность",
|
||||
@@ -568,10 +566,8 @@
|
||||
"cmd.timeline_roll_to": "Переместить маркер истории",
|
||||
"cmd.timeline_rollback": "Сдвинуть маркер шкалы",
|
||||
"cmd.timeline_suppress": "Подавить операцию",
|
||||
"cmd.view_ambient_occlusion": "Окружающее затенение",
|
||||
"cmd.view_apply": "Вернуть именованный вид",
|
||||
"cmd.view_delete": "Удалить именованный вид",
|
||||
"cmd.view_graphics_preset": "Графический профиль",
|
||||
"cmd.view_list": "Список именованных видов",
|
||||
"cmd.view_navigate": "Навигация вида",
|
||||
"cmd.view_rename": "Переименовать именованный вид",
|
||||
@@ -752,8 +748,6 @@
|
||||
"ui.agent_cursor": "Курсор агента",
|
||||
"ui.agent_cursor_speed": "Скорость курсора агента",
|
||||
"ui.all": "Все",
|
||||
"ui.ambient_occlusion": "Окружающее затенение",
|
||||
"ui.ambient_occlusion_help": "Мягкое затенение в местах соприкосновения непрозрачных поверхностей. Требуется видовой экран GPU.",
|
||||
"ui.analysis": "Анализ",
|
||||
"ui.angle": "Угол",
|
||||
"ui.angle_decimals": "Знаков после запятой (углы)",
|
||||
@@ -821,7 +815,6 @@
|
||||
"ui.construct": "Построение",
|
||||
"ui.construction": "Вспомогательная",
|
||||
"ui.context_menu": "Контекстное меню",
|
||||
"ui.continuity_gap": "Зазор",
|
||||
"ui.copy": "Копировать",
|
||||
"ui.corner": "Угол",
|
||||
"ui.corners": "Углы",
|
||||
@@ -903,11 +896,6 @@
|
||||
"ui.fully_constrained": "Полностью определён",
|
||||
"ui.general": "Общие",
|
||||
"ui.graphics": "Графика",
|
||||
"ui.graphics_custom": "Настраиваемый",
|
||||
"ui.graphics_performance": "Производительность",
|
||||
"ui.graphics_preset": "Графический профиль",
|
||||
"ui.graphics_preset_help": "Качество: окружающее затенение, детальная тесселяция и адаптивный суперсэмплинг 2×. Производительность: без затенения, обычная тесселяция и без суперсэмплинга. Ручные изменения отображаются как настраиваемые. MSAA не меняется.",
|
||||
"ui.graphics_quality": "Качество",
|
||||
"ui.ground_shadow": "Тень на земле",
|
||||
"ui.groupSelected": "Группировать выбранное",
|
||||
"ui.heading_add_ins": "НАДСТРОЙКИ",
|
||||
@@ -1125,8 +1113,6 @@
|
||||
"ui.save_mesh_pick_bodies": "Выберите тела для сохранения",
|
||||
"ui.save_mesh_temp_failed": "Не удалось создать {folder}: {error}",
|
||||
"ui.saved_to": "Сохранено: {path}",
|
||||
"ui.sbox_pin": "Закрепить вверху панели S",
|
||||
"ui.sbox_unpin": "Открепить",
|
||||
"ui.scale": "Масштаб",
|
||||
"ui.section_analysis": "Анализ сечения",
|
||||
"ui.select": "Выбрать",
|
||||
@@ -1137,7 +1123,6 @@
|
||||
"ui.send": "Отправить",
|
||||
"ui.send_to_slicer": "Отправить в слайсер",
|
||||
"ui.sent_to_slicer": "Отправлено в {slicer}",
|
||||
"ui.sheet_gap": "Зазор",
|
||||
"ui.shell": "Оболочка",
|
||||
"ui.show": "Показать",
|
||||
"ui.showAll": "Показать всё",
|
||||
|
||||
@@ -80,7 +80,6 @@
|
||||
"Density": "Yoğunluk",
|
||||
"Designs you open or save appear here.": "Açtığınız veya kaydettiğiniz tasarımlar burada görünür.",
|
||||
"Determined by To Object. Choose Angle to edit the angular extent.": "Nesneye kadar seçeneğiyle belirlenir. Açısal kapsamı düzenlemek için Açı seçeneğini seçin.",
|
||||
"Direction": "Yön",
|
||||
"Discard": "At",
|
||||
"Double-click to rename": "Yeniden adlandırmak için çift tıklayın",
|
||||
"Double-clicking the middle button in the view: Home (the home view, framing what is shown), Fit (frame what is shown, keeping the orientation) or nothing": "Görünümde orta tuşla çift tıklama: Başlangıç (gösterileni çerçeveleyen başlangıç görünümü), Sığdır (yönü koruyarak gösterileni çerçeveler) veya hiçbir şey",
|
||||
@@ -107,6 +106,7 @@
|
||||
"Follow the camera (Look At sketches)": "Kamera takibi (eskizlerde Bak)",
|
||||
"Font": "Yazı tipi",
|
||||
"Full Length": "Tam uzunluk",
|
||||
"Gap": "Boşluk",
|
||||
"Grid": "Izgara",
|
||||
"Height": "Yükseklik",
|
||||
"Holes": "Delikler",
|
||||
@@ -114,7 +114,6 @@
|
||||
"Home view": "Başlangıç görünümü",
|
||||
"In This Design": "Bu tasarımda",
|
||||
"In place (surfaces)": "Yerinde (yüzeyler)",
|
||||
"In plane": "Düzlemde",
|
||||
"Inside": "İç",
|
||||
"Interferes": "Girişim var",
|
||||
"Issue title": "Sorun başlığı",
|
||||
@@ -148,7 +147,6 @@
|
||||
"No crash reports.": "Çökme raporu yok.",
|
||||
"No matching appearances": "Eşleşen görünüm yok",
|
||||
"None": "Hiçbiri",
|
||||
"Normal to sketch": "Eskize dik",
|
||||
"Not available yet": "Henüz kullanılamıyor",
|
||||
"Opacity": "Opaklık",
|
||||
"Open GitHub draft": "GitHub taslağını aç",
|
||||
@@ -959,7 +957,6 @@
|
||||
"ui.construct": "Yapı",
|
||||
"ui.construction": "Yapı çizgisi",
|
||||
"ui.context_menu": "Bağlam menüsü",
|
||||
"ui.continuity_gap": "Boşluk",
|
||||
"ui.copy": "Kopyala",
|
||||
"ui.corner": "Köşe",
|
||||
"ui.corners": "Köşeler",
|
||||
@@ -1264,8 +1261,6 @@
|
||||
"ui.save_mesh_pick_bodies": "Kaydedilecek gövdeleri seçin",
|
||||
"ui.save_mesh_temp_failed": "{folder} oluşturulamadı: {error}",
|
||||
"ui.saved_to": "{path} konumuna kaydedildi",
|
||||
"ui.sbox_pin": "S kutusunun en üstüne sabitle",
|
||||
"ui.sbox_unpin": "Sabitlemeyi kaldır",
|
||||
"ui.scale": "Ölçekle",
|
||||
"ui.section_analysis": "Kesit analizi",
|
||||
"ui.select": "Seç",
|
||||
@@ -1276,7 +1271,6 @@
|
||||
"ui.send": "Gönder",
|
||||
"ui.send_to_slicer": "Dilimleyiciye gönder",
|
||||
"ui.sent_to_slicer": "{slicer} uygulamasına gönderildi",
|
||||
"ui.sheet_gap": "Boşluk",
|
||||
"ui.shell": "Kabuk",
|
||||
"ui.show": "Göster",
|
||||
"ui.showAll": "Tümünü göster",
|
||||
|
||||
@@ -86,7 +86,6 @@
|
||||
"Density": "Густина",
|
||||
"Designs you open or save appear here.": "Проєкти, які ви відкриваєте чи зберігаєте, з'являються тут.",
|
||||
"Determined by To Object. Choose Angle to edit the angular extent.": "Визначається об'єктом призначення. Виберіть «Кут», щоб редагувати кутову протяжність.",
|
||||
"Direction": "Напрямок",
|
||||
"Discard": "Відхилити",
|
||||
"Do not export hidden edges": "Не експортувати приховані ребра",
|
||||
"Double-click to rename": "Двічі клацніть, щоб перейменувати",
|
||||
@@ -118,6 +117,7 @@
|
||||
"Follow the camera (Look At sketches)": "Стежити за камерою (ескізи наведення)",
|
||||
"Font": "Шрифт",
|
||||
"Full Length": "Повна довжина",
|
||||
"Gap": "Проміжок",
|
||||
"Grid": "Сітка",
|
||||
"Height": "Висота",
|
||||
"Hidden edges": "Приховані ребра",
|
||||
@@ -126,7 +126,6 @@
|
||||
"Home view": "Головний вид",
|
||||
"In This Design": "У цьому проєкті",
|
||||
"In place (surfaces)": "На місці (поверхні)",
|
||||
"In plane": "У площині",
|
||||
"Inside": "Усередину",
|
||||
"Interferes": "Перетинається",
|
||||
"Issue title": "Заголовок повідомлення",
|
||||
@@ -161,7 +160,6 @@
|
||||
"No matching appearances": "Немає відповідних виглядів",
|
||||
"No visible bodies to export.": "Немає видимих тіл для експорту.",
|
||||
"None": "Немає",
|
||||
"Normal to sketch": "За нормаллю до ескізу",
|
||||
"Not available yet": "Ще недоступно",
|
||||
"Notch": "Виріз",
|
||||
"Opacity": "Непрозорість",
|
||||
@@ -1033,7 +1031,6 @@
|
||||
"ui.construct": "Побудувати",
|
||||
"ui.construction": "Конструкційна",
|
||||
"ui.context_menu": "Контекстне меню",
|
||||
"ui.continuity_gap": "Проміжок",
|
||||
"ui.copy": "Копіювати",
|
||||
"ui.corner": "Кут",
|
||||
"ui.corners": "Кути",
|
||||
@@ -1340,8 +1337,6 @@
|
||||
"ui.save_mesh_pick_bodies": "Виберіть тіла для збереження",
|
||||
"ui.save_mesh_temp_failed": "Не вдалося створити {folder}: {error}",
|
||||
"ui.saved_to": "Збережено {path}",
|
||||
"ui.sbox_pin": "Закріпити вгорі поля S",
|
||||
"ui.sbox_unpin": "Відкріпити",
|
||||
"ui.scale": "Масштаб",
|
||||
"ui.section_analysis": "Аналіз перерізу",
|
||||
"ui.select": "Вибрати",
|
||||
@@ -1352,7 +1347,6 @@
|
||||
"ui.send": "Надіслати",
|
||||
"ui.send_to_slicer": "Надіслати в слайсер",
|
||||
"ui.sent_to_slicer": "Надіслано в {slicer}",
|
||||
"ui.sheet_gap": "Проміжок",
|
||||
"ui.shell": "Оболонка",
|
||||
"ui.show": "Показати",
|
||||
"ui.showAll": "Показати все",
|
||||
|
||||
+1
-86
@@ -31,7 +31,6 @@
|
||||
"Arc length": "弧长",
|
||||
"Area": "面积",
|
||||
"Attachment": "附着",
|
||||
"BOUNDARY SURFACE": "边界曲面",
|
||||
"BROWSER": "浏览器",
|
||||
"Bend Line Position": "弯折线位置",
|
||||
"Bend Position": "折弯位置",
|
||||
@@ -74,7 +73,6 @@
|
||||
"Counterbore depth": "沉孔深度",
|
||||
"Countersink angle": "沉头角度",
|
||||
"Create Issue — review draft": "创建 Issue——审阅草稿",
|
||||
"Cross curves": "交叉曲线",
|
||||
"Cube": "立方体",
|
||||
"Current orthographic view (1:1 mm)": "当前正交视图(1:1毫米)",
|
||||
"Current perspective view": "当前透视视图",
|
||||
@@ -88,7 +86,6 @@
|
||||
"Density": "密度",
|
||||
"Designs you open or save appear here.": "你打开或保存的设计会显示在此处。",
|
||||
"Determined by To Object. Choose Angle to edit the angular extent.": "由“到对象”确定。选择“角度”可编辑角度范围。",
|
||||
"Direction": "方向",
|
||||
"Discard": "放弃",
|
||||
"Do not export hidden edges": "不导出隐藏线",
|
||||
"Don't ask again": "不再询问",
|
||||
@@ -122,6 +119,7 @@
|
||||
"Follow the camera (Look At sketches)": "跟随相机(注视草图)",
|
||||
"Font": "字体",
|
||||
"Full Length": "全长",
|
||||
"Gap": "间隙",
|
||||
"Grid": "网格",
|
||||
"Height": "高度",
|
||||
"Hidden edges": "隐藏线",
|
||||
@@ -130,7 +128,6 @@
|
||||
"Home view": "主视图",
|
||||
"In This Design": "在此设计中",
|
||||
"In place (surfaces)": "原位(曲面)",
|
||||
"In plane": "在平面内",
|
||||
"Inside": "内侧",
|
||||
"Interferes": "发生干涉",
|
||||
"Issue title": "Issue 标题",
|
||||
@@ -166,7 +163,6 @@
|
||||
"No matching appearances": "没有匹配的外观",
|
||||
"No visible bodies to export.": "没有可导出的可见实体。",
|
||||
"None": "无",
|
||||
"Normal to sketch": "垂直于草图",
|
||||
"Not available yet": "暂不可用",
|
||||
"Notch": "缺口",
|
||||
"Opacity": "不透明度",
|
||||
@@ -350,7 +346,6 @@
|
||||
"cmd.component_move_sketches": "将草图移至组件",
|
||||
"cmd.component_paste_new": "粘贴为新对象",
|
||||
"cmd.component_properties": "组件物理属性",
|
||||
"cmd.component_relink": "重新链接源文件",
|
||||
"cmd.component_rename": "重命名组件",
|
||||
"cmd.component_revert_position": "恢复位置",
|
||||
"cmd.component_unlock": "解锁内容",
|
||||
@@ -560,8 +555,6 @@
|
||||
"cmd.sketch_isoparametric_curve": "等参数曲线",
|
||||
"cmd.sketch_line3d": "三维直线",
|
||||
"cmd.sketch_line_midpoint": "中点线",
|
||||
"cmd.sketch_loose_ends": "松散端点",
|
||||
"cmd.sketch_loose_ends_tie": "约束松散端点",
|
||||
"cmd.sketch_move": "移动/复制草图图元",
|
||||
"cmd.sketch_move3d": "移动三维点",
|
||||
"cmd.sketch_move_point": "拖动草图点",
|
||||
@@ -633,10 +626,8 @@
|
||||
"cmd.timeline_roll_to": "滚动历史标记",
|
||||
"cmd.timeline_rollback": "移动时间线标记",
|
||||
"cmd.timeline_suppress": "抑制特征",
|
||||
"cmd.view_ambient_occlusion": "环境光遮蔽",
|
||||
"cmd.view_apply": "还原命名视图",
|
||||
"cmd.view_delete": "删除命名视图",
|
||||
"cmd.view_graphics_preset": "图形预设",
|
||||
"cmd.view_list": "列出命名视图",
|
||||
"cmd.view_navigate": "导航视图",
|
||||
"cmd.view_rename": "重命名命名视图",
|
||||
@@ -658,12 +649,7 @@
|
||||
"coupling.rack": "齿条",
|
||||
"coupling.radius_1": "半径 1",
|
||||
"coupling.radius_2": "半径 2",
|
||||
"dialog.bodies_count": "{count} 个实体",
|
||||
"dialog.clearance_invalid": "间隙必须是 0 或更大的长度",
|
||||
"dialog.compute": "计算",
|
||||
"dialog.compute_again": "重新计算",
|
||||
"dialog.computing": "正在计算… {seconds} 秒",
|
||||
"dialog.could_not_be_checked": "无法检查",
|
||||
"dialog.custom_axis": "↖ 自定义轴",
|
||||
"dialog.edit_title": "编辑 {title}",
|
||||
"dialog.select": "↖ 选择",
|
||||
@@ -719,15 +705,6 @@
|
||||
"joint.from_bodies_hint": "每个所选实体移入各自的新组件,位置保持不变。",
|
||||
"joint.one_body": "两次选择都在 {body} 上。请在另一个零件上选择第二个。",
|
||||
"joint.one_component": "这些零件是同一组件的实体:{bodies}。接头使一个组件相对另一个组件运动,因此每个零件都需要自己的组件。",
|
||||
"messages.ago_h": "{n} 小时前",
|
||||
"messages.ago_min": "{n} 分钟前",
|
||||
"messages.ago_s": "{n} 秒前",
|
||||
"messages.chip_hint": "点击查看本次会话的所有消息",
|
||||
"messages.clear": "清除",
|
||||
"messages.copy_all": "全部复制",
|
||||
"messages.menu": "消息…",
|
||||
"messages.none": "暂无消息。",
|
||||
"messages.title": "消息",
|
||||
"motion.checked_to": "仅检查到第 {step} 步:检查已用完其工作量上限",
|
||||
"motion.collision": "{a} · {b}:第 {steps} 步",
|
||||
"motion.duration_hint": "秒(留空:按步)",
|
||||
@@ -742,11 +719,9 @@
|
||||
"new configuration": "新配置",
|
||||
"new name": "新名称",
|
||||
"new parameter": "新参数",
|
||||
"no hole": "无孔",
|
||||
"no interference": "无干涉",
|
||||
"no joint that moves yet": "尚无运动接头",
|
||||
"no picture": "无图片",
|
||||
"none": "无",
|
||||
"pattern.alignment_2": "对齐 2",
|
||||
"pattern.alignment_hint": "数量包含原始图形。居中时副本分布在两侧;数量为偶数时,反转可切换多一个副本的一侧。",
|
||||
"pattern.direction_1": "方向 1",
|
||||
@@ -819,8 +794,6 @@
|
||||
"sketch.degrees_of_freedom": "{count} 个自由度",
|
||||
"sketch.draw_curves": "绘制为 {linetype}",
|
||||
"sketch.make_selection": "将选择转换为 {linetype}",
|
||||
"sketch.open_ends": "开放 {count}",
|
||||
"sketch.untied_ends": "未约束 {count}",
|
||||
"sketch_text.align_center": "居中",
|
||||
"sketch_text.align_left": "左对齐",
|
||||
"sketch_text.align_right": "右对齐",
|
||||
@@ -833,7 +806,6 @@
|
||||
"sketch_text.path_clear": "将文字移离其曲线",
|
||||
"sketch_text.path_hint": "文字所跟随的草图曲线:先单击此处,再选择曲线",
|
||||
"sketch_text.use_font_file": "使用字体文件 {path}",
|
||||
"solid": "实心",
|
||||
"the parts library is not available": "零件库不可用",
|
||||
"thread.automatic_size": "自动尺寸",
|
||||
"thread.designation": "螺纹标记",
|
||||
@@ -845,7 +817,6 @@
|
||||
"timeline.rename_group": "重命名特征文件夹",
|
||||
"timeline.reorder_many": "特征重新排序",
|
||||
"timeline.ungroup": "移除特征文件夹",
|
||||
"to the face": "到面",
|
||||
"to the next face": "到下一个面",
|
||||
"toolbar.image": "图像",
|
||||
"toolbar.render": "渲染",
|
||||
@@ -861,15 +832,12 @@
|
||||
"ui.activate": "激活",
|
||||
"ui.active_component": "活动组件",
|
||||
"ui.agent_chat": "智能体聊天",
|
||||
"ui.agent_chat_add": "附加文件",
|
||||
"ui.agent_chat_add_file": "选择文件…",
|
||||
"ui.agent_chat_allow": "允许",
|
||||
"ui.agent_chat_allowed": "已允许",
|
||||
"ui.agent_chat_answered": "回答:{option}",
|
||||
"ui.agent_chat_approval": "允许智能体执行此操作吗?",
|
||||
"ui.agent_chat_attach_view": "附加视图",
|
||||
"ui.agent_chat_attach_view_tip": "随消息发送当前视图的图片",
|
||||
"ui.agent_chat_attachable": "图片、PDF、文本和 CAD 文件",
|
||||
"ui.agent_chat_auth_agent": "使用 {name} 登录",
|
||||
"ui.agent_chat_auth_env": "请在环境中设置 {vars},然后重启 SolidCraft。",
|
||||
"ui.agent_chat_auth_needed": "智能体要求你登录。登录在智能体本身完成;SolidCraft 从不接触你的密码或密钥。",
|
||||
@@ -889,32 +857,11 @@
|
||||
"ui.agent_chat_deny": "拒绝",
|
||||
"ui.agent_chat_desktop_only": "智能体聊天需要桌面应用。",
|
||||
"ui.agent_chat_detecting": "正在查找智能体…",
|
||||
"ui.agent_chat_dock": "停靠到侧面板",
|
||||
"ui.agent_chat_dock_left": "停靠在左侧",
|
||||
"ui.agent_chat_dock_right": "停靠在右侧",
|
||||
"ui.agent_chat_drop_hint": "拖放到此处,附加到下一条消息",
|
||||
"ui.agent_chat_edit": "编辑",
|
||||
"ui.agent_chat_exited": "智能体已停止。",
|
||||
"ui.agent_chat_failed_start": "智能体无法启动:{error}",
|
||||
"ui.agent_chat_file_cad_not_read": "{name}:CAD 文件会导入设计,而不是读取",
|
||||
"ui.agent_chat_file_empty_picture": "{name}:空图片",
|
||||
"ui.agent_chat_file_error": "{name}:{error}",
|
||||
"ui.agent_chat_file_heic": "{name}:这里无法读取 HEIC 图片;请先导出为 JPEG 或 PNG",
|
||||
"ui.agent_chat_file_importing": "正在导入设计…",
|
||||
"ui.agent_chat_file_loading": "正在读取文件…",
|
||||
"ui.agent_chat_file_not_a_file": "{name}:不是文件",
|
||||
"ui.agent_chat_file_not_a_picture": "{name}:不是图片",
|
||||
"ui.agent_chat_file_not_utf8": "{name}:不是 UTF-8 文本",
|
||||
"ui.agent_chat_file_picture_too_big": "{name}:图片最大 {max} MB",
|
||||
"ui.agent_chat_file_text_too_big": "{name}:文本文件最大 {max} KB",
|
||||
"ui.agent_chat_file_too_large": "{name}:太大",
|
||||
"ui.agent_chat_file_too_large_to_send": "{name}:即使缩小也太大,无法发送",
|
||||
"ui.agent_chat_file_unreadable_picture": "{name}:无法读取的图片",
|
||||
"ui.agent_chat_files": "文件:{files}",
|
||||
"ui.agent_chat_files_left_out": "一条消息最多发送 {max} 个文件;未发送:{files}",
|
||||
"ui.agent_chat_files_loading": "正在等待文件读取完成",
|
||||
"ui.agent_chat_files_max": "一条消息最多附加 {max} 个文件",
|
||||
"ui.agent_chat_import_failed": "{name}:导入失败:{error}",
|
||||
"ui.agent_chat_intro": "与你已在使用的编程智能体或你选择的模型端点聊天。它通过 SolidCraft 的工具处理打开的设计:更改即时显示,每条消息的更改可一步撤销。无需询问即可执行的范围在“首选项 > AI”中设置。",
|
||||
"ui.agent_chat_key_in_env": "已设置密钥",
|
||||
"ui.agent_chat_left": "左侧",
|
||||
@@ -927,18 +874,14 @@
|
||||
"ui.agent_chat_no_cli": "在应用旁边未找到 solidcraft-cli,因此智能体无法访问设计。",
|
||||
"ui.agent_chat_no_control": "智能体聊天的端点未能启动,因此智能体无法访问设计。",
|
||||
"ui.agent_chat_no_images": "该智能体不接收图片",
|
||||
"ui.agent_chat_no_picture": "剪贴板上没有图片",
|
||||
"ui.agent_chat_none_found": "未找到编程智能体。请安装以下之一,然后点击刷新:",
|
||||
"ui.agent_chat_none_found_short": "未找到智能体",
|
||||
"ui.agent_chat_not_installed": "未安装",
|
||||
"ui.agent_chat_not_signed_in": "未登录",
|
||||
"ui.agent_chat_own_window": "独立窗口",
|
||||
"ui.agent_chat_paste_picture": "粘贴图片",
|
||||
"ui.agent_chat_permission": "智能体请求运行:{title}",
|
||||
"ui.agent_chat_pick": "智能体",
|
||||
"ui.agent_chat_placeholder": "让智能体修改设计…",
|
||||
"ui.agent_chat_placeholder_busy": "输入消息,待智能体完成后发送(Ctrl+Enter:立即发送)",
|
||||
"ui.agent_chat_pop_out": "在独立窗口中打开(可移到另一个屏幕)",
|
||||
"ui.agent_chat_privacy": "除了你以自己的账户运行的智能体之外,不会向任何地方发送任何内容。",
|
||||
"ui.agent_chat_privacy_note": "SolidCraft 不向任何地方发送任何内容:你运行的智能体以你自己的账户与它自己的服务通信。SolidCraft 从不查看或保存任何密钥或密码。",
|
||||
"ui.agent_chat_queue": "加入队列",
|
||||
@@ -948,8 +891,6 @@
|
||||
"ui.agent_chat_refresh": "刷新",
|
||||
"ui.agent_chat_refused": "不允许智能体执行:{reason}",
|
||||
"ui.agent_chat_remove": "从队列中移除",
|
||||
"ui.agent_chat_remove_file": "从这条消息中移除",
|
||||
"ui.agent_chat_remove_imported": "从这条消息中移除;零件仍保留在设计中(撤销可将其删除)",
|
||||
"ui.agent_chat_right": "右侧",
|
||||
"ui.agent_chat_save": "保存",
|
||||
"ui.agent_chat_send": "发送",
|
||||
@@ -1047,8 +988,6 @@
|
||||
"ui.ai_toolbar_button_note": "工具栏右端的 AI 按钮。Ctrl+Shift+A 和视图菜单仍可打开面板。",
|
||||
"ui.ai_undo_tip": "将智能体为此消息所做的全部更改一步撤销",
|
||||
"ui.all": "全部",
|
||||
"ui.ambient_occlusion": "环境光遮蔽",
|
||||
"ui.ambient_occlusion_help": "在不透明表面相接处添加柔和阴影。需要 GPU 视口。",
|
||||
"ui.analysis": "分析",
|
||||
"ui.angle": "角度",
|
||||
"ui.angle_decimals": "角度小数位",
|
||||
@@ -1066,7 +1005,6 @@
|
||||
"ui.base_point": "基点",
|
||||
"ui.blocked": "被遮挡",
|
||||
"ui.bodies": "实体",
|
||||
"ui.bodies_all_if_none": "实体(未选择则全部)",
|
||||
"ui.box": "长方体",
|
||||
"ui.break": "打断",
|
||||
"ui.calibrate": "校准",
|
||||
@@ -1121,7 +1059,6 @@
|
||||
"ui.continuity_g0": "G0 折痕",
|
||||
"ui.continuity_g1": "G1 相切",
|
||||
"ui.continuity_g2": "G2 曲率",
|
||||
"ui.continuity_gap": "缝隙",
|
||||
"ui.copy": "复制",
|
||||
"ui.corner": "角",
|
||||
"ui.corners": "角点",
|
||||
@@ -1209,13 +1146,6 @@
|
||||
"ui.fully_constrained": "完全约束",
|
||||
"ui.general": "常规",
|
||||
"ui.graphics": "图形",
|
||||
"ui.graphics_custom": "自定义",
|
||||
"ui.graphics_performance": "性能",
|
||||
"ui.graphics_preset": "图形预设",
|
||||
"ui.graphics_preset_help": "质量:环境光遮蔽、精细显示质量和自适应2×超采样。性能:关闭环境光遮蔽、普通显示质量和关闭超采样。手动修改显示为自定义。MSAA保持不变。",
|
||||
"ui.graphics_quality": "质量",
|
||||
"ui.grid_follows_view": "网格跟随视图",
|
||||
"ui.grid_follows_view_hint": "在侧视图中,将网格显示在朝向该视图的竖直平面上,而不是地面上",
|
||||
"ui.ground_shadow": "地面阴影",
|
||||
"ui.groupSelected": "将所选编组",
|
||||
"ui.heading_add_ins": "加载项",
|
||||
@@ -1242,7 +1172,6 @@
|
||||
"ui.heading_home": "主页",
|
||||
"ui.heading_insert": "插入",
|
||||
"ui.heading_inspect": "检查",
|
||||
"ui.heading_interference": "干涉",
|
||||
"ui.heading_loft": "放样",
|
||||
"ui.heading_make": "制作",
|
||||
"ui.heading_measure": "测量",
|
||||
@@ -1367,7 +1296,6 @@
|
||||
"ui.one_side": "单侧",
|
||||
"ui.open": "打开",
|
||||
"ui.open_ellipsis": "打开...",
|
||||
"ui.open_ends_tip": "未接触任何曲线",
|
||||
"ui.open_preferences": "打开首选项",
|
||||
"ui.open_recent": "打开最近",
|
||||
"ui.operation": "操作",
|
||||
@@ -1418,11 +1346,6 @@
|
||||
"ui.rectangle": "矩形",
|
||||
"ui.rectangular_pattern": "矩形阵列",
|
||||
"ui.redo": "重做",
|
||||
"ui.relink_design_changed": "选择源文件时装配已更改。请重新开始链接。",
|
||||
"ui.relink_done": "已为所有实例重新链接源文件。",
|
||||
"ui.relink_not_linked": "此组件未链接到源设计。",
|
||||
"ui.relink_select_one": "请先选择一个链接组件或其实例。",
|
||||
"ui.relink_source_all": "重新链接源文件(所有实例)…",
|
||||
"ui.removeFromToolbar": "从工具栏移除",
|
||||
"ui.removeSection": "移除剖面",
|
||||
"ui.render": "渲染",
|
||||
@@ -1434,8 +1357,6 @@
|
||||
"ui.reset_the_settings_on_this_page": "重置此页面上的设置",
|
||||
"ui.restore": "还原",
|
||||
"ui.restore_defaults": "恢复默认值",
|
||||
"ui.reverse_horizontal_pan_direction": "反转水平平移方向",
|
||||
"ui.reverse_horizontal_pan_tip": "水平双指滚动和拇指滚轮向相反方向平移",
|
||||
"ui.reverse_zoom_direction": "反转缩放方向",
|
||||
"ui.revolve": "旋转",
|
||||
"ui.right": "右视",
|
||||
@@ -1446,8 +1367,6 @@
|
||||
"ui.save_mesh_pick_bodies": "请选择要保存的实体",
|
||||
"ui.save_mesh_temp_failed": "无法创建 {folder}:{error}",
|
||||
"ui.saved_to": "已保存:{path}",
|
||||
"ui.sbox_pin": "固定到 S 框顶部",
|
||||
"ui.sbox_unpin": "取消固定",
|
||||
"ui.scale": "缩放",
|
||||
"ui.second_rail": "第二导轨",
|
||||
"ui.section_analysis": "截面分析",
|
||||
@@ -1459,7 +1378,6 @@
|
||||
"ui.send": "发送",
|
||||
"ui.send_to_slicer": "发送到切片软件",
|
||||
"ui.sent_to_slicer": "已发送到 {slicer}",
|
||||
"ui.sheet_gap": "间隙",
|
||||
"ui.shell": "抽壳",
|
||||
"ui.show": "显示",
|
||||
"ui.showAll": "显示全部",
|
||||
@@ -1520,8 +1438,6 @@
|
||||
"ui.theme": "主题",
|
||||
"ui.thickness": "厚度",
|
||||
"ui.thread": "螺纹",
|
||||
"ui.tie_ends": "约束",
|
||||
"ui.tie_ends_tip": "约束全部未约束端点",
|
||||
"ui.timeline_bottom": "底部时间轴",
|
||||
"ui.timeline_left_note": "左侧布局下,回退条显示在零件和场景浏览器的上方。",
|
||||
"ui.timeline_left_sidebar": "左侧边栏",
|
||||
@@ -1544,7 +1460,6 @@
|
||||
"ui.units_of_new_designs": "新设计的单位",
|
||||
"ui.unlock": "解锁",
|
||||
"ui.unsuppress": "取消抑制",
|
||||
"ui.untied_ends_tip": "仅位置重合",
|
||||
"ui.untitled": "无标题",
|
||||
"ui.up_axis_note": "视图中朝上的轴(轴视图、主视图、注视)。设计本身不变。",
|
||||
"ui.updateToCurrentView": "更新为当前视图",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<project_license>MIT OR Apache-2.0</project_license>
|
||||
<name>SolidCraft</name>
|
||||
<summary>Independent parametric 3D CAD</summary>
|
||||
<summary>Parametric 3D CAD in the style of Fusion</summary>
|
||||
<developer id="io.github.bherbruck">
|
||||
<name>SolidCraft contributors</name>
|
||||
</developer>
|
||||
|
||||
@@ -10,7 +10,6 @@ struct U {
|
||||
acc_u: vec4<f32>, // accessibility map axes (xyz, offset)
|
||||
acc_v: vec4<f32>,
|
||||
eye: vec4<f32>, // eye position, w = 1 for perspective (0: orthographic, looking along -back)
|
||||
ao: vec4<f32>, // radius in world units (zero: disabled)
|
||||
// analysis colours (`theme::AnalysisColors`): draft positive, between, negative; reachable,
|
||||
// blocked; curvature flat, middle, tight
|
||||
ana_c: array<vec4<f32>, 8>,
|
||||
@@ -29,7 +28,6 @@ fn near_z(c: vec4<f32>) -> f32 {
|
||||
}
|
||||
|
||||
@group(1) @binding(2) var acc_t: texture_2d<f32>;
|
||||
@group(2) @binding(0) var ao_depth: texture_depth_2d;
|
||||
|
||||
fn clipped(p: vec3<f32>) -> bool {
|
||||
return length(u.clip.xyz) > 0.5 && dot(u.clip.xyz, p) > u.clip.w;
|
||||
@@ -121,7 +119,7 @@ fn solid(i: TOut, front: bool) -> vec4<f32> {
|
||||
let fw = fwidth(ndv);
|
||||
if (clipped(i.wp)) { discard; }
|
||||
if (!front && length(u.clip.xyz) > 0.5) {
|
||||
// The cut face in its body's colour ("from component" colouring), tinted toward the
|
||||
// The cut face in its body's colour (as Fusion's "from component"), tinted toward the
|
||||
// cap colour so it reads as a cut, hatched at 45 degrees on screen.
|
||||
let base = mix(i.c.rgb, u.cap.rgb, 0.35);
|
||||
let h = fract((i.pos.x + i.pos.y) / 9.0);
|
||||
@@ -130,7 +128,7 @@ fn solid(i: TOut, front: bool) -> vec4<f32> {
|
||||
if (u.ana.x > 0.5 && length(i.n) > 0.5) {
|
||||
return analysis(i, k);
|
||||
}
|
||||
let c = lit(i, occlusion(i));
|
||||
let c = shade(i);
|
||||
// Silhouettes (styles with edges): where a curved surface turns away from the eye, a line
|
||||
// about 1.5 px wide, on the visible surface only (depth-correct).
|
||||
if (u.screen.w > 0.5 && length(i.n) > 0.5 && fw > 1e-6) {
|
||||
@@ -140,59 +138,6 @@ fn solid(i: TOut, front: bool) -> vec4<f32> {
|
||||
return c;
|
||||
}
|
||||
|
||||
// A single-sample opaque depth map, independent of the window's MSAA and its UI offsets.
|
||||
@fragment
|
||||
fn fs_ao_depth(i: TOut, @builtin(front_facing) front: bool) -> @builtin(frag_depth) f32 {
|
||||
if (clipped(i.wp) || length(i.n) < 0.5) { discard; }
|
||||
if (!front && length(u.clip.xyz) > 0.5) { return cap_depth(i.wp, i.pos.z); }
|
||||
return i.pos.z;
|
||||
}
|
||||
|
||||
// Deterministic cosine-weighted hemisphere samples. Projecting the sample and a point one
|
||||
// radius toward the eye supplies a depth range in both orthographic and perspective views;
|
||||
// distant foreground geometry cannot cast a dark halo onto a background surface.
|
||||
fn occlusion(i: TOut) -> f32 {
|
||||
let radius = u.ao.x;
|
||||
if (radius <= 0.0 || length(i.n) < 0.5) { return 1.0; }
|
||||
var n = normalize(i.n);
|
||||
if (dot(n, u.back.xyz) < 0.0) { n = -n; }
|
||||
let axis = select(vec3<f32>(0.0, 0.0, 1.0), vec3<f32>(0.0, 1.0, 0.0), abs(n.z) > 0.9);
|
||||
let tangent = normalize(cross(axis, n));
|
||||
let bitangent = cross(n, tangent);
|
||||
let size = vec2<i32>(textureDimensions(ao_depth));
|
||||
// The map is a 1 × 1 stand-in until a frame draws into it (the product preview draws no
|
||||
// prepass): sampling it tells nothing, so leave the surface unoccluded.
|
||||
if (size.x < 2 || size.y < 2) { return 1.0; }
|
||||
// One comparison for both depth directions: depths read as 0 near / 1 far (mirrored when
|
||||
// the buffer runs reversed). The map is cleared to its far plane either way, so a texel
|
||||
// with nothing in it reads as 1 and the `d < 1.0` test below skips it.
|
||||
let flip = reversed();
|
||||
var blocked = 0.0;
|
||||
for (var j = 0u; j < 32u; j = j + 1u) {
|
||||
let f = (f32(j) + 0.5) / 32.0;
|
||||
let angle = f32(j) * 2.39996323;
|
||||
let direction = tangent * (cos(angle) * sqrt(f)) + bitangent * (sin(angle) * sqrt(f)) + n * sqrt(1.0 - f);
|
||||
let reach = radius * (0.2 + 0.8 * fract(f32(j) * 0.61803399 + 0.5));
|
||||
let sample = i.wp + n * (radius * 0.025) + direction * reach;
|
||||
let q = u.vp * vec4<f32>(sample, 1.0);
|
||||
let near = u.vp * vec4<f32>(sample + u.back.xyz * radius, 1.0);
|
||||
if (q.w <= 0.0 || near.w <= 0.0) { continue; }
|
||||
let ndc = q.xyz / q.w;
|
||||
let uv = vec2<f32>(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
|
||||
if (any(uv < vec2<f32>(0.0)) || any(uv >= vec2<f32>(1.0)) || ndc.z < 0.0 || ndc.z > 1.0) { continue; }
|
||||
let d0 = textureLoad(ao_depth, vec2<i32>(uv * vec2<f32>(size)), 0);
|
||||
let zs = select(ndc.z, 1.0 - ndc.z, flip);
|
||||
let zn = select(near.z / near.w, 1.0 - near.z / near.w, flip);
|
||||
let d = select(d0, 1.0 - d0, flip);
|
||||
let span = max(zs - zn, 1e-7);
|
||||
let delta = (zs - d) / span;
|
||||
if (d < 1.0 && delta > 0.005 && delta < 1.0) {
|
||||
blocked = blocked + smoothstep(0.005, 0.15, delta) * (1.0 - smoothstep(0.5, 1.0, delta));
|
||||
}
|
||||
}
|
||||
return 1.0 - 0.85 * blocked / 32.0;
|
||||
}
|
||||
|
||||
/// How fast the normal turns across the pixel (per mm).
|
||||
fn curvature(i: TOut) -> f32 {
|
||||
let nn = normalize(i.n);
|
||||
@@ -285,10 +230,6 @@ fn fs_tri_all(i: TOut) -> @location(0) vec4<f32> {
|
||||
}
|
||||
|
||||
fn shade(i: TOut) -> vec4<f32> {
|
||||
return lit(i, 1.0);
|
||||
}
|
||||
|
||||
fn lit(i: TOut, visibility: f32) -> vec4<f32> {
|
||||
// A zero normal marks flat colour (selection fills, translucent planes).
|
||||
if (length(i.n) < 0.5) {
|
||||
return out_color(i.c);
|
||||
@@ -301,7 +242,7 @@ fn lit(i: TOut, visibility: f32) -> vec4<f32> {
|
||||
let fill = max(dot(n, v), 0.0);
|
||||
let h = normalize(key + v);
|
||||
let spec = pow(max(dot(n, h), 0.0), 40.0) * 0.25;
|
||||
let k = 0.42 + 0.38 * diff + 0.25 * fill - (0.42 + 0.25 * fill) * (1.0 - visibility);
|
||||
let k = 0.42 + 0.38 * diff + 0.25 * fill;
|
||||
return out_color(vec4<f32>(min(i.c.rgb * k + vec3<f32>(spec), vec3<f32>(1.0)), i.c.a));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
{
|
||||
"format": "solidcraft-theme/1",
|
||||
"name": "Gruvbox Dark Hard",
|
||||
"base": "dark",
|
||||
"colors": {
|
||||
"app_bar": "#141617",
|
||||
"toolbar": "#1d2021",
|
||||
"panel": "#1d2021",
|
||||
"panel_header": "#3c3836",
|
||||
"border": "#504945",
|
||||
"text": "#ebdbb2",
|
||||
"text_dim": "#a89984",
|
||||
"accent": "#83a598",
|
||||
"accent_soft": "#414f4b",
|
||||
"sketch_accent": "#83a598",
|
||||
"error": "#fb4934",
|
||||
"warning": "#fabd2f",
|
||||
"icon": "#d5c4a1",
|
||||
"icon_fill": "#458588",
|
||||
"timeline": "#141617",
|
||||
"hover": "#46403e",
|
||||
"sel_face": "#458588",
|
||||
"sel_edge": "#83a598",
|
||||
"sel_edge_rim": "#aec4bc",
|
||||
"sel_profile": "#076678",
|
||||
"sel_profile_edge": "#458588",
|
||||
"hover_profile": "#83a59830",
|
||||
"hover_profile_edge": "#83a598",
|
||||
"sel_vertex": "#83a598",
|
||||
"origin_plane": "#fe801950",
|
||||
"origin_plane_edge": "#d65d0ec8",
|
||||
"origin_plane_hover": "#83a59896",
|
||||
"origin_point": "#d5c4a1",
|
||||
"axis_x": "#fb4934",
|
||||
"axis_y": "#b8bb26",
|
||||
"axis_z": "#83a598",
|
||||
"construction_plane": "#fe801940",
|
||||
"construction_geom": "#fe8019",
|
||||
"body": "#a89984",
|
||||
"body_edge": "#141617",
|
||||
"manipulator": "#83a598",
|
||||
"preview_add": "#83a598",
|
||||
"preview_new": "#fabd2f",
|
||||
"preview_sketch": "#b8bb26",
|
||||
"preview_cut": "#fb49346e",
|
||||
"cube_face": "#ebdbb2",
|
||||
"cube_border": "#665c54",
|
||||
"cube_text": "#282828",
|
||||
"cube_hover": "#83a598",
|
||||
"viewport_top": "#2c2c2c",
|
||||
"viewport_bottom": "#2c2c2c",
|
||||
"grid": "#443f3c",
|
||||
"grid_major": "#504945",
|
||||
"body_selected": "#83a598",
|
||||
"sketch": "#83a598",
|
||||
"sketch_fixed": "#ebdbb2",
|
||||
"sketch_locked": "#b8bb26",
|
||||
"dimension": "#ebdbb2",
|
||||
"dimension_driven": "#928374",
|
||||
"sketch_point": "#ebdbb2",
|
||||
"sketch_point_free": "#83a598",
|
||||
"dialog_bg": "#3c3836",
|
||||
"dialog_border": "#504945",
|
||||
"chip": "#458588",
|
||||
"heading_bg": "#1d2021",
|
||||
"subheading_bg": "#2c2c2c",
|
||||
"section_cap": "#d65d0e",
|
||||
"profile_fill": "#83a59828",
|
||||
"glyph_bg": "#2c2c2c",
|
||||
"glyph_edge": "#d65d0e",
|
||||
"glyph_text": "#fe8019",
|
||||
"overlay": "#1d2021f0",
|
||||
"rubber_band": "#83a598",
|
||||
"app_bar_text": "#ebdbb2",
|
||||
"app_bar_hover": "#ebdbb21c",
|
||||
"timeline_item": "#3c3836",
|
||||
"timeline_item_error": "#63221f",
|
||||
"timeline_marker": "#bdae93",
|
||||
"field": "#141617",
|
||||
"box_window": "#83a598",
|
||||
"box_crossing": "#b8bb26",
|
||||
"sketch_projected": "#d3869b",
|
||||
"sketch_lost": "#fb4934",
|
||||
"sketch_construction": "#fe8019",
|
||||
"sketch_centerline": "#d65d0e",
|
||||
"bend_up": "#b8bb26",
|
||||
"bend_down": "#fe8019",
|
||||
"interference": "#fb4934",
|
||||
"triad": [
|
||||
"#fb4934",
|
||||
"#b8bb26",
|
||||
"#83a598"
|
||||
],
|
||||
"gizmo": [
|
||||
"#fb4934",
|
||||
"#b8bb26",
|
||||
"#83a598"
|
||||
],
|
||||
"app_bar_icon": [
|
||||
"#ebdbb2",
|
||||
"#928374",
|
||||
"#83a598"
|
||||
],
|
||||
"icons.folder": "#d79921",
|
||||
"icons.axes": [
|
||||
"#fb4934",
|
||||
"#b8bb26",
|
||||
"#83a598"
|
||||
],
|
||||
"icons.agent": "#d3869b",
|
||||
"icons.warning": "#fabd2f",
|
||||
"icons.interference": "#fb4934",
|
||||
"icons.paint": [
|
||||
"#fb4934",
|
||||
"#83a598",
|
||||
"#fabd2f"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
{
|
||||
"format": "solidcraft-theme/1",
|
||||
"name": "Gruvbox Dark Soft",
|
||||
"base": "dark",
|
||||
"colors": {
|
||||
"app_bar": "#1d2021",
|
||||
"toolbar": "#32302f",
|
||||
"panel": "#32302f",
|
||||
"panel_header": "#3c3836",
|
||||
"border": "#504945",
|
||||
"text": "#ebdbb2",
|
||||
"text_dim": "#a89984",
|
||||
"accent": "#83a598",
|
||||
"accent_soft": "#4e5954",
|
||||
"sketch_accent": "#83a598",
|
||||
"error": "#fb4934",
|
||||
"warning": "#fabd2f",
|
||||
"icon": "#d5c4a1",
|
||||
"icon_fill": "#458588",
|
||||
"timeline": "#1d2021",
|
||||
"hover": "#46403e",
|
||||
"sel_face": "#458588",
|
||||
"sel_edge": "#83a598",
|
||||
"sel_edge_rim": "#aec4bc",
|
||||
"sel_profile": "#076678",
|
||||
"sel_profile_edge": "#458588",
|
||||
"hover_profile": "#83a59830",
|
||||
"hover_profile_edge": "#83a598",
|
||||
"sel_vertex": "#83a598",
|
||||
"origin_plane": "#fe801950",
|
||||
"origin_plane_edge": "#d65d0ec8",
|
||||
"origin_plane_hover": "#83a59896",
|
||||
"origin_point": "#d5c4a1",
|
||||
"axis_x": "#fb4934",
|
||||
"axis_y": "#b8bb26",
|
||||
"axis_z": "#83a598",
|
||||
"construction_plane": "#fe801940",
|
||||
"construction_geom": "#fe8019",
|
||||
"body": "#a89984",
|
||||
"body_edge": "#1d2021",
|
||||
"manipulator": "#83a598",
|
||||
"preview_add": "#83a598",
|
||||
"preview_new": "#fabd2f",
|
||||
"preview_sketch": "#b8bb26",
|
||||
"preview_cut": "#fb49346e",
|
||||
"cube_face": "#ebdbb2",
|
||||
"cube_border": "#665c54",
|
||||
"cube_text": "#282828",
|
||||
"cube_hover": "#83a598",
|
||||
"viewport_top": "#373432",
|
||||
"viewport_bottom": "#373432",
|
||||
"grid": "#443f3c",
|
||||
"grid_major": "#504945",
|
||||
"body_selected": "#83a598",
|
||||
"sketch": "#83a598",
|
||||
"sketch_fixed": "#ebdbb2",
|
||||
"sketch_locked": "#b8bb26",
|
||||
"dimension": "#ebdbb2",
|
||||
"dimension_driven": "#928374",
|
||||
"sketch_point": "#ebdbb2",
|
||||
"sketch_point_free": "#83a598",
|
||||
"dialog_bg": "#3c3836",
|
||||
"dialog_border": "#504945",
|
||||
"chip": "#458588",
|
||||
"heading_bg": "#32302f",
|
||||
"subheading_bg": "#373432",
|
||||
"section_cap": "#d65d0e",
|
||||
"profile_fill": "#83a59828",
|
||||
"glyph_bg": "#373432",
|
||||
"glyph_edge": "#d65d0e",
|
||||
"glyph_text": "#fe8019",
|
||||
"overlay": "#32302ff0",
|
||||
"rubber_band": "#83a598",
|
||||
"app_bar_text": "#ebdbb2",
|
||||
"app_bar_hover": "#ebdbb21c",
|
||||
"timeline_item": "#3c3836",
|
||||
"timeline_item_error": "#702b28",
|
||||
"timeline_marker": "#bdae93",
|
||||
"field": "#1d2021",
|
||||
"box_window": "#83a598",
|
||||
"box_crossing": "#b8bb26",
|
||||
"sketch_projected": "#d3869b",
|
||||
"sketch_lost": "#fb4934",
|
||||
"sketch_construction": "#fe8019",
|
||||
"sketch_centerline": "#d65d0e",
|
||||
"bend_up": "#b8bb26",
|
||||
"bend_down": "#fe8019",
|
||||
"interference": "#fb4934",
|
||||
"triad": [
|
||||
"#fb4934",
|
||||
"#b8bb26",
|
||||
"#83a598"
|
||||
],
|
||||
"gizmo": [
|
||||
"#fb4934",
|
||||
"#b8bb26",
|
||||
"#83a598"
|
||||
],
|
||||
"app_bar_icon": [
|
||||
"#ebdbb2",
|
||||
"#928374",
|
||||
"#83a598"
|
||||
],
|
||||
"icons.folder": "#d79921",
|
||||
"icons.axes": [
|
||||
"#fb4934",
|
||||
"#b8bb26",
|
||||
"#83a598"
|
||||
],
|
||||
"icons.agent": "#d3869b",
|
||||
"icons.warning": "#fabd2f",
|
||||
"icons.interference": "#fb4934",
|
||||
"icons.paint": [
|
||||
"#fb4934",
|
||||
"#83a598",
|
||||
"#fabd2f"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
{
|
||||
"format": "solidcraft-theme/1",
|
||||
"name": "Gruvbox Dark",
|
||||
"base": "dark",
|
||||
"colors": {
|
||||
"app_bar": "#1d2021",
|
||||
"toolbar": "#282828",
|
||||
"panel": "#282828",
|
||||
"panel_header": "#3c3836",
|
||||
"border": "#504945",
|
||||
"text": "#ebdbb2",
|
||||
"text_dim": "#a89984",
|
||||
"accent": "#83a598",
|
||||
"accent_soft": "#48544f",
|
||||
"sketch_accent": "#83a598",
|
||||
"error": "#fb4934",
|
||||
"warning": "#fabd2f",
|
||||
"icon": "#d5c4a1",
|
||||
"icon_fill": "#458588",
|
||||
"timeline": "#1d2021",
|
||||
"hover": "#46403e",
|
||||
"sel_face": "#458588",
|
||||
"sel_edge": "#83a598",
|
||||
"sel_edge_rim": "#aec4bc",
|
||||
"sel_profile": "#076678",
|
||||
"sel_profile_edge": "#458588",
|
||||
"hover_profile": "#83a59830",
|
||||
"hover_profile_edge": "#83a598",
|
||||
"sel_vertex": "#83a598",
|
||||
"origin_plane": "#fe801950",
|
||||
"origin_plane_edge": "#d65d0ec8",
|
||||
"origin_plane_hover": "#83a59896",
|
||||
"origin_point": "#d5c4a1",
|
||||
"axis_x": "#fb4934",
|
||||
"axis_y": "#b8bb26",
|
||||
"axis_z": "#83a598",
|
||||
"construction_plane": "#fe801940",
|
||||
"construction_geom": "#fe8019",
|
||||
"body": "#a89984",
|
||||
"body_edge": "#1d2021",
|
||||
"manipulator": "#83a598",
|
||||
"preview_add": "#83a598",
|
||||
"preview_new": "#fabd2f",
|
||||
"preview_sketch": "#b8bb26",
|
||||
"preview_cut": "#fb49346e",
|
||||
"cube_face": "#ebdbb2",
|
||||
"cube_border": "#665c54",
|
||||
"cube_text": "#282828",
|
||||
"cube_hover": "#83a598",
|
||||
"viewport_top": "#32302f",
|
||||
"viewport_bottom": "#32302f",
|
||||
"grid": "#443f3c",
|
||||
"grid_major": "#504945",
|
||||
"body_selected": "#83a598",
|
||||
"sketch": "#83a598",
|
||||
"sketch_fixed": "#ebdbb2",
|
||||
"sketch_locked": "#b8bb26",
|
||||
"dimension": "#ebdbb2",
|
||||
"dimension_driven": "#928374",
|
||||
"sketch_point": "#ebdbb2",
|
||||
"sketch_point_free": "#83a598",
|
||||
"dialog_bg": "#3c3836",
|
||||
"dialog_border": "#504945",
|
||||
"chip": "#458588",
|
||||
"heading_bg": "#282828",
|
||||
"subheading_bg": "#32302f",
|
||||
"section_cap": "#d65d0e",
|
||||
"profile_fill": "#83a59828",
|
||||
"glyph_bg": "#32302f",
|
||||
"glyph_edge": "#d65d0e",
|
||||
"glyph_text": "#fe8019",
|
||||
"overlay": "#282828f0",
|
||||
"rubber_band": "#83a598",
|
||||
"app_bar_text": "#ebdbb2",
|
||||
"app_bar_hover": "#ebdbb21c",
|
||||
"timeline_item": "#3c3836",
|
||||
"timeline_item_error": "#6a2624",
|
||||
"timeline_marker": "#bdae93",
|
||||
"field": "#1d2021",
|
||||
"box_window": "#83a598",
|
||||
"box_crossing": "#b8bb26",
|
||||
"sketch_projected": "#d3869b",
|
||||
"sketch_lost": "#fb4934",
|
||||
"sketch_construction": "#fe8019",
|
||||
"sketch_centerline": "#d65d0e",
|
||||
"bend_up": "#b8bb26",
|
||||
"bend_down": "#fe8019",
|
||||
"interference": "#fb4934",
|
||||
"triad": [
|
||||
"#fb4934",
|
||||
"#b8bb26",
|
||||
"#83a598"
|
||||
],
|
||||
"gizmo": [
|
||||
"#fb4934",
|
||||
"#b8bb26",
|
||||
"#83a598"
|
||||
],
|
||||
"app_bar_icon": [
|
||||
"#ebdbb2",
|
||||
"#928374",
|
||||
"#83a598"
|
||||
],
|
||||
"icons.folder": "#d79921",
|
||||
"icons.axes": [
|
||||
"#fb4934",
|
||||
"#b8bb26",
|
||||
"#83a598"
|
||||
],
|
||||
"icons.agent": "#d3869b",
|
||||
"icons.warning": "#fabd2f",
|
||||
"icons.interference": "#fb4934",
|
||||
"icons.paint": [
|
||||
"#fb4934",
|
||||
"#83a598",
|
||||
"#fabd2f"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
{
|
||||
"format": "solidcraft-theme/1",
|
||||
"name": "Gruvbox Light Hard",
|
||||
"base": "light",
|
||||
"colors": {
|
||||
"app_bar": "#3c3836",
|
||||
"toolbar": "#f9f5d7",
|
||||
"panel": "#f9f5d7",
|
||||
"panel_header": "#ebdbb2",
|
||||
"border": "#d5c4a1",
|
||||
"text": "#3c3836",
|
||||
"text_dim": "#7c6f64",
|
||||
"accent": "#076678",
|
||||
"accent_soft": "#c9d8c4",
|
||||
"sketch_accent": "#076678",
|
||||
"error": "#9d0006",
|
||||
"warning": "#b57614",
|
||||
"icon": "#504945",
|
||||
"icon_fill": "#bdae93",
|
||||
"timeline": "#ebdbb2",
|
||||
"hover": "#f1e5c1",
|
||||
"sel_face": "#458588",
|
||||
"sel_edge": "#076678",
|
||||
"sel_edge_rim": "#83a598",
|
||||
"sel_profile": "#076678",
|
||||
"sel_profile_edge": "#458588",
|
||||
"hover_profile": "#45858830",
|
||||
"hover_profile_edge": "#458588",
|
||||
"sel_vertex": "#076678",
|
||||
"origin_plane": "#fe80195e",
|
||||
"origin_plane_edge": "#d65d0ec8",
|
||||
"origin_plane_hover": "#45858896",
|
||||
"origin_point": "#d5c4a1",
|
||||
"axis_x": "#cc241d",
|
||||
"axis_y": "#98971a",
|
||||
"axis_z": "#458588",
|
||||
"construction_plane": "#fe801946",
|
||||
"construction_geom": "#af3a03",
|
||||
"body": "#bdae93",
|
||||
"body_edge": "#3c3836",
|
||||
"manipulator": "#076678",
|
||||
"preview_add": "#458588",
|
||||
"preview_new": "#d79921",
|
||||
"preview_sketch": "#98971a",
|
||||
"preview_cut": "#cc241d6e",
|
||||
"cube_face": "#fbf1c7",
|
||||
"cube_border": "#7c6f64",
|
||||
"cube_text": "#3c3836",
|
||||
"cube_hover": "#83a598",
|
||||
"viewport_top": "#fbf8e3",
|
||||
"viewport_bottom": "#ebdbb2",
|
||||
"grid": "#d5c4a1",
|
||||
"grid_major": "#bdae93",
|
||||
"body_selected": "#83a598",
|
||||
"sketch": "#076678",
|
||||
"sketch_fixed": "#282828",
|
||||
"sketch_locked": "#79740e",
|
||||
"dimension": "#3c3836",
|
||||
"dimension_driven": "#7c6f64",
|
||||
"sketch_point": "#282828",
|
||||
"sketch_point_free": "#076678",
|
||||
"dialog_bg": "#f9f5d7",
|
||||
"dialog_border": "#d5c4a1",
|
||||
"chip": "#458588",
|
||||
"heading_bg": "#d5c4a1",
|
||||
"subheading_bg": "#ebdbb2",
|
||||
"section_cap": "#d65d0e",
|
||||
"profile_fill": "#fabd2f46",
|
||||
"glyph_bg": "#fcfaeb",
|
||||
"glyph_edge": "#af3a03",
|
||||
"glyph_text": "#af3a03",
|
||||
"overlay": "#f9f5d7eb",
|
||||
"rubber_band": "#076678",
|
||||
"app_bar_text": "#fbf1c7",
|
||||
"app_bar_hover": "#fbf1c71c",
|
||||
"timeline_item": "#f9f5d7",
|
||||
"timeline_item_error": "#f0cbb2",
|
||||
"timeline_marker": "#504945",
|
||||
"field": "#fcfaeb",
|
||||
"box_window": "#076678",
|
||||
"box_crossing": "#79740e",
|
||||
"sketch_projected": "#8f3f71",
|
||||
"sketch_lost": "#9d0006",
|
||||
"sketch_construction": "#af3a03",
|
||||
"sketch_centerline": "#d65d0e",
|
||||
"bend_up": "#79740e",
|
||||
"bend_down": "#af3a03",
|
||||
"interference": "#cc241d",
|
||||
"triad": [
|
||||
"#cc241d",
|
||||
"#98971a",
|
||||
"#458588"
|
||||
],
|
||||
"gizmo": [
|
||||
"#cc241d",
|
||||
"#98971a",
|
||||
"#458588"
|
||||
],
|
||||
"app_bar_icon": [
|
||||
"#fbf1c7",
|
||||
"#a89984",
|
||||
"#83a598"
|
||||
],
|
||||
"icons.folder": "#d79921",
|
||||
"icons.axes": [
|
||||
"#cc241d",
|
||||
"#98971a",
|
||||
"#458588"
|
||||
],
|
||||
"icons.agent": "#8f3f71",
|
||||
"icons.warning": "#d79921",
|
||||
"icons.interference": "#cc241d",
|
||||
"icons.paint": [
|
||||
"#cc241d",
|
||||
"#458588",
|
||||
"#d79921"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
{
|
||||
"format": "solidcraft-theme/1",
|
||||
"name": "Gruvbox Light Soft",
|
||||
"base": "light",
|
||||
"colors": {
|
||||
"app_bar": "#3c3836",
|
||||
"toolbar": "#f2e5bc",
|
||||
"panel": "#f2e5bc",
|
||||
"panel_header": "#ebdbb2",
|
||||
"border": "#d5c4a1",
|
||||
"text": "#3c3836",
|
||||
"text_dim": "#7c6f64",
|
||||
"accent": "#076678",
|
||||
"accent_soft": "#c3ccae",
|
||||
"sketch_accent": "#076678",
|
||||
"error": "#9d0006",
|
||||
"warning": "#b57614",
|
||||
"icon": "#504945",
|
||||
"icon_fill": "#bdae93",
|
||||
"timeline": "#ebdbb2",
|
||||
"hover": "#eedfb6",
|
||||
"sel_face": "#458588",
|
||||
"sel_edge": "#076678",
|
||||
"sel_edge_rim": "#83a598",
|
||||
"sel_profile": "#076678",
|
||||
"sel_profile_edge": "#458588",
|
||||
"hover_profile": "#45858830",
|
||||
"hover_profile_edge": "#458588",
|
||||
"sel_vertex": "#076678",
|
||||
"origin_plane": "#fe80195e",
|
||||
"origin_plane_edge": "#d65d0ec8",
|
||||
"origin_plane_hover": "#45858896",
|
||||
"origin_point": "#d5c4a1",
|
||||
"axis_x": "#cc241d",
|
||||
"axis_y": "#98971a",
|
||||
"axis_z": "#458588",
|
||||
"construction_plane": "#fe801946",
|
||||
"construction_geom": "#af3a03",
|
||||
"body": "#bdae93",
|
||||
"body_edge": "#3c3836",
|
||||
"manipulator": "#076678",
|
||||
"preview_add": "#458588",
|
||||
"preview_new": "#d79921",
|
||||
"preview_sketch": "#98971a",
|
||||
"preview_cut": "#cc241d6e",
|
||||
"cube_face": "#fbf1c7",
|
||||
"cube_border": "#7c6f64",
|
||||
"cube_text": "#3c3836",
|
||||
"cube_hover": "#83a598",
|
||||
"viewport_top": "#f6edd0",
|
||||
"viewport_bottom": "#ebdbb2",
|
||||
"grid": "#d5c4a1",
|
||||
"grid_major": "#bdae93",
|
||||
"body_selected": "#83a598",
|
||||
"sketch": "#076678",
|
||||
"sketch_fixed": "#282828",
|
||||
"sketch_locked": "#79740e",
|
||||
"dimension": "#3c3836",
|
||||
"dimension_driven": "#7c6f64",
|
||||
"sketch_point": "#282828",
|
||||
"sketch_point_free": "#076678",
|
||||
"dialog_bg": "#f2e5bc",
|
||||
"dialog_border": "#d5c4a1",
|
||||
"chip": "#458588",
|
||||
"heading_bg": "#d5c4a1",
|
||||
"subheading_bg": "#ebdbb2",
|
||||
"section_cap": "#d65d0e",
|
||||
"profile_fill": "#fabd2f46",
|
||||
"glyph_bg": "#f8f2de",
|
||||
"glyph_edge": "#af3a03",
|
||||
"glyph_text": "#af3a03",
|
||||
"overlay": "#f2e5bceb",
|
||||
"rubber_band": "#076678",
|
||||
"app_bar_text": "#fbf1c7",
|
||||
"app_bar_hover": "#fbf1c71c",
|
||||
"timeline_item": "#f2e5bc",
|
||||
"timeline_item_error": "#eabe9c",
|
||||
"timeline_marker": "#504945",
|
||||
"field": "#f8f2de",
|
||||
"box_window": "#076678",
|
||||
"box_crossing": "#79740e",
|
||||
"sketch_projected": "#8f3f71",
|
||||
"sketch_lost": "#9d0006",
|
||||
"sketch_construction": "#af3a03",
|
||||
"sketch_centerline": "#d65d0e",
|
||||
"bend_up": "#79740e",
|
||||
"bend_down": "#af3a03",
|
||||
"interference": "#cc241d",
|
||||
"triad": [
|
||||
"#cc241d",
|
||||
"#98971a",
|
||||
"#458588"
|
||||
],
|
||||
"gizmo": [
|
||||
"#cc241d",
|
||||
"#98971a",
|
||||
"#458588"
|
||||
],
|
||||
"app_bar_icon": [
|
||||
"#fbf1c7",
|
||||
"#a89984",
|
||||
"#83a598"
|
||||
],
|
||||
"icons.folder": "#d79921",
|
||||
"icons.axes": [
|
||||
"#cc241d",
|
||||
"#98971a",
|
||||
"#458588"
|
||||
],
|
||||
"icons.agent": "#8f3f71",
|
||||
"icons.warning": "#d79921",
|
||||
"icons.interference": "#cc241d",
|
||||
"icons.paint": [
|
||||
"#cc241d",
|
||||
"#458588",
|
||||
"#d79921"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
{
|
||||
"format": "solidcraft-theme/1",
|
||||
"name": "Gruvbox Light",
|
||||
"base": "light",
|
||||
"colors": {
|
||||
"app_bar": "#3c3836",
|
||||
"toolbar": "#fbf1c7",
|
||||
"panel": "#fbf1c7",
|
||||
"panel_header": "#ebdbb2",
|
||||
"border": "#d5c4a1",
|
||||
"text": "#3c3836",
|
||||
"text_dim": "#7c6f64",
|
||||
"accent": "#076678",
|
||||
"accent_soft": "#cad5b7",
|
||||
"sketch_accent": "#076678",
|
||||
"error": "#9d0006",
|
||||
"warning": "#b57614",
|
||||
"icon": "#504945",
|
||||
"icon_fill": "#bdae93",
|
||||
"timeline": "#ebdbb2",
|
||||
"hover": "#f1e4ba",
|
||||
"sel_face": "#458588",
|
||||
"sel_edge": "#076678",
|
||||
"sel_edge_rim": "#83a598",
|
||||
"sel_profile": "#076678",
|
||||
"sel_profile_edge": "#458588",
|
||||
"hover_profile": "#45858830",
|
||||
"hover_profile_edge": "#458588",
|
||||
"sel_vertex": "#076678",
|
||||
"origin_plane": "#fe80195e",
|
||||
"origin_plane_edge": "#d65d0ec8",
|
||||
"origin_plane_hover": "#45858896",
|
||||
"origin_point": "#d5c4a1",
|
||||
"axis_x": "#cc241d",
|
||||
"axis_y": "#98971a",
|
||||
"axis_z": "#458588",
|
||||
"construction_plane": "#fe801946",
|
||||
"construction_geom": "#af3a03",
|
||||
"body": "#bdae93",
|
||||
"body_edge": "#3c3836",
|
||||
"manipulator": "#076678",
|
||||
"preview_add": "#458588",
|
||||
"preview_new": "#d79921",
|
||||
"preview_sketch": "#98971a",
|
||||
"preview_cut": "#cc241d6e",
|
||||
"cube_face": "#fbf1c7",
|
||||
"cube_border": "#7c6f64",
|
||||
"cube_text": "#3c3836",
|
||||
"cube_hover": "#83a598",
|
||||
"viewport_top": "#fcf5d8",
|
||||
"viewport_bottom": "#ebdbb2",
|
||||
"grid": "#d5c4a1",
|
||||
"grid_major": "#bdae93",
|
||||
"body_selected": "#83a598",
|
||||
"sketch": "#076678",
|
||||
"sketch_fixed": "#282828",
|
||||
"sketch_locked": "#79740e",
|
||||
"dimension": "#3c3836",
|
||||
"dimension_driven": "#7c6f64",
|
||||
"sketch_point": "#282828",
|
||||
"sketch_point_free": "#076678",
|
||||
"dialog_bg": "#fbf1c7",
|
||||
"dialog_border": "#d5c4a1",
|
||||
"chip": "#458588",
|
||||
"heading_bg": "#d5c4a1",
|
||||
"subheading_bg": "#ebdbb2",
|
||||
"section_cap": "#d65d0e",
|
||||
"profile_fill": "#fabd2f46",
|
||||
"glyph_bg": "#fdf8e3",
|
||||
"glyph_edge": "#af3a03",
|
||||
"glyph_text": "#af3a03",
|
||||
"overlay": "#fbf1c7eb",
|
||||
"rubber_band": "#076678",
|
||||
"app_bar_text": "#fbf1c7",
|
||||
"app_bar_hover": "#fbf1c71c",
|
||||
"timeline_item": "#fbf1c7",
|
||||
"timeline_item_error": "#f2c8a5",
|
||||
"timeline_marker": "#504945",
|
||||
"field": "#fdf8e3",
|
||||
"box_window": "#076678",
|
||||
"box_crossing": "#79740e",
|
||||
"sketch_projected": "#8f3f71",
|
||||
"sketch_lost": "#9d0006",
|
||||
"sketch_construction": "#af3a03",
|
||||
"sketch_centerline": "#d65d0e",
|
||||
"bend_up": "#79740e",
|
||||
"bend_down": "#af3a03",
|
||||
"interference": "#cc241d",
|
||||
"triad": [
|
||||
"#cc241d",
|
||||
"#98971a",
|
||||
"#458588"
|
||||
],
|
||||
"gizmo": [
|
||||
"#cc241d",
|
||||
"#98971a",
|
||||
"#458588"
|
||||
],
|
||||
"app_bar_icon": [
|
||||
"#fbf1c7",
|
||||
"#a89984",
|
||||
"#83a598"
|
||||
],
|
||||
"icons.folder": "#d79921",
|
||||
"icons.axes": [
|
||||
"#cc241d",
|
||||
"#98971a",
|
||||
"#458588"
|
||||
],
|
||||
"icons.agent": "#8f3f71",
|
||||
"icons.warning": "#d79921",
|
||||
"icons.interference": "#cc241d",
|
||||
"icons.paint": [
|
||||
"#cc241d",
|
||||
"#458588",
|
||||
"#d79921"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ pub(crate) fn remap_sheet_metadata(st: &mut ModelState, names: &[String], transf
|
||||
.filter(|(_, t)| names.contains(t.body.as_str()))
|
||||
.map(|(i, t)| {
|
||||
let point = apply_point(transform, t.axis_point);
|
||||
let axis = apply_vector(transform, *t.axis).unit().ok_or_else(|| DocError::Invalid("invalid transferred thread axis".into()))?;
|
||||
let axis = apply_vector(transform, t.axis).normalized().ok_or_else(|| DocError::Invalid("invalid transferred thread axis".into()))?;
|
||||
if !point.is_finite() {
|
||||
return Err(DocError::Invalid("invalid transferred thread placement".into()));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Canvases: reference images placed on a plane (Insert Canvas), kept in the design.
|
||||
//! Canvases: reference images placed on a plane (Fusion's Insert Canvas), kept in the design.
|
||||
//! The image bytes travel inside the design file (base64), so it reopens without the original.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -864,15 +864,7 @@ mod tests {
|
||||
|
||||
fn solved(mut sketch: solidcraft_sketch::Sketch) -> SolvedSketch {
|
||||
let report = solidcraft_sketch::solve(&mut sketch);
|
||||
SolvedSketch {
|
||||
feature: 1,
|
||||
name: "sources".into(),
|
||||
plane: solidcraft_geom::Plane::XY,
|
||||
sketch,
|
||||
report,
|
||||
profiles: Vec::new(),
|
||||
ends: Default::default(),
|
||||
}
|
||||
SolvedSketch { feature: 1, name: "sources".into(), plane: solidcraft_geom::Plane::XY, sketch, report, profiles: Vec::new() }
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -253,20 +253,9 @@ impl Document {
|
||||
return self.certified_pieces(body, indices, target.origin_feature);
|
||||
}
|
||||
if let Some(super::FaceCertificate::ExtrudeSide { sketch, curve }) = &target.certificate {
|
||||
let side = |index: usize, exact: bool| matches!(body.body.face_sources().and_then(|sources| sources.get(index)), Some(Some(solidcraft_kernel::FaceSource::CertifiedExtrudeSide { producer, sketch: current_sketch, curve: current_curve, .. })) if *producer == target.origin_feature && current_sketch == sketch && (!exact || current_curve == curve));
|
||||
let count = body.body.face_sources().map_or(0, |sources| sources.len());
|
||||
let mut indices: Vec<_> = (0..count).filter(|index| side(*index, true)).collect();
|
||||
// A side drawn in collinear pieces is one face over all of them (#1788): a certificate
|
||||
// made when the pieces were faces of their own finds it by the piece's name, which
|
||||
// the joined face answers to.
|
||||
if indices.is_empty() {
|
||||
let names = crate::naming::names(body);
|
||||
indices = (0..count)
|
||||
.filter(|index| {
|
||||
side(*index, false) && crate::history_names::matches(&names, *index, &target.name) == Some(crate::history_names::Match::Alias)
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
let indices: Vec<_> = body.body.face_sources().into_iter().flatten().enumerate().filter_map(|(index, source)| {
|
||||
matches!(source, Some(solidcraft_kernel::FaceSource::CertifiedExtrudeSide { producer, sketch: current_sketch, curve: current_curve, .. }) if *producer == target.origin_feature && current_sketch == sketch && current_curve == curve).then_some(index)
|
||||
}).collect();
|
||||
if indices.is_empty() {
|
||||
return Err(DocError::Unknown("constructed side certificate no longer exists".into()));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Construction geometry from the model: references to vertices, edges, faces, planes, axes,
|
||||
//! points and sketch entities, resolved on each evaluation, and the axes, points and planes
|
||||
//! built from them (the CONSTRUCT panel).
|
||||
//! built from them (Fusion's CONSTRUCT panel).
|
||||
//!
|
||||
//! Face and edge references carry the persistent name of what was picked (see `naming`), so
|
||||
//! they follow the model when earlier features change; the picked point is the fallback.
|
||||
@@ -285,9 +285,9 @@ pub(crate) fn line(ctx: &Ctx, g: &GeoRef) -> Result<(Vec3, Vec3)> {
|
||||
GeoRef::Face { at, name } => {
|
||||
let (b, fi, _) = face_of(ctx.st, *at, name)?;
|
||||
match kernel::face_surface(&b.body, fi) {
|
||||
Some(FaceSurface::Cylinder { origin, axis, .. }) => Ok((origin, *axis)),
|
||||
Some(FaceSurface::Cone { apex, axis, .. }) => Ok((apex, *axis)),
|
||||
Some(FaceSurface::Torus { center, axis, .. }) => Ok((center, *axis)),
|
||||
Some(FaceSurface::Cylinder { origin, axis, .. }) => Ok((origin, axis)),
|
||||
Some(FaceSurface::Cone { apex, axis, .. }) => Ok((apex, axis)),
|
||||
Some(FaceSurface::Torus { center, axis, .. }) => Ok((center, axis)),
|
||||
_ => Err(bad("the face is not a cylinder, cone or torus")),
|
||||
}
|
||||
}
|
||||
@@ -367,7 +367,7 @@ fn path_points(ctx: &Ctx, g: &GeoRef) -> Result<Vec<Vec3>> {
|
||||
/// The surface's unit normal at (the point of the surface nearest) `p`, and that point.
|
||||
fn surface_normal(s: FaceSurface, p: Vec3) -> Option<(Vec3, Vec3)> {
|
||||
match s {
|
||||
FaceSurface::Plane { normal, point } => Some((p - normal * (p - point).dot(normal), *normal)),
|
||||
FaceSurface::Plane { normal, point } => Some((p - normal * (p - point).dot(normal), normal)),
|
||||
FaceSurface::Cylinder { origin, axis, radius } => {
|
||||
let w = p - origin;
|
||||
let r = (w - axis * w.dot(axis)).normalized()?;
|
||||
@@ -403,7 +403,7 @@ fn face_surface_at(ctx: &Ctx, g: &GeoRef) -> Result<(FaceSurface, Vec3)> {
|
||||
}
|
||||
GeoRef::Plane { .. } => {
|
||||
let pl = plane(ctx, g)?;
|
||||
Ok((FaceSurface::Plane { normal: pl.normal().unit().ok_or_else(|| bad("degenerate plane"))?, point: pl.origin }, pl.origin))
|
||||
Ok((FaceSurface::Plane { normal: pl.normal(), point: pl.origin }, pl.origin))
|
||||
}
|
||||
_ => Err(bad("pick a face")),
|
||||
}
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
//! The existing length display policy, shared by windowed and headless consumers.
|
||||
//!
|
||||
//! Values are stored in millimetres and displayed in the five design units. Unknown unit
|
||||
//! names retain the millimetre fallback; expression units have a separate, broader vocabulary.
|
||||
//! These functions format values, not validate them: non-finite inputs retain Rust's `NaN`,
|
||||
//! `inf` and `-inf` text. Callers that require finite measurements must validate them first.
|
||||
|
||||
/// Units per millimetre and the unit's name. Unknown names use millimetres.
|
||||
pub fn unit_scale(units: &str) -> (f64, &'static str) {
|
||||
match units {
|
||||
"cm" => (0.1, "cm"),
|
||||
"m" => (0.001, "m"),
|
||||
"in" => (1.0 / 25.4, "in"),
|
||||
"ft" => (1.0 / 304.8, "ft"),
|
||||
_ => (1.0, "mm"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Inch labels retain a meaningful fourth digit without promoting floating-point noise.
|
||||
/// `value` is already converted to the requested display unit, not a millimetre value.
|
||||
pub fn length_precision(value: f64, units: &str, decimals: u8) -> usize {
|
||||
let base = usize::from(decimals.min(8));
|
||||
if units != "in" {
|
||||
return base;
|
||||
}
|
||||
let needs_four = ((value * 10_000.0).round() - (value * 1000.0).round() * 10.0).abs() >= 0.5;
|
||||
base.max(if needs_four { 4 } else { 3 })
|
||||
}
|
||||
|
||||
/// A millimetre length in the requested display unit, without a unit suffix.
|
||||
pub fn length_number(mm: f64, units: &str, decimals: u8) -> String {
|
||||
let value = mm * unit_scale(units).0;
|
||||
format!("{:.*}", length_precision(value, units, decimals), value)
|
||||
}
|
||||
|
||||
/// A length in millimetres in `units`, with `decimals` (for display).
|
||||
pub fn format_length(mm: f64, units: &str, decimals: u8) -> String {
|
||||
let (_, u) = unit_scale(units);
|
||||
format!("{} {u}", length_number(mm, units, decimals))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{format_length, length_number, length_precision, unit_scale};
|
||||
|
||||
#[test]
|
||||
fn all_design_units_keep_bare_and_suffixed_labels() {
|
||||
for (mm, units, bare, suffixed) in [
|
||||
(12.5, "mm", "12.50", "12.50 mm"),
|
||||
(125.0, "cm", "12.50", "12.50 cm"),
|
||||
(12_500.0, "m", "12.50", "12.50 m"),
|
||||
(317.5, "in", "12.500", "12.500 in"),
|
||||
(3810.0, "ft", "12.50", "12.50 ft"),
|
||||
] {
|
||||
assert_eq!(length_number(mm, units, 2), bare, "{units}");
|
||||
assert_eq!(format_length(mm, units, 2), suffixed, "{units}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inch_labels_keep_three_or_four_digits_without_promoting_noise() {
|
||||
for (mm, decimals, expected) in [
|
||||
(25.4, 0, "1.000"),
|
||||
(25.4 * 1.234, 2, "1.234"),
|
||||
(25.4 * 1.2345, 0, "1.2345"),
|
||||
(-25.4 * 1.2345, 3, "-1.2345"),
|
||||
(25.4 * 0.001, 2, "0.001"),
|
||||
(25.4 * (1.0 + 1e-12), 2, "1.000"),
|
||||
(25.4, 4, "1.0000"),
|
||||
(25.4, 5, "1.00000"),
|
||||
(0.0, 0, "0.000"),
|
||||
] {
|
||||
assert_eq!(length_number(mm, "in", decimals), expected, "{mm} mm, {decimals} places");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn precision_uses_the_converted_value_and_caps_requested_places() {
|
||||
assert_eq!(length_precision(1.2345, "in", 2), 4);
|
||||
assert_eq!(length_precision(1.234, "in", 2), 3);
|
||||
assert_eq!(length_precision(1.2345, "mm", 2), 2);
|
||||
assert_eq!(length_precision(1.2345, "in", 6), 6);
|
||||
assert_eq!(length_precision(1.2345, "in", u8::MAX), 8);
|
||||
assert_eq!(length_precision(1.2345, "mm", u8::MAX), 8);
|
||||
assert_eq!(length_number(12.5, "mm", u8::MAX), "12.50000000");
|
||||
assert_eq!(length_number(25.4, "in", u8::MAX), "1.00000000");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rounding_and_signed_zero_keep_the_existing_display_policy() {
|
||||
for (mm, decimals, expected) in [
|
||||
(0.0, 2, "0.00"),
|
||||
(-0.0, 2, "-0.00"),
|
||||
(-0.0001, 2, "-0.00"),
|
||||
(-12.5, 2, "-12.50"),
|
||||
(12.5, 0, "12"),
|
||||
(13.5, 0, "14"),
|
||||
(-12.5, 0, "-12"),
|
||||
(-13.5, 0, "-14"),
|
||||
(1.25, 1, "1.2"),
|
||||
(1.75, 1, "1.8"),
|
||||
] {
|
||||
assert_eq!(length_number(mm, "mm", decimals), expected, "{mm} mm, {decimals} places");
|
||||
}
|
||||
assert_eq!(format_length(-0.0, "in", 2), "-0.000 in");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_display_units_keep_the_millimetre_fallback() {
|
||||
for units in ["", "inch", " in ", "IN", "unsupported"] {
|
||||
assert_eq!(unit_scale(units), (1.0, "mm"), "{units}");
|
||||
assert_eq!(length_number(25.4, units, 2), "25.40", "{units}");
|
||||
assert_eq!(format_length(25.4, units, 2), "25.40 mm", "{units}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_finite_text_is_preserved_without_claiming_validation() {
|
||||
assert_eq!(length_number(f64::NAN, "mm", 2), "NaN");
|
||||
assert_eq!(format_length(f64::INFINITY, "in", u8::MAX), "inf in");
|
||||
assert_eq!(format_length(f64::NEG_INFINITY, "cm", 2), "-inf cm");
|
||||
}
|
||||
}
|
||||
@@ -812,11 +812,11 @@ pub enum FeatureKind {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
line: Option<String>,
|
||||
},
|
||||
/// A construction axis (CONSTRUCT axes).
|
||||
/// A construction axis (Fusion's CONSTRUCT axes).
|
||||
ConstructionAxis {
|
||||
def: AxisDef,
|
||||
},
|
||||
/// A construction point (CONSTRUCT points).
|
||||
/// A construction point (Fusion's CONSTRUCT points).
|
||||
ConstructionPoint {
|
||||
def: PointDef,
|
||||
},
|
||||
@@ -901,8 +901,7 @@ pub enum FeatureKind {
|
||||
targets: Vec<BodyName>,
|
||||
},
|
||||
/// A thin wall from open sketch curves: `thickness` across the sketch plane, filling from
|
||||
/// the curves toward the body (to the next face, or `depth`), or with `normal` along the
|
||||
/// sketch normal. A web makes one per curve.
|
||||
/// the curves toward the body (to the next face, or `depth`). A web makes one per curve.
|
||||
Rib {
|
||||
sketch: SketchId,
|
||||
curves: Vec<String>,
|
||||
@@ -918,10 +917,6 @@ pub enum FeatureKind {
|
||||
/// Web: each curve is its own wall.
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
web: bool,
|
||||
/// Grow the wall along the sketch normal (thickness across the curves, in the sketch
|
||||
/// plane) instead of across the plane from the curves.
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
normal: bool,
|
||||
},
|
||||
/// Move planar faces (at the given points) onto a target plane parallel to them.
|
||||
ReplaceFace {
|
||||
@@ -1465,19 +1460,6 @@ pub enum FeatureKind {
|
||||
#[serde(default = "boundary_samples")]
|
||||
samples: usize,
|
||||
},
|
||||
/// One surface through a curve network (`surface.boundary` with `u` and `v`, issue #763): the
|
||||
/// curves `u` run one way (two or more: the surface lofts through them) and `v` cross them the
|
||||
/// other way (two each way: a Coons patch; more: a Gordon surface through every curve). A side
|
||||
/// may be a point (a three-sided patch). A curve that is a chain of a surface's open edges may
|
||||
/// carry G1 or G2 with the face beside it. `operation` join sews the surface into the body of
|
||||
/// its first edge curve (a solid when it closes the gap there).
|
||||
SurfaceNetwork {
|
||||
u: Vec<NetworkCurve>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
v: Vec<NetworkCurve>,
|
||||
#[serde(default)]
|
||||
operation: Operation,
|
||||
},
|
||||
/// One face of a surface body rebuilt against another body (`surface.match`): the rebuilt
|
||||
/// boundary nearest the chain of `edges` on `target` takes that chain's place and repeats the
|
||||
/// target's own shape along the seam up to `order`, while the boundary opposite it, and the
|
||||
@@ -1705,27 +1687,6 @@ pub struct BlendSide {
|
||||
pub edges: Vec<Position>,
|
||||
}
|
||||
|
||||
/// One curve of a boundary surface's network ([`FeatureKind::SurfaceNetwork`]): a chain of a
|
||||
/// sketch's `curves` (planar or 3D, meeting end to end), a chain of `body`'s edges picked by points
|
||||
/// on them (`edges`), or a side collapsed to the point `point` (the apex of a three-sided patch).
|
||||
/// A chain of a surface's open edges may carry `continuity` with the face beside it (G0, the
|
||||
/// default, G1 or G2).
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, solidcraft_derive::Walk)]
|
||||
pub struct NetworkCurve {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub sketch: Option<SketchId>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub curves: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub body: Option<BodyName>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub edges: Vec<Position>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub point: Option<Position>,
|
||||
#[serde(default = "patch_g0", skip_serializing_if = "is_g0")]
|
||||
pub continuity: Continuity,
|
||||
}
|
||||
|
||||
/// How a patch on a loop of edges meets the faces beside the loop: every edge with `all`, but the
|
||||
/// edges picked in `edges` with their own.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, solidcraft_derive::Walk)]
|
||||
@@ -1767,12 +1728,9 @@ fn is_g0(g: &Continuity) -> bool {
|
||||
/// The continuity a blend repeats by default: the tangent plane on both sides.
|
||||
/// The stations a boundary patch carries by default.
|
||||
fn boundary_samples() -> usize {
|
||||
BOUNDARY_SAMPLES
|
||||
25
|
||||
}
|
||||
|
||||
/// The stations a boundary patch carries when a design does not say.
|
||||
pub const BOUNDARY_SAMPLES: usize = 25;
|
||||
|
||||
fn blend_orders() -> [Continuity; 2] {
|
||||
[Continuity::G1, Continuity::G1]
|
||||
}
|
||||
@@ -1815,10 +1773,6 @@ pub struct SketchPoints {
|
||||
/// Drill the other way: away from the side the holes would go into by themselves.
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub flip: bool,
|
||||
/// A Single hole snapped to this one point (#2110): drilled at the point as it moves, along
|
||||
/// the hole's `direction` (into the face it was placed on) rather than across the sketch.
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub single: bool,
|
||||
}
|
||||
|
||||
/// A component: a node of the design tree that owns sketches, features and bodies. The root
|
||||
@@ -2065,7 +2019,7 @@ impl FeatureKind {
|
||||
)
|
||||
}
|
||||
|
||||
/// Short type name (timeline tooltip, `inspect`).
|
||||
/// Fusion-like type name (timeline tooltip, `inspect`).
|
||||
pub fn type_name(&self) -> &'static str {
|
||||
match self {
|
||||
FeatureKind::BodyTransfer { .. } => "BodyTransferFeature",
|
||||
@@ -2129,7 +2083,7 @@ impl FeatureKind {
|
||||
FeatureKind::SurfaceSweep { .. } => "SurfaceSweepFeature",
|
||||
FeatureKind::SurfaceLoft { .. } => "SurfaceLoftFeature",
|
||||
FeatureKind::SurfaceBlend { .. } => "BlendFeature",
|
||||
FeatureKind::SurfaceBoundary { .. } | FeatureKind::SurfaceNetwork { .. } => "BoundaryFeature",
|
||||
FeatureKind::SurfaceBoundary { .. } => "BoundaryFeature",
|
||||
FeatureKind::SurfaceMatch { .. } => "MatchFeature",
|
||||
FeatureKind::SurfaceFair { .. } => "FairFeature",
|
||||
FeatureKind::Stitch { .. } => "StitchFeature",
|
||||
@@ -2209,7 +2163,7 @@ impl FeatureKind {
|
||||
FeatureKind::SurfaceSweep { .. } => "SurfaceSweep",
|
||||
FeatureKind::SurfaceLoft { .. } => "SurfaceLoft",
|
||||
FeatureKind::SurfaceBlend { .. } => "Blend",
|
||||
FeatureKind::SurfaceBoundary { .. } | FeatureKind::SurfaceNetwork { .. } => "Boundary",
|
||||
FeatureKind::SurfaceBoundary { .. } => "Boundary",
|
||||
FeatureKind::SurfaceMatch { .. } => "Match",
|
||||
FeatureKind::SurfaceFair { .. } => "Fair",
|
||||
FeatureKind::Stitch { .. } => "Stitch",
|
||||
@@ -2424,12 +2378,6 @@ pub struct Visibility {
|
||||
/// Assembly joints by id: their glyphs are not drawn (they still constrain and move).
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub hidden_joints: Vec<u64>,
|
||||
/// Browser folders and groups hidden themselves, by tree key: `<scope>/<folder>` or
|
||||
/// `<scope>/g<group id>`, where the scope is `c0` (the design) or `o:<placement path>`
|
||||
/// (`o:4/9/bodies`). Their items keep their own state: they are drawn again, as they were,
|
||||
/// once the folder is shown.
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub hidden_folders: Vec<String>,
|
||||
}
|
||||
|
||||
impl Visibility {
|
||||
@@ -2441,7 +2389,6 @@ impl Visibility {
|
||||
&& self.hidden_sketches.is_empty()
|
||||
&& self.shown_sketches.is_empty()
|
||||
&& self.hidden_joints.is_empty()
|
||||
&& self.hidden_folders.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3273,7 +3220,7 @@ impl Document {
|
||||
}
|
||||
}
|
||||
|
||||
/// Density of the default material (g/cm³), as the default.
|
||||
/// Density of the default material (g/cm³), as Fusion's default.
|
||||
#[path = "../../../assets/materials/library.rs"]
|
||||
mod materials;
|
||||
pub use materials::{DEFAULT_DENSITY, MATERIALS, material_color};
|
||||
|
||||
+39
-318
@@ -1,15 +1,12 @@
|
||||
//! Timeline evaluation.
|
||||
|
||||
#[path = "placed_mesh.rs"]
|
||||
mod placed_mesh;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
use serde::Serialize;
|
||||
use solidcraft_geom::tess::TessPlan;
|
||||
use solidcraft_geom::{Mesh, Plane, Region2, UnitVec3, Vec2, Vec3, regions_apart};
|
||||
use solidcraft_geom::{Mesh, Plane, Region2, Vec2, Vec3, regions_apart};
|
||||
use solidcraft_kernel::{self as kernel, Body, BoolOp};
|
||||
use solidcraft_sketch::{CurveKind, Profile, SolveReport, find_profiles};
|
||||
|
||||
@@ -31,8 +28,6 @@ pub struct ModelBody {
|
||||
// Timeline snapshots are cloned before meshes are requested. Share the cell itself so
|
||||
// meshing any snapshot also warms every unchanged copy of this body.
|
||||
mesh: Arc<OnceLock<Arc<Mesh>>>,
|
||||
/// Lazy canonical reference triangles for a proper rigid occurrence placement.
|
||||
mesh_placement: Option<Arc<placed_mesh::PlacedMesh>>,
|
||||
/// The display plan (none when the body has no plan: a mesh body, or one the planner
|
||||
/// refuses), shared like the mesh. A GPU viewport draws it.
|
||||
plan: Arc<[OnceLock<Option<Arc<TessPlan>>>; 4]>,
|
||||
@@ -66,7 +61,6 @@ impl ModelBody {
|
||||
body,
|
||||
feature,
|
||||
mesh: Arc::new(OnceLock::new()),
|
||||
mesh_placement: None,
|
||||
plan: Arc::new(Default::default()),
|
||||
plan_mesh: Arc::new(Default::default()),
|
||||
key: 0,
|
||||
@@ -89,10 +83,9 @@ impl ModelBody {
|
||||
// finding their surface, picks naming their face, sketch planes), and a different
|
||||
// triangulation moved those and changed what designs build. A GPU viewport draws the
|
||||
// plan ([`ModelBody::display_plan`]).
|
||||
match self.mesh_placement.as_deref() {
|
||||
Some(placed) => self.mesh.get_or_init(|| placed.mesh()).clone(),
|
||||
None => placed_mesh::reference_mesh(&self.body, &self.mesh, self.display_tol()),
|
||||
}
|
||||
self.mesh
|
||||
.get_or_init(|| Arc::new(self.body.reference_display_mesh_angular(self.display_tol(), kernel::DISPLAY_ANGLE).unwrap_or_default()))
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// The display plan of this body at the viewport's display quality
|
||||
@@ -152,10 +145,9 @@ impl ModelBody {
|
||||
Some((Arc::new(plan), Arc::new(mesh)))
|
||||
}
|
||||
|
||||
/// The chord tolerance of [`ModelBody::mesh`] (mm): 1/1000 of the definition's size, at least
|
||||
/// 1 µm. Proper rigid placements retain that tolerance along with the reference triangles.
|
||||
/// The chord tolerance of [`ModelBody::mesh`] (mm): 1/1000 of the body's size, at least 1 µm.
|
||||
pub fn display_tol(&self) -> f64 {
|
||||
self.mesh_placement.as_ref().map(|placed| placed.tolerance).unwrap_or_else(|| (self.body.size() * 1e-3).max(1e-3))
|
||||
(self.body.size() * 1e-3).max(1e-3)
|
||||
}
|
||||
|
||||
/// Already computed display geometry; saving must not trigger cold tessellation.
|
||||
@@ -173,8 +165,6 @@ pub struct SolvedSketch {
|
||||
pub sketch: solidcraft_sketch::Sketch,
|
||||
pub report: SolveReport,
|
||||
pub profiles: Vec<Profile>,
|
||||
/// Open and untied ends of its profile curves (where a profile can fail).
|
||||
pub ends: solidcraft_sketch::EndReport,
|
||||
}
|
||||
|
||||
/// A cosmetic thread: on which cylinder, its size, and the threaded part of the axis.
|
||||
@@ -187,7 +177,7 @@ pub struct ThreadInfo {
|
||||
pub pitch: f64,
|
||||
pub internal: bool,
|
||||
pub axis_point: Vec3,
|
||||
pub axis: UnitVec3,
|
||||
pub axis: Vec3,
|
||||
/// Threaded part along the axis from `axis_point`.
|
||||
pub start: f64,
|
||||
pub end: f64,
|
||||
@@ -373,10 +363,10 @@ fn thread_groove(t: &ThreadInfo, radius: f64, open: (bool, bool)) -> Result<soli
|
||||
// at the threaded length's end.
|
||||
let (s0, s1) = (if open.0 { t.start - p } else { t.start }, if open.1 { t.end + p } else { t.end });
|
||||
let base = t.axis_point + k * s0;
|
||||
let plane = Plane { origin: base, x: u, y: *k };
|
||||
let plane = Plane { origin: base, x: u, y: k };
|
||||
let section = [Vec2::new(r0, -w0), Vec2::new(r1, -w1), Vec2::new(r1, w1), Vec2::new(r0, w0)];
|
||||
// Sections 7.5° apart: the chords sag 0.2% of the radius.
|
||||
Ok(kernel::sweep_helix_mesh(&plane, §ion, base, *k, p, (s1 - s0) / p, 48)?)
|
||||
Ok(kernel::sweep_helix_mesh(&plane, §ion, base, k, p, (s1 - s0) / p, 48)?)
|
||||
}
|
||||
|
||||
/// A constant fillet of `edges`; with [`FilletFit::Reduce`](crate::FilletFit::Reduce), a radius too
|
||||
@@ -438,12 +428,6 @@ fn one_by_one(input: Traced, edges: &[Vec3], blend: impl Fn(&kernel::Body, Vec3)
|
||||
}
|
||||
}
|
||||
|
||||
/// `Posts` for `Posts (1)`: the body a split piece was numbered after.
|
||||
fn split_piece_of(name: &str) -> Option<&str> {
|
||||
let (base, n) = name.strip_suffix(')')?.rsplit_once(" (")?;
|
||||
(!n.starts_with('0') && n.parse::<u32>().is_ok()).then_some(base)
|
||||
}
|
||||
|
||||
/// Add `note` to a feature's warning.
|
||||
fn append_warning(warning: &mut Option<String>, note: String) {
|
||||
*warning = Some(match warning.take() {
|
||||
@@ -1049,20 +1033,6 @@ fn placement_digest(doc: &Document, f: &Feature) -> u64 {
|
||||
}
|
||||
return hash.finish();
|
||||
}
|
||||
// A network through 3D sketch curves reads their points through the components the
|
||||
// feature and those sketches are placed in, as a sweep's wired path does.
|
||||
if let FeatureKind::SurfaceNetwork { u, v, .. } = &f.kind {
|
||||
let sketches: Vec<crate::SketchId> = u.iter().chain(v).filter_map(|c| c.sketch).collect();
|
||||
if sketches.iter().any(wired) {
|
||||
let mut hash = std::collections::hash_map::DefaultHasher::new();
|
||||
doc.component_transform(f.component).iter().flatten().for_each(|value| value.to_bits().hash(&mut hash));
|
||||
for sketch in &sketches {
|
||||
let owner = doc.feature(sketch.0).map(|feature| feature.component).unwrap_or(0);
|
||||
doc.component_transform(owner).iter().flatten().for_each(|value| value.to_bits().hash(&mut hash));
|
||||
}
|
||||
return hash.finish();
|
||||
}
|
||||
}
|
||||
// To / From Object finds its target where that target's component is placed (#928).
|
||||
let to_object = match &f.kind {
|
||||
FeatureKind::Extrude { extent, .. } => extent.to.is_some() || extent.from.is_some(),
|
||||
@@ -1134,15 +1104,7 @@ impl Model {
|
||||
let Some(last) = self.results.get(i).filter(|r| !r.skipped && r.error.is_none()).and_then(|r| r.state.sketch(feature)) else {
|
||||
return false;
|
||||
};
|
||||
let solved = Arc::new(SolvedSketch {
|
||||
feature,
|
||||
name: last.name.clone(),
|
||||
plane: last.plane,
|
||||
profiles: find_profiles(&sketch),
|
||||
ends: solidcraft_sketch::loose_ends(&sketch),
|
||||
sketch,
|
||||
report,
|
||||
});
|
||||
let solved = Arc::new(SolvedSketch { feature, name: last.name.clone(), plane: last.plane, profiles: find_profiles(&sketch), sketch, report });
|
||||
let shown = self.results.iter().rposition(|r| !r.skipped);
|
||||
for (k, r) in self.results.iter_mut().enumerate().skip(i) {
|
||||
r.stale = true;
|
||||
@@ -1295,7 +1257,7 @@ impl Model {
|
||||
// the face along the hole's axis and never slides sideways to the face's nearest
|
||||
// point when the face no longer reaches it.
|
||||
let hole_axis = match &f.kind {
|
||||
FeatureKind::Hole { direction, points: None | Some(crate::SketchPoints { single: true, .. }), .. } => direction.normalized(),
|
||||
FeatureKind::Hole { direction, points: None, .. } => direction.normalized(),
|
||||
_ => None,
|
||||
};
|
||||
let fst = face_point_state(doc, &state, &f.kind, f.component);
|
||||
@@ -1619,29 +1581,6 @@ fn apply_beside_sheet_cut(st: &mut ModelState, f: &Feature, tools: Vec<Traced>,
|
||||
apply_traced(st, f, tools, op, &rest)
|
||||
}
|
||||
|
||||
/// Only one target meets every separated tool. A tool with no contact must retain
|
||||
/// Join's first-target fallback; tools touching different targets must retain absorption order.
|
||||
fn common_join_target(state: &ModelState, indices: &[usize], tools: &[Traced]) -> Result<Option<usize>> {
|
||||
if indices.len() > 64 {
|
||||
return Ok(None);
|
||||
}
|
||||
let bodies: Vec<_> = tools.iter().map(|tool| tool.body.clone()).collect();
|
||||
let mut contacts = vec![None; tools.len()];
|
||||
for &index in indices {
|
||||
let body = state.bodies.get(index).ok_or_else(|| DocError::Invalid("join target disappeared".into()))?;
|
||||
for (contact, meets) in contacts.iter_mut().zip(body.body.meets_each(&bodies)?) {
|
||||
if meets {
|
||||
if contact.is_some() {
|
||||
return Ok(None);
|
||||
}
|
||||
*contact = Some(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
let common = contacts.first().copied().flatten();
|
||||
Ok(common.filter(|index| contacts.iter().all(|contact| *contact == Some(*index))))
|
||||
}
|
||||
|
||||
fn apply_op(state: &mut ModelState, f: &Feature, tools: Vec<Body>, op: Operation, targets: &[String]) -> Result<()> {
|
||||
apply_traced(state, f, tools.into_iter().map(Traced::unnamed).collect(), op, targets)
|
||||
}
|
||||
@@ -1697,41 +1636,6 @@ fn apply_traced(state: &mut ModelState, f: &Feature, tools: Vec<Traced>, op: Ope
|
||||
}
|
||||
}
|
||||
}
|
||||
// Batching solid targets must preserve Join's absorption order,
|
||||
// sheet material tracking and the separate bodies of a New Body extrusion.
|
||||
if tools.len() > 1
|
||||
&& matches!(f.kind, FeatureKind::Extrude { .. })
|
||||
&& matches!(op, Operation::Join | Operation::Cut)
|
||||
&& !target_idx.is_empty()
|
||||
&& target_idx.iter().all(|i| state.bodies.get(*i).is_some_and(|b| !b.body.is_mesh() && !state.sheets.iter().any(|s| s.body == b.name)))
|
||||
&& let Ok(Some(compound)) = kernel::compound_disjoint_extrusions(&tools.iter().map(|tool| tool.body.clone()).collect::<Vec<_>>())
|
||||
{
|
||||
let target = if target_idx.len() == 1 {
|
||||
target_idx.first().copied()
|
||||
} else if op == Operation::Join {
|
||||
common_join_target(state, &target_idx, &tools).ok().flatten()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// The optimization must not reject geometry the original sequential path can
|
||||
// handle. A failed batch leaves the model untouched and retries the original tools.
|
||||
let batch_targets = if op == Operation::Cut {
|
||||
// Subtracting a union equals subtracting its separated tools in order.
|
||||
// Keep ALL original participants; no sampled contact test excludes a body.
|
||||
Some(target_idx.iter().filter_map(|index| state.bodies.get(*index).map(|body| body.name.clone())).collect::<Vec<_>>())
|
||||
} else {
|
||||
target.and_then(|index| state.bodies.get(index)).map(|body| vec![body.name.clone()])
|
||||
};
|
||||
if let Some(names) = batch_targets {
|
||||
let mut batched = state.clone();
|
||||
// Preserve the original participants. For Join, the compound's larger
|
||||
// size must not expand the contact tolerance and absorb a nearby body.
|
||||
if apply_traced(&mut batched, f, vec![Traced::step(compound, &tools.iter().collect::<Vec<_>>())], op, &names).is_ok() {
|
||||
*state = batched;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
match op {
|
||||
Operation::Join => {
|
||||
// Each tool joins every target it meets into one body, kept under the first one's
|
||||
@@ -1778,9 +1682,8 @@ fn apply_traced(state: &mut ModelState, f: &Feature, tools: Vec<Traced>, op: Ope
|
||||
Operation::Cut | Operation::Intersect => {
|
||||
let bop = if op == Operation::Cut { BoolOp::Cut } else { BoolOp::Intersect };
|
||||
let tools = if op == Operation::Cut { clear_tools_as_one(tools) } else { tools };
|
||||
// Each target's pieces before the cut: a body joined from separate pieces stays one.
|
||||
let affected: std::collections::BTreeMap<String, usize> =
|
||||
target_idx.iter().filter_map(|i| state.bodies.get(*i)).map(|b| (b.name.clone(), b.body.lumps().map_or(1, |l| l.len()))).collect();
|
||||
let affected: std::collections::BTreeSet<String> =
|
||||
target_idx.iter().filter_map(|i| state.bodies.get(*i).map(|b| b.name.clone())).collect();
|
||||
let mut remove = Vec::new();
|
||||
for &ti in &target_idx {
|
||||
let Some(mut acc) = state.bodies.get(ti).map(|b| Some(b.traced())) else { continue };
|
||||
@@ -1808,19 +1711,16 @@ fn apply_traced(state: &mut ModelState, f: &Feature, tools: Vec<Traced>, op: Ope
|
||||
let live: std::collections::BTreeSet<&str> = state.bodies.iter().map(|b| b.name.as_str()).collect();
|
||||
state.threads.retain(|t| live.contains(t.body.as_str()));
|
||||
// A cut can leave a body in pieces: each piece becomes its own body ("Bar", "Bar (1)"),
|
||||
// numbered on past names already taken (a body cut in pieces before). Only a cut that
|
||||
// made more pieces than the body had splits it: the pieces of a join stay together.
|
||||
// Not yet: a cut that splits one piece of such a body still splits all of them; keeping
|
||||
// the untouched ones together needs each new piece traced to the piece it came from.
|
||||
// numbered on past names already taken (a body cut in pieces before).
|
||||
let mut taken: std::collections::BTreeSet<String> = state.bodies.iter().map(|b| b.name.clone()).collect();
|
||||
let mut split: Vec<ModelBody> = Vec::new();
|
||||
for mb in std::mem::take(&mut state.bodies) {
|
||||
let Some(&had) = affected.get(&mb.name).filter(|_| !state.sheets.iter().any(|s| s.body == mb.name)) else {
|
||||
if !affected.contains(&mb.name) || state.sheets.iter().any(|s| s.body == mb.name) {
|
||||
split.push(mb);
|
||||
continue;
|
||||
};
|
||||
}
|
||||
let pieces = mb.body.lumps().unwrap_or_else(|_| vec![mb.body.clone()]);
|
||||
if pieces.len() <= had {
|
||||
if pieces.len() < 2 {
|
||||
split.push(mb);
|
||||
continue;
|
||||
}
|
||||
@@ -2241,46 +2141,6 @@ fn extend_for_coplanar(sur: &Surroundings, plane: &Plane, r: &Region2, lo: f64,
|
||||
(lo2, hi2)
|
||||
}
|
||||
|
||||
/// Remove ends of a join tool already wholly inside its targets. A tab extending
|
||||
/// a rib otherwise shares the rib's side faces through its buried portion; on a
|
||||
/// chamfered base those coincident faces can defeat every boolean retry (#2059).
|
||||
/// Only straight profiles are trimmed, after checking the entire swept material
|
||||
/// against the targets' faces, with the same clearance used when extending ends.
|
||||
fn trim_embedded_join_ends(sur: &Surroundings, plane: &Plane, r: &Region2, lo: f64, hi: f64) -> (f64, f64) {
|
||||
if sur.op != Operation::Join
|
||||
|| !std::iter::once(&r.outer).chain(&r.holes).flat_map(|l| &l.segs).all(|s| matches!(s, solidcraft_geom::Seg2::Line { .. }))
|
||||
{
|
||||
return (lo, hi);
|
||||
}
|
||||
let pieces = r.triangulate(1e-6);
|
||||
let covered: f64 = pieces.iter().map(|[a, b, c]| (*b - *a).cross(*c - *a).abs() * 0.5).sum();
|
||||
if pieces.is_empty() || (covered - r.area()).abs() > 1e-7 * r.area() {
|
||||
return (lo, hi);
|
||||
}
|
||||
let n = plane.normal();
|
||||
let mut levels: Vec<f64> = sur
|
||||
.bodies
|
||||
.iter()
|
||||
.flat_map(|(_, mesh, _)| {
|
||||
mesh.triangles.iter().filter_map(|t| {
|
||||
let [a, b, c] = mesh.tri(t)?;
|
||||
let normal = (b - a).cross(c - a).normalized()?;
|
||||
(normal.dot(n).abs() > 1.0 - 1e-9).then(|| (a - plane.origin).dot(n))
|
||||
})
|
||||
})
|
||||
.filter(|h| *h > lo + sur.skin && *h < hi - sur.skin)
|
||||
.collect();
|
||||
levels.sort_by(f64::total_cmp);
|
||||
levels.dedup_by(|a, b| (*a - *b).abs() < sur.skin);
|
||||
if levels.len() > 256 {
|
||||
return (lo, hi);
|
||||
}
|
||||
let start = levels.iter().rev().copied().find(|h| sur.harmless(plane, &pieces, -sur.skin, lo + sur.skin, h - sur.skin)).unwrap_or(lo);
|
||||
let end =
|
||||
levels.iter().copied().find(|h| *h > start + sur.skin && sur.harmless(plane, &pieces, -sur.skin, h + sur.skin, hi - sur.skin)).unwrap_or(hi);
|
||||
(start, end)
|
||||
}
|
||||
|
||||
/// Whether a tapered tool, run on by `depth` behind the plane it starts in (`dir` the way it
|
||||
/// extrudes), stays where running on changes nothing: in the targets for a join, out of them for
|
||||
/// a cut. Its sides keep their slope, so the section there is the profile grown by up to `widen`
|
||||
@@ -2398,7 +2258,7 @@ fn faces_through(st: &ModelState, p: Vec3, reach: f64) -> Vec<(f64, Vec3, Option
|
||||
}
|
||||
for (f, (d, facet)) in near {
|
||||
let (exact, off) = match kernel::face_surface(&b.body, f as usize) {
|
||||
Some(kernel::FaceSurface::Plane { normal, point }) => (Some(*normal), Some((p - point).dot(normal).abs())),
|
||||
Some(kernel::FaceSurface::Plane { normal, point }) => (Some(normal), Some((p - point).dot(normal).abs())),
|
||||
Some(kernel::FaceSurface::Cylinder { origin, axis, .. }) => {
|
||||
let v = p - origin;
|
||||
((v - axis * v.dot(axis)).normalized(), None)
|
||||
@@ -2687,7 +2547,7 @@ fn sketch_hole_dir(st: &ModelState, spots: &[Vec3], n: Vec3) -> Vec3 {
|
||||
fn hole_spots(vals: &BTreeMap<String, Value>, st: &ModelState, f: &Feature) -> Result<(Vec<Vec3>, Vec3)> {
|
||||
let FeatureKind::Hole { position, at, direction, points, .. } = &f.kind else { return Err(DocError::Invalid("not a hole".into())) };
|
||||
let (spots, dir) = match points {
|
||||
Some(sp) if !sp.single => {
|
||||
Some(sp) => {
|
||||
let ss =
|
||||
st.sketch(sp.sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {} (it must come earlier in the timeline)", sp.sketch)))?;
|
||||
let mut v = Vec::new();
|
||||
@@ -2699,9 +2559,9 @@ fn hole_spots(vals: &BTreeMap<String, Value>, st: &ModelState, f: &Feature) -> R
|
||||
let dir = if sp.flip { -dir } else { dir };
|
||||
(v, dir)
|
||||
}
|
||||
_ => {
|
||||
None => {
|
||||
let dir = direction.normalized().ok_or_else(|| DocError::Invalid("hole direction".into()))?;
|
||||
let at = hole_on_surface(st, single_hole_position(vals, st, position, at, points)?, dir);
|
||||
let at = hole_on_surface(st, hole_position(vals, position, at)?, dir);
|
||||
let dir = hole_dir_into_face(st, at, dir);
|
||||
return Ok((vec![hole_on_surface(st, at, dir)], dir));
|
||||
}
|
||||
@@ -2738,7 +2598,7 @@ fn hole_depth_to(st: &ModelState, at: &Vec3, dir: Vec3, to: &Option<Vec3>, depth
|
||||
fn hole_spots_raw(vals: &BTreeMap<String, Value>, st: &ModelState, f: &Feature) -> Result<(Vec<Vec3>, Vec3)> {
|
||||
let FeatureKind::Hole { position, at, direction, points, .. } = &f.kind else { return Err(DocError::Invalid("not a hole".into())) };
|
||||
match points {
|
||||
Some(sp) if !sp.single => {
|
||||
Some(sp) => {
|
||||
let ss = st.sketch(sp.sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {}", sp.sketch)))?;
|
||||
let v: Vec<Vec3> =
|
||||
sp.ids.iter().filter_map(|id| ss.sketch.points.iter().find(|p| &p.id == id)).map(|p| ss.plane.to_world(p.pos)).take(10_000).collect();
|
||||
@@ -2746,30 +2606,14 @@ fn hole_spots_raw(vals: &BTreeMap<String, Value>, st: &ModelState, f: &Feature)
|
||||
let dir = if sp.flip { -dir } else { dir };
|
||||
Ok((v, dir))
|
||||
}
|
||||
_ => {
|
||||
None => {
|
||||
let dir = direction.normalized().ok_or_else(|| DocError::Invalid("hole direction".into()))?;
|
||||
let p = single_hole_position(vals, st, position, at, points)?;
|
||||
let p = hole_position(vals, position, at)?;
|
||||
Ok((vec![p], hole_dir_into_face(st, hole_on_surface(st, p, dir), dir)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Where a hole placed by position is drilled: at its sketch point where the sketch has it now,
|
||||
/// for a Single hole snapped to one (#2110), else as [`hole_position`].
|
||||
fn single_hole_position(
|
||||
vals: &BTreeMap<String, Value>,
|
||||
st: &ModelState,
|
||||
position: &crate::FacePick,
|
||||
at: &Option<crate::PointExpr>,
|
||||
points: &Option<crate::SketchPoints>,
|
||||
) -> Result<Vec3> {
|
||||
let Some(sp) = points else { return hole_position(vals, position, at) };
|
||||
let ss = st.sketch(sp.sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {} (it must come earlier in the timeline)", sp.sketch)))?;
|
||||
let id = sp.ids.first().ok_or_else(|| DocError::Invalid("a Single hole needs its sketch point".into()))?;
|
||||
let p = ss.sketch.points.iter().find(|p| &p.id == id).ok_or_else(|| DocError::Unknown(format!("sketch point `{id}`")))?;
|
||||
Ok(ss.plane.to_world(p.pos))
|
||||
}
|
||||
|
||||
/// Where a hole placed by position is drilled: its `at` expressions evaluated, so it follows the
|
||||
/// parameters (#1678), else the point it was given.
|
||||
fn hole_position(vals: &BTreeMap<String, Value>, position: &crate::FacePick, at: &Option<crate::PointExpr>) -> Result<Vec3> {
|
||||
@@ -3113,8 +2957,7 @@ fn centered_extrude(plane: &Plane, region: &Region2, start: f64, end: f64, sketc
|
||||
};
|
||||
// Fit cap surfaces in their own coordinates. Fitting a distant world-space
|
||||
// wire can tilt an otherwise exact planar cap through numeric cancellation.
|
||||
// A side drawn in collinear pieces is one face (#1788).
|
||||
let bodies = kernel::extrude_whole_sides(&Plane::XY, std::slice::from_ref(region), start, end)?;
|
||||
let bodies = kernel::extrude(&Plane::XY, std::slice::from_ref(region), start, end)?;
|
||||
if *plane == Plane::XY {
|
||||
return Ok(bodies.into_iter().map(|body| roles.tool(body, None)).collect());
|
||||
}
|
||||
@@ -3225,30 +3068,21 @@ fn feature_tools_with_constructor(
|
||||
};
|
||||
if let Some(tp) = &extent.taper {
|
||||
let taper = val(vals, tp, Kind::Angle)?;
|
||||
if extent.distance2.is_some() || extent.direction == crate::Direction::Symmetric {
|
||||
return Err(DocError::Invalid("not supported yet: taper with two-sided or symmetric extents".into()));
|
||||
if extent.distance2.is_some() || extent.start_offset.is_some() || extent.direction == crate::Direction::Symmetric {
|
||||
return Err(DocError::Invalid("not supported yet: taper with two-sided, symmetric or offset extents".into()));
|
||||
}
|
||||
let sign = if extent.to.is_none() && extent.direction == crate::Direction::Negative { -1.0 } else { 1.0 };
|
||||
let (len, sign) = if d < 0.0 { (-d, -sign) } else { (d, sign) };
|
||||
// Where the extrude starts: the From Object's level and a start offset along the
|
||||
// normal from it. The offset moves the profile that far and the sides are drafted
|
||||
// from there, so the profile stays the section at the start whatever the offset
|
||||
// (`d` is measured from the start already, To Object included).
|
||||
let base = from
|
||||
+ match &extent.start_offset {
|
||||
Some(e) => val(vals, e, Kind::Length)?,
|
||||
None => 0.0,
|
||||
};
|
||||
let start_plane = ss.plane.offset(base);
|
||||
let start_plane = ss.plane.offset(from);
|
||||
let caps = [
|
||||
Some(crate::lineage::extrude_cap_role(base)),
|
||||
Some(crate::lineage::extrude_cap_role(if sign < 0.0 { base - len } else { base + len })),
|
||||
Some(crate::lineage::extrude_cap_role(from)),
|
||||
Some(crate::lineage::extrude_cap_role(if sign < 0.0 { from - len } else { from + len })),
|
||||
];
|
||||
// How far a tool grows past its profile at either end, at most.
|
||||
let mut growth: f64 = 0.0;
|
||||
// A face of the targets in the profile's plane (the same for every profile).
|
||||
let plain_targets: Vec<String> = crate::plain(targets);
|
||||
let on_face = matches!(operation, Operation::Join | Operation::Cut) && face_level_at(st, &ss.plane, base, &plain_targets);
|
||||
let on_face = matches!(operation, Operation::Join | Operation::Cut) && face_level_at(st, &ss.plane, from, &plain_targets);
|
||||
let surroundings = Surroundings::of(st, *operation, &plain_targets);
|
||||
let tools: Vec<Traced> = regions
|
||||
.iter()
|
||||
@@ -3272,9 +3106,9 @@ fn feature_tools_with_constructor(
|
||||
growth = growth.max(len * taper.tan().abs());
|
||||
return Ok(roles.tool(exact, None));
|
||||
}
|
||||
let (lo, hi) = if sign < 0.0 { (base - len, base) } else { (base, base + len) };
|
||||
let (lo, hi) = if sign < 0.0 { (from - len, from) } else { (from, from + len) };
|
||||
let (l2, h2) = surroundings.as_ref().map_or((lo, hi), |sur| extend_for_coplanar(sur, &ss.plane, r, lo, hi));
|
||||
let (mut a, b) = if sign < 0.0 { (base - h2, base - l2) } else { (l2 - base, h2 - base) };
|
||||
let (mut a, b) = if sign < 0.0 { (from - h2, from - l2) } else { (l2 - from, h2 - from) };
|
||||
// A taper that narrows the tool away from the face widens it as it runs on into it, past
|
||||
// the profile (where the profile is the whole face, out through the body's sides, #1534):
|
||||
// then it starts on the face instead.
|
||||
@@ -3345,63 +3179,8 @@ fn feature_tools_with_constructor(
|
||||
// Each tool's extent along the normal (they differ where one is run on into a face).
|
||||
let mut extents: Vec<(f64, f64)> = Vec::with_capacity(regions.len());
|
||||
let plain_targets: Vec<String> = crate::plain(targets);
|
||||
let exact_planar_cut = *operation == Operation::Cut
|
||||
&& ss.sketch.links.len() > 1
|
||||
&& ss.sketch.links.iter().all(|link| matches!(&link.source, solidcraft_sketch::LinkSource::Face { .. }))
|
||||
&& st
|
||||
.bodies
|
||||
.iter()
|
||||
.filter(|body| plain_targets.is_empty() || plain_targets.iter().any(|target| target == "*" || target == &body.name))
|
||||
.all(|body| kernel::is_planar(&body.body));
|
||||
let mut touching_batch = false;
|
||||
// Generated face profiles have full source seams. Check all of them in
|
||||
// one guarded construction; arbitrary sketch profiles retain their existing path.
|
||||
if *operation == Operation::Join
|
||||
&& regions.len() > 1
|
||||
&& ss.sketch.links.len() > 1
|
||||
&& ss.sketch.links.iter().all(|link| matches!(&link.source, solidcraft_sketch::LinkSource::Face { .. }))
|
||||
{
|
||||
let exact = regions
|
||||
.iter()
|
||||
.map(|region| centered_extrude(&ss.plane, region, lo + off, hi + off, Some(&ss)))
|
||||
.collect::<Result<Vec<_>>>()?
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
if let Ok(Some(compound)) = kernel::compound_disjoint_extrusions(&exact.iter().map(|tool| tool.body.clone()).collect::<Vec<_>>())
|
||||
&& st
|
||||
.bodies
|
||||
.iter()
|
||||
.filter(|body| plain_targets.is_empty() || plain_targets.contains(&body.name))
|
||||
.any(|body| kernel::can_join_all_touching_faces(&body.body, &compound))
|
||||
{
|
||||
tools = exact;
|
||||
touching_batch = true;
|
||||
}
|
||||
}
|
||||
let surroundings = if exact_planar_cut || touching_batch { None } else { Surroundings::of(st, *operation, &plain_targets) };
|
||||
let join_surroundings = if *operation == Operation::Join && !touching_batch {
|
||||
// Trimming may discard a tool's material, so only bodies this feature
|
||||
// actually joins can establish that the discarded end is already filled.
|
||||
let participants = if plain_targets.is_empty() { &f.participants } else { &plain_targets };
|
||||
let trim_targets: Vec<String> = st
|
||||
.bodies
|
||||
.iter()
|
||||
.filter(|b| {
|
||||
if participants.is_empty() {
|
||||
b.component == f.component || b.feature == f.id
|
||||
} else {
|
||||
participants.iter().any(|t| t == "*" || t == &b.name)
|
||||
}
|
||||
})
|
||||
.map(|b| b.name.clone())
|
||||
.collect();
|
||||
|
||||
if trim_targets.is_empty() { None } else { Surroundings::of(st, *operation, &trim_targets) }
|
||||
} else {
|
||||
None
|
||||
};
|
||||
for r in regions.iter().filter(|_| !touching_batch) {
|
||||
let surroundings = Surroundings::of(st, *operation, &plain_targets);
|
||||
for r in ®ions {
|
||||
if *operation == Operation::Join {
|
||||
let exact = centered_extrude(&ss.plane, r, lo + off, hi + off, Some(&ss))?;
|
||||
if exact.iter().all(|tool| {
|
||||
@@ -3419,12 +3198,6 @@ fn feature_tools_with_constructor(
|
||||
}
|
||||
}
|
||||
let (l2, h2) = surroundings.as_ref().map_or((lo + off, hi + off), |sur| extend_for_coplanar(sur, &ss.plane, r, lo + off, hi + off));
|
||||
let (l2, h2) = join_surroundings.as_ref().map_or((l2, h2), |sur| {
|
||||
// Trim only material in the requested extent. A run-on added
|
||||
// above to resolve a touching cap must keep doing its job.
|
||||
let (l, h) = trim_embedded_join_ends(sur, &ss.plane, r, lo + off, hi + off);
|
||||
(if l > lo + off { l } else { l2 }, if h < hi + off { h } else { h2 })
|
||||
});
|
||||
let probe = |p: Vec2| (1..4).map(|k| ss.plane.to_world(p) + n * (lo + off + (hi - lo) * k as f64 / 4.0)).collect::<Vec<_>>();
|
||||
let original = r;
|
||||
let grown = |gains: &dyn Fn(&[[Vec2; 3]]) -> bool| match &surroundings {
|
||||
@@ -4364,9 +4137,6 @@ fn replay(
|
||||
continue;
|
||||
}
|
||||
let (op, targets) = feature_op(src);
|
||||
// A primitive's or a hole's targets are its participants (#1108): the copies change
|
||||
// those bodies only, like the feature itself (#2755).
|
||||
let targets = if targets.is_empty() { src.participants.clone() } else { targets };
|
||||
let mut copies = Vec::new();
|
||||
if let FeatureKind::Hole { diameter, depth, hole, to, .. } = &src.kind {
|
||||
// A hole is drilled again at each copy of its spot (found on the surface there), so
|
||||
@@ -4752,7 +4522,6 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
| FeatureKind::SurfaceLoft { .. }
|
||||
| FeatureKind::SurfaceBlend { .. }
|
||||
| FeatureKind::SurfaceBoundary { .. }
|
||||
| FeatureKind::SurfaceNetwork { .. }
|
||||
| FeatureKind::SurfaceMatch { .. }
|
||||
| FeatureKind::SurfaceFair { .. }
|
||||
| FeatureKind::SurfaceOffset { .. }
|
||||
@@ -4807,9 +4576,8 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
}
|
||||
sk.validate_periodic_splines()?;
|
||||
let profiles = find_profiles(&sk);
|
||||
let ends = solidcraft_sketch::loose_ends(&sk);
|
||||
st.sketches.retain(|s| s.feature != f.id);
|
||||
st.sketches.push(Arc::new(SolvedSketch { feature: f.id, name: f.name.clone(), plane, sketch: sk, report, profiles, ends }));
|
||||
st.sketches.push(Arc::new(SolvedSketch { feature: f.id, name: f.name.clone(), plane, sketch: sk, report, profiles }));
|
||||
Ok(())
|
||||
}
|
||||
FeatureKind::Extrude { operation, targets, .. }
|
||||
@@ -5288,29 +5056,11 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
}
|
||||
let mut tb = Vec::new();
|
||||
for t in tools {
|
||||
match st.body(t) {
|
||||
Some(b) => tb.push(b.traced()),
|
||||
// Designs saved before #2247 combined the pieces a cut split off a body that
|
||||
// was already several pieces (`Posts`, `Posts (1)`). A cut keeps them in one
|
||||
// body now: the piece is in `Posts`, which is a tool too or the body joined to.
|
||||
None => match split_piece_of(t) {
|
||||
Some(base)
|
||||
if st.body(base).is_some()
|
||||
&& (tools.iter().any(|o| **o == *base)
|
||||
|| (**target == *base && matches!(operation, Operation::Join | Operation::NewBody))) =>
|
||||
{
|
||||
append_warning(warning, format!("`{t}` is part of `{base}` now"));
|
||||
}
|
||||
_ => return Err(DocError::Unknown(format!("body `{t}`"))),
|
||||
},
|
||||
}
|
||||
tb.push(st.body(t).ok_or_else(|| DocError::Unknown(format!("body `{t}`")))?.traced());
|
||||
}
|
||||
if st.body(target).is_none() {
|
||||
return Err(DocError::Unknown(format!("body `{target}`")));
|
||||
}
|
||||
if tb.is_empty() && !tools.is_empty() {
|
||||
return Ok(()); // the pieces were all joined back into the body they are part of
|
||||
}
|
||||
let op = if *operation == Operation::NewBody { Operation::Join } else { *operation };
|
||||
apply_traced(st, f, tb, op, std::slice::from_ref(&target.0))?;
|
||||
if !keep_tools {
|
||||
@@ -5718,19 +5468,11 @@ impl PlacedBodies {
|
||||
Some((_, placed)) => {
|
||||
let mut fresh = ModelBody::new(name, placed.body.clone(), b.feature);
|
||||
fresh.mesh = placed.mesh.clone();
|
||||
fresh.mesh_placement = placed.mesh_placement.clone();
|
||||
fresh.plan = placed.plan.clone();
|
||||
fresh.plan_mesh = placed.plan_mesh.clone();
|
||||
fresh
|
||||
}
|
||||
None => match kernel::transform_matrix(&b.body, *m) {
|
||||
Ok(body) => {
|
||||
let mut fresh = ModelBody::new(name, body, b.feature);
|
||||
fresh.mesh_placement = placed_mesh::PlacedMesh::new(b, m).map(Arc::new);
|
||||
fresh
|
||||
}
|
||||
Err(_) => ModelBody::new(name, b.body.clone(), b.feature),
|
||||
},
|
||||
None => ModelBody::new(name, kernel::transform_matrix(&b.body, *m).unwrap_or_else(|_| b.body.clone()), b.feature),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5824,7 +5566,7 @@ pub fn world_state_reusing(doc: &Document, st: &ModelState, placed_before: &mut
|
||||
let mut t2 = t.clone();
|
||||
let frame = st.body(&t.body).and_then(|body| doc.model_body_offset(body)).map(|offset| crate::mat_mul(&m, offset)).unwrap_or(m);
|
||||
t2.axis_point = crate::apply_point(&frame, t.axis_point);
|
||||
t2.axis = crate::apply_vector(&frame, *t.axis).unit().unwrap_or(t.axis);
|
||||
t2.axis = crate::apply_vector(&frame, t.axis);
|
||||
out.threads.push(t2);
|
||||
}
|
||||
}
|
||||
@@ -5992,27 +5734,6 @@ mod body_name_tests {
|
||||
mod coplanar_growth_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn buried_join_ends_stop_at_material_boundaries() {
|
||||
let block = kernel::box_solid(Vec3::ZERO, Vec3::new(10.0, 10.0, 8.0)).unwrap();
|
||||
let profile = rect(2.0, 2.0, 4.0, 4.0);
|
||||
let st = state(vec![block.clone()]);
|
||||
let sur = Surroundings::of(&st, Operation::Join, &[]).unwrap();
|
||||
assert_eq!(trim_embedded_join_ends(&sur, &Plane::XY, &profile, 0.0, 16.0), (8.0, 16.0));
|
||||
assert_eq!(trim_embedded_join_ends(&sur, &Plane::XY, &profile, -8.0, 8.0), (-8.0, 0.0));
|
||||
// A thin sealed void between the old start and the rib's top must still
|
||||
// be filled by the join. The remaining tool must start before that void.
|
||||
let void = kernel::box_solid(Vec3::new(1.0, 1.0, 3.01), Vec3::new(5.0, 5.0, 3.02)).unwrap();
|
||||
let hollow = kernel::boolean(&block, &void, BoolOp::Cut).unwrap().unwrap();
|
||||
let st = state(vec![hollow]);
|
||||
let sur = Surroundings::of(&st, Operation::Join, &[]).unwrap();
|
||||
let (lo, hi) = trim_embedded_join_ends(&sur, &Plane::XY, &profile, 0.0, 16.0);
|
||||
assert!(lo <= 3.01, "trimming past {lo} would discard material the join adds in the void");
|
||||
assert_eq!(hi, 16.0);
|
||||
let cut = Surroundings::of(&st, Operation::Cut, &[]).unwrap();
|
||||
assert_eq!(trim_embedded_join_ends(&cut, &Plane::XY, &profile, 0.0, 16.0), (0.0, 16.0));
|
||||
}
|
||||
|
||||
fn state(bodies: Vec<Body>) -> ModelState {
|
||||
ModelState { bodies: bodies.into_iter().enumerate().map(|(i, b)| ModelBody::new(format!("Body{i}"), b, 0)).collect(), ..Default::default() }
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
|
||||
}
|
||||
apply_op(st, f, tools, if *deboss { Operation::Cut } else { Operation::Join }, &crate::plain(targets))
|
||||
}
|
||||
FeatureKind::Rib { sketch, curves, thickness, depth, flip, web, normal } => {
|
||||
FeatureKind::Rib { sketch, curves, thickness, depth, flip, web } => {
|
||||
let ss = st.sketch(sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {sketch}")))?.clone();
|
||||
let t = val(vals, thickness, Kind::Length)?;
|
||||
if !(t > 0.0) {
|
||||
@@ -88,7 +88,7 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
|
||||
}
|
||||
let mut tools = Vec::new();
|
||||
for g in &groups {
|
||||
tools.push(if *normal { rib_tool_normal(st, &ss, g, t, depth, *flip)? } else { rib_tool(st, &ss, g, t, depth, *flip)? });
|
||||
tools.push(rib_tool(st, &ss, g, t, depth, *flip)?);
|
||||
}
|
||||
apply_op(st, f, tools, Operation::Join, &[])
|
||||
}
|
||||
@@ -286,83 +286,6 @@ fn rib_tool(st: &ModelState, ss: &SolvedSketch, ids: &[String], t: f64, depth: O
|
||||
tools.into_iter().next().ok_or_else(|| DocError::Invalid("the rib made no solid".into()))
|
||||
}
|
||||
|
||||
/// A rib wall along the sketch normal: the curves widened to `t` in the sketch plane, run from
|
||||
/// the sketch toward the body by `depth` (or until the whole wall reaches the body).
|
||||
fn rib_tool_normal(st: &ModelState, ss: &SolvedSketch, ids: &[String], t: f64, depth: Option<f64>, flip: bool) -> Result<Body> {
|
||||
let pts = chain_points(ss, ids)?;
|
||||
let len: f64 = pts.windows(2).map(|w| w[0].dist(w[1])).sum();
|
||||
if !(len > 1e-9) {
|
||||
return Err(DocError::Invalid("a zero-length rib curve".into()));
|
||||
}
|
||||
let samples = ((len / (t * 0.5).max(len / 400.0)).ceil() as usize).clamp(2, 400);
|
||||
let along: Vec<Vec3> = (0..=samples).map(|k| ss.plane.to_world(point_on_polyline(&pts, len * k as f64 / samples as f64))).collect();
|
||||
let normal = ss.plane.normal();
|
||||
let reach = st.bodies.iter().map(|b| b.mesh().bounds()).fold(0.0f64, |acc, bb| acc.max((bb.max - bb.min).len()));
|
||||
let inside = |q: Vec3| st.bodies.iter().any(|b| b.mesh().contains(q));
|
||||
// Where the body begins from each point of the curves, along `sign` × the sketch normal.
|
||||
let limit = reach * 2.0 + 1.0;
|
||||
let step = (limit / 400.0).max(1e-3);
|
||||
let hits = |sign: f64| -> Vec<f64> {
|
||||
let mut out = Vec::new();
|
||||
for q in &along {
|
||||
let Some(k) = (1..=400).find(|k| inside(*q + normal * (sign * *k as f64 * step))) else { continue };
|
||||
let (mut lo, mut hi) = ((k - 1) as f64 * step, k as f64 * step);
|
||||
for _ in 0..30 {
|
||||
let m = 0.5 * (lo + hi);
|
||||
if inside(*q + normal * (sign * m)) { hi = m } else { lo = m }
|
||||
}
|
||||
out.push(hi);
|
||||
}
|
||||
out
|
||||
};
|
||||
// Toward the material: the side of the sketch where the curves meet the bodies (unless flipped).
|
||||
let (front, back) = (hits(1.0), hits(-1.0));
|
||||
let mut sign = if back.len() > front.len() { -1.0 } else { 1.0 };
|
||||
let mut reached = if sign > 0.0 { front } else { back };
|
||||
if flip {
|
||||
sign = -sign;
|
||||
reached = hits(sign);
|
||||
}
|
||||
let h = match depth {
|
||||
Some(d) => d,
|
||||
None => {
|
||||
let far = reached.iter().copied().fold(f64::NAN, f64::max);
|
||||
if !far.is_finite() {
|
||||
return Err(DocError::Invalid("the rib curves do not face a body along the sketch normal (give a depth)".into()));
|
||||
}
|
||||
far + (t * 0.5).max(step)
|
||||
}
|
||||
};
|
||||
// The curves widened to `t`: each point offset by half the thickness along the miter of its
|
||||
// neighbours, both sides joined into one band.
|
||||
let half = t / 2.0;
|
||||
let mut left = Vec::with_capacity(pts.len());
|
||||
let mut right = Vec::with_capacity(pts.len());
|
||||
for (i, p) in pts.iter().enumerate() {
|
||||
let prev = i.checked_sub(1).and_then(|j| pts.get(j)).and_then(|q| (*p - *q).normalized());
|
||||
let next = pts.get(i + 1).and_then(|q| (*q - *p).normalized());
|
||||
let (n, scale) = match (prev, next) {
|
||||
(Some(a), Some(b)) => {
|
||||
let m = (a.perp() + b.perp()).normalized().unwrap_or(a.perp());
|
||||
(m, 1.0 / m.dot(a.perp()).max(0.25))
|
||||
}
|
||||
(Some(a), None) | (None, Some(a)) => (a.perp(), 1.0),
|
||||
(None, None) => continue,
|
||||
};
|
||||
left.push(*p + n * (half * scale));
|
||||
right.push(*p - n * (half * scale));
|
||||
}
|
||||
left.extend(right.into_iter().rev());
|
||||
let mut lp = Loop2::polygon(&left);
|
||||
if lp.signed_area() < 0.0 {
|
||||
lp = lp.reversed();
|
||||
}
|
||||
let region = Region2 { outer: lp, holes: Vec::new() };
|
||||
let (a, b) = if sign > 0.0 { (0.0, h) } else { (-h, 0.0) };
|
||||
let tools = kernel::extrude(&ss.plane, &[region], a, b)?;
|
||||
tools.into_iter().next().ok_or_else(|| DocError::Invalid("the rib made no solid".into()))
|
||||
}
|
||||
|
||||
/// The point `d` along a polyline.
|
||||
fn point_on_polyline(pts: &[Vec2], d: f64) -> Vec2 {
|
||||
let mut left = d.max(0.0);
|
||||
|
||||
@@ -151,49 +151,12 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
|
||||
st.sketch(sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?,
|
||||
f.component,
|
||||
)?;
|
||||
// Two curves are the two sides of a bridge. Three or four run round a loop: the Coons
|
||||
// patch of the loop, which lies in the sketch's plane with them, so every order holds.
|
||||
let body = if curves.len() >= 3 {
|
||||
if *crown != 0.0 {
|
||||
return Err(DocError::Invalid(
|
||||
"not supported yet: crown on a patch through three or four curves (it is the flat patch of its sides)".into(),
|
||||
));
|
||||
}
|
||||
loop_patch(&ss, curves)?
|
||||
} else {
|
||||
let boundaries = boundary_frames(&ss, curves, orders, *samples)?;
|
||||
kernel::boundary_patch(&boundaries, *crown, *samples)?
|
||||
};
|
||||
let boundaries = boundary_frames(&ss, curves, orders, *samples)?;
|
||||
let body = kernel::boundary_patch(&boundaries, *crown, *samples)?;
|
||||
let name = new_name(st, f, 0);
|
||||
st.bodies.push(ModelBody::new(name, body, f.id));
|
||||
Ok(())
|
||||
}
|
||||
FeatureKind::SurfaceNetwork { u, v, operation } => {
|
||||
if !matches!(operation, crate::Operation::NewBody | crate::Operation::Join) {
|
||||
return Err(DocError::Invalid("a boundary surface is a new body or joins the body of its edges".into()));
|
||||
}
|
||||
let us = network_sources(doc, st, f, u, "U")?;
|
||||
let vs = network_sources(doc, st, f, v, "V")?;
|
||||
let made = kernel::boundary_surface(&network_curves(&us), &network_curves(&vs))?;
|
||||
if *operation == crate::Operation::Join {
|
||||
// Into the body of the first curve picked on a body's edges.
|
||||
let target = u
|
||||
.iter()
|
||||
.chain(v)
|
||||
.find_map(|c| c.body.clone())
|
||||
.ok_or_else(|| DocError::Invalid("join needs a curve on a body's edges: the surface is sewn into that body".into()))?;
|
||||
let i = st.bodies.iter().position(|b| b.name == target).ok_or_else(|| DocError::Unknown(format!("body `{target}`")))?;
|
||||
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
|
||||
let joined = kernel::boundary_join(&mb.body, &made)?;
|
||||
if let Some(slot) = st.bodies.get_mut(i) {
|
||||
*slot = ModelBody::new(mb.name, joined, mb.feature);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
let name = new_name(st, f, 0);
|
||||
st.bodies.push(ModelBody::new(name, made, f.id));
|
||||
Ok(())
|
||||
}
|
||||
FeatureKind::SurfaceMatch { body, faces, target, edges, order } => {
|
||||
let i = st.bodies.iter().position(|b| &b.name == body).ok_or_else(|| DocError::Unknown(format!("body `{body}`")))?;
|
||||
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
|
||||
@@ -476,135 +439,6 @@ fn push_sheet(st: &mut ModelState, f: &Feature, traced: crate::lineage::Traced)
|
||||
st.bodies.push(ModelBody::traced_new(name, traced, f.id));
|
||||
}
|
||||
|
||||
/// The most curves one direction of a network may list (the kernel's own cap).
|
||||
const MAX_NETWORK_CURVES: usize = solidcraft_geom::network::MAX_NETWORK_CURVES;
|
||||
|
||||
/// A loop of three or four sketch curves as one patch: four give the Coons patch of opposite pairs
|
||||
/// (the first and third one way, the second and fourth the other); three collapse the side
|
||||
/// opposite the first curve to the corner where the other two meet.
|
||||
fn loop_patch(ss: &SolvedSketch, curves: &[String]) -> Result<kernel::Body> {
|
||||
let mut chains: Vec<Vec<Seg2>> = Vec::with_capacity(curves.len());
|
||||
for id in curves {
|
||||
let ci = ss.sketch.curve_index(id).ok_or_else(|| DocError::Unknown(format!("sketch curve `{id}`")))?;
|
||||
let segs = ss.sketch.exact_segs(ci).unwrap_or_else(|| ss.sketch.segs(ci));
|
||||
if segs.is_empty() {
|
||||
return Err(DocError::Invalid(format!("curve `{id}` has no geometry to build on")));
|
||||
}
|
||||
chains.push(segs);
|
||||
}
|
||||
let side = |k: usize| -> Result<kernel::BoundaryCurve<'static>> {
|
||||
let (Some(id), Some(segs)) = (curves.get(k), chains.get(k)) else { return Err(DocError::Invalid("a boundary curve is missing".into())) };
|
||||
Ok(kernel::BoundaryCurve {
|
||||
label: format!("curve `{id}`"),
|
||||
source: kernel::CurveSource::Sketch { plane: ss.plane, segs: segs.clone() },
|
||||
continuity: kernel::Continuity::G0,
|
||||
})
|
||||
};
|
||||
match curves.len() {
|
||||
4 => Ok(kernel::boundary_surface(&[side(0)?, side(2)?], &[side(1)?, side(3)?])?),
|
||||
3 => {
|
||||
// The corner the second and third curves share, whichever way each was drawn.
|
||||
let ends = |k: usize| -> Option<[Vec2; 2]> {
|
||||
let c = chains.get(k)?;
|
||||
Some([c.first()?.start(), c.last()?.end()])
|
||||
};
|
||||
let (Some(b), Some(c)) = (ends(1), ends(2)) else { return Err(DocError::Invalid("a boundary curve is missing".into())) };
|
||||
let mut best = (f64::INFINITY, Vec2::new(0.0, 0.0));
|
||||
for p in b {
|
||||
for q in c {
|
||||
if p.dist(q) < best.0 {
|
||||
best = (p.dist(q), (p + q) * 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
let apex = kernel::BoundaryCurve {
|
||||
label: "the corner opposite the first curve".into(),
|
||||
source: kernel::CurveSource::Point(ss.plane.to_world(best.1)),
|
||||
continuity: kernel::Continuity::G0,
|
||||
};
|
||||
Ok(kernel::boundary_surface(&[side(0)?, apex], &[side(1)?, side(2)?])?)
|
||||
}
|
||||
_ => Err(DocError::Invalid("a loop patch takes three or four curves".into())),
|
||||
}
|
||||
}
|
||||
|
||||
/// A network curve resolved in the feature's frame, owning what it is built from.
|
||||
enum NetSource {
|
||||
Sketch(solidcraft_geom::Plane, Vec<Seg2>),
|
||||
Points(Vec<Vec3>),
|
||||
Edges(kernel::Body, Vec<Vec3>),
|
||||
Point(Vec3),
|
||||
}
|
||||
|
||||
/// The curves of one direction of a network, resolved: each one sketch chain, 3D sketch chain,
|
||||
/// edge chain or point, labelled `U1`, `U2`, … (with `dir` "U") for messages.
|
||||
fn network_sources(
|
||||
doc: &Document,
|
||||
st: &ModelState,
|
||||
f: &Feature,
|
||||
list: &[crate::NetworkCurve],
|
||||
dir: &str,
|
||||
) -> Result<Vec<(String, NetSource, kernel::Continuity)>> {
|
||||
if list.len() > MAX_NETWORK_CURVES {
|
||||
return Err(DocError::Invalid(format!("at most {MAX_NETWORK_CURVES} curves each way")));
|
||||
}
|
||||
let mut out = Vec::with_capacity(list.len());
|
||||
for (k, c) in list.iter().enumerate() {
|
||||
let label = format!("{dir}{}", k + 1);
|
||||
let given = [c.sketch.is_some() || !c.curves.is_empty(), c.body.is_some() || !c.edges.is_empty(), c.point.is_some()];
|
||||
if given.iter().filter(|g| **g).count() != 1 {
|
||||
return Err(DocError::Invalid(format!("{label} must be sketch curves, a body's edges or a point")));
|
||||
}
|
||||
let source = if let Some(p) = c.point {
|
||||
NetSource::Point(p.0)
|
||||
} else if let Some(name) = &c.body {
|
||||
if c.edges.is_empty() {
|
||||
return Err(DocError::Invalid(format!("{label} needs points on the edges of {name}")));
|
||||
}
|
||||
let b = st.body(name).ok_or_else(|| DocError::Unknown(format!("body `{name}`")))?;
|
||||
NetSource::Edges(b.body.clone(), crate::plain(&c.edges))
|
||||
} else {
|
||||
let sk = c.sketch.ok_or_else(|| DocError::Invalid(format!("{label} needs the sketch its curves are in")))?;
|
||||
if c.curves.is_empty() || c.curves.len() > MAX_CHAIN_CURVES {
|
||||
return Err(DocError::Invalid(format!("{label} needs 1 to {MAX_CHAIN_CURVES} curves of its sketch")));
|
||||
}
|
||||
let raw = st.sketch(sk.0).ok_or_else(|| DocError::Unknown(format!("sketch {sk} (it must come earlier in the timeline)")))?;
|
||||
// 3D sketch curves are followed through their points; planar ones exactly.
|
||||
let mapped = path_in_frame(doc, raw, f.component)?;
|
||||
match path_points(&mapped, &c.curves, Vec3::new(f64::NAN, f64::NAN, f64::NAN))? {
|
||||
Some(pts) => NetSource::Points(pts),
|
||||
None => {
|
||||
let ss = sketch_in_feature_frame(doc, raw, f.component)?;
|
||||
let mut chains = curve_chains(&ss, &c.curves)?.into_iter();
|
||||
let (Some(chain), None) = (chains.next(), chains.next()) else {
|
||||
return Err(DocError::Invalid(format!("{label}: its curves must meet end to end in one chain")));
|
||||
};
|
||||
NetSource::Sketch(ss.plane, chain)
|
||||
}
|
||||
}
|
||||
};
|
||||
out.push((label, source, c.continuity));
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The kernel's curves over resolved sources (borrowing their bodies).
|
||||
fn network_curves(sources: &[(String, NetSource, kernel::Continuity)]) -> Vec<kernel::BoundaryCurve<'_>> {
|
||||
sources
|
||||
.iter()
|
||||
.map(|(label, src, g)| kernel::BoundaryCurve {
|
||||
label: label.clone(),
|
||||
source: match src {
|
||||
NetSource::Sketch(plane, segs) => kernel::CurveSource::Sketch { plane: *plane, segs: segs.clone() },
|
||||
NetSource::Points(pts) => kernel::CurveSource::Points(pts.clone()),
|
||||
NetSource::Edges(body, at) => kernel::CurveSource::Edges { body, at: at.clone() },
|
||||
NetSource::Point(p) => kernel::CurveSource::Point(*p),
|
||||
},
|
||||
continuity: *g,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The boundaries `surface.boundary` builds its patch from: each picked curve's own stations in
|
||||
/// the sketch's frame, every one of them leaving the boundary square into the loop — the direction
|
||||
/// from the curve towards the middle of the loop, taken across its tangent — over the plane the
|
||||
|
||||
@@ -395,7 +395,7 @@ fn lex_spans(s: &str) -> Result<Vec<(Tok, usize, usize)>, DocError> {
|
||||
out.push((Tok::Op('-'), at(i), at(i + 1)));
|
||||
i += 1;
|
||||
} else if c == ';' {
|
||||
// Some CAD tools separate function arguments with `;` in some locales.
|
||||
// Fusion separates function arguments with `;` in some locales.
|
||||
out.push((Tok::Op(','), at(i), at(i + 1)));
|
||||
i += 1;
|
||||
} else if "+-*/^(),%".contains(c) {
|
||||
|
||||
@@ -164,10 +164,6 @@ pub(super) fn fixtures() -> Vec<(&'static str, Value)> {
|
||||
// The Class-A surface family (surface.blend/boundary/match/fair).
|
||||
("surface_blend", json!({"type": "surface_blend", "from": {"body": "B", "edges": [o]}, "to": {"body": "C", "edges": [o]}, "align": [o, o]})),
|
||||
("surface_boundary", json!({"type": "surface_boundary", "sketch": 1, "curves": ["l1", "l2"]})),
|
||||
(
|
||||
"surface_network",
|
||||
json!({"type": "surface_network", "u": [{"sketch": 1, "curves": ["l1"]}, {"body": "B", "edges": [o], "continuity": "g1"}], "v": [{"sketch": 2, "curves": ["l1"]}, {"point": o}], "operation": "join"}),
|
||||
),
|
||||
("surface_match", json!({"type": "surface_match", "body": "B", "faces": [o], "target": "C", "edges": [o]})),
|
||||
("surface_fair", json!({"type": "surface_fair", "body": "B", "faces": [o], "tolerance": "sfair.tolerance"})),
|
||||
("unstitch", json!({"type": "unstitch", "body": "B"})),
|
||||
|
||||
@@ -342,7 +342,6 @@ surface_offset
|
||||
expressions: soff.distance, soff.tolerance
|
||||
surface_blend
|
||||
surface_boundary
|
||||
surface_network
|
||||
surface_match
|
||||
surface_fair
|
||||
inputs:
|
||||
|
||||
@@ -157,8 +157,6 @@ surface_offset
|
||||
surface_blend
|
||||
moves: align[0][0], align[0][1], align[0][2], align[1][0], align[1][1], align[1][2], from.edges[0][0], from.edges[0][1], from.edges[0][2], to.edges[0][0], to.edges[0][1], to.edges[0][2]
|
||||
surface_boundary
|
||||
surface_network
|
||||
moves: u[1].edges[0][0], u[1].edges[0][1], u[1].edges[0][2], v[1].point[0], v[1].point[1], v[1].point[2]
|
||||
surface_match
|
||||
moves: edges[0][0], edges[0][1], edges[0][2], faces[0][0], faces[0][1], faces[0][2]
|
||||
face points: [0, 0, 0]
|
||||
|
||||
@@ -182,10 +182,6 @@ surface_blend
|
||||
surface_boundary
|
||||
sketches: [1]
|
||||
remap_refs: sketch
|
||||
surface_network
|
||||
sketches: [1, 2]
|
||||
remap_refs: u[0].sketch, v[0].sketch
|
||||
remap_bodies: u[1].body
|
||||
surface_match
|
||||
remap_bodies: body
|
||||
surface_fair
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
//! - 5: joins, cuts and intersects change only their own component's bodies unless bodies are
|
||||
//! named as `participants`; older features in designs with components get `participants:
|
||||
//! ["*"]` (every body), which is what they did, so those designs keep their shape.
|
||||
//! - 4: joint origins mate by default (B's z against A's); `flip` now means
|
||||
//! - 4: joint origins mate by default (B's z against A's, as Fusion does); `flip` now means
|
||||
//! "don't mate". Older joints keep their placement: their `flip` is inverted.
|
||||
//! - 6: XZ origin planes use sketch y = -Z and normal +Y. Older XZ references become
|
||||
//! explicit planes retaining their previous axes, including offset-plane expressions.
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
//! A joint keeps the expressions its offset, angle and values were typed as (`exprs`) and works
|
||||
//! them out again whenever the design changes, so it follows the parameters they name. Older
|
||||
//! designs need no transformation: a joint without `exprs` keeps its numbers as before. The last
|
||||
//! release refuses a design saved with this (the newer format number).
|
||||
@@ -1,4 +0,0 @@
|
||||
//! A joint origin can sit on an edge of a planar face (`Snap::Edge`, saved as `"type": "edge"`):
|
||||
//! at the edge's middle or at a corner where it meets another edge, all three found again by name.
|
||||
//! Older designs need no transformation: they have no such origins. The last release refuses a
|
||||
//! design saved with this (the newer format number).
|
||||
@@ -1,5 +0,0 @@
|
||||
//! A face or circle joint origin given no `x` reference keeps the x its frame had when picked
|
||||
//! (`x` as a direction, component coordinates) and lays it onto the face as it is now, so a part
|
||||
//! on a face that tilts keeps its turn. Older designs need no transformation: an origin without
|
||||
//! `x` works its frame out from the normal alone, as before. The last release refuses a design
|
||||
//! saved with this (the newer format number).
|
||||
@@ -1,7 +0,0 @@
|
||||
//! A `SurfaceNetwork` (Boundary Surface through a curve network, #763) saves `u` and `v`, each a
|
||||
//! list of curves: a `sketch` and its `curves`, a `body` and points on its `edges`, or a `point`
|
||||
//! (the apex of a three-sided patch), with an optional `continuity` (`g1` or `g2` with the face
|
||||
//! beside an edge chain), and an `operation` (`new_body`, or `join` into the body of its edges).
|
||||
//! A `SurfaceBoundary` of three or four curves, which older builds refused, now builds and saves
|
||||
//! as it always did. Older designs need no transformation; the last release asks for an update
|
||||
//! instead of meeting a feature it can't read.
|
||||
@@ -1,5 +0,0 @@
|
||||
//! A browser folder or group can be hidden on its own (`visibility.hidden_folders`, tree keys
|
||||
//! such as `c0/bodies`, `o:4/sketches` or `c0/g7`): its items keep their own hidden state and
|
||||
//! are drawn again as they were once it is shown. Older designs need no transformation: they
|
||||
//! hide no folder, and a folder's eye then follows its own state. The last release asks for an
|
||||
//! update instead of opening a design with this format number.
|
||||
@@ -1,5 +0,0 @@
|
||||
//! A cut keeps a body that is several separate pieces (posts joined while apart) as one body; it
|
||||
//! splits off only the pieces the cut itself made (#2247). Older designs need no transformation:
|
||||
//! where they named a piece the cut used to split off next to its body in a combine (tools
|
||||
//! `Posts`, `Posts (1)`), the combine takes `Posts` and warns that the piece is part of it. 0.15.0
|
||||
//! asks for an update instead of splitting the pieces apart again.
|
||||
@@ -1,8 +0,0 @@
|
||||
//! A face-centre or circle-centre joint origin can carry `x`: what turns its frame about z (a
|
||||
//! direction, `[x, y, z]`, or `{face, pick?}`: a face by its persistent name), and a
|
||||
//! circle-centre one `end`: the name of the face across the rim at the end it takes, so a part
|
||||
//! stays on the same side of a hole when the design changes. A named face is now resolved by its
|
||||
//! own plane or cylinder rather than the face nearest the pick. Older designs need no
|
||||
//! transformation: an origin without `end` takes the end nearer its pick, and one without `x`
|
||||
//! any direction square to z, as before; 0.14.x asks for an update instead of dropping a saved
|
||||
//! end or turn.
|
||||
@@ -1,4 +0,0 @@
|
||||
//! A Rib or Web can grow along the sketch normal (`normal: true`): the wall is `thickness` wide
|
||||
//! across its curves in the sketch plane and runs from the sketch toward the body. Older designs
|
||||
//! need no transformation: a rib without `normal` grows across the sketch plane as before; 0.14.x
|
||||
//! asks for an update instead of building a normal rib in the plane.
|
||||
@@ -1,4 +0,0 @@
|
||||
//! A Single hole picked at a sketch point keeps that point (`points` with `single: true`) and is
|
||||
//! drilled where the point is after the sketch changes, along its own direction into the face
|
||||
//! (#2110). Older designs need no transformation: a Single hole without it stays at the position
|
||||
//! it was placed at, as before; 0.15.x asks for an update instead of opening one that has it.
|
||||
+39
-361
@@ -36,43 +36,18 @@ pub enum Snap {
|
||||
x: Option<Vec3>,
|
||||
},
|
||||
/// The centre of the planar face nearest `pick`, z along its outward normal. `face` is the
|
||||
/// face's persistent name: when it names a planar face, that face's own plane and centre are
|
||||
/// taken (the pick only tells the pieces of a split face apart).
|
||||
/// face's persistent name (found first).
|
||||
FaceCenter {
|
||||
pick: Vec3,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
face: Option<String>,
|
||||
/// What turns the frame about z (default: any direction square to z).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
x: Option<XRef>,
|
||||
},
|
||||
/// The centre of a circular end of the cylindrical face at `pick`, z along its axis
|
||||
/// (pointing out of the face's end). `face` is the face's persistent name: when it names a
|
||||
/// cylindrical face, that face's own axis, radius and extent are taken. `end` is the name of
|
||||
/// the face across the rim at the chosen end (the plate's face a hole opens onto), so the
|
||||
/// end stays the same when the design changes; without it (or when neither end has that
|
||||
/// face any more), the end nearer `pick` along the axis.
|
||||
/// The centre of the circular end nearest `pick` of the cylindrical face there, z along its
|
||||
/// axis (pointing out of the face's end).
|
||||
CircleCenter {
|
||||
pick: Vec3,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
face: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
end: Option<String>,
|
||||
/// What turns the frame about z (default: any direction square to z).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
x: Option<XRef>,
|
||||
},
|
||||
/// A point on an edge of a planar face: the edge's middle, or the corner where it meets the
|
||||
/// edge `corner` names (`joints_edge.rs`). `edge` and `face` are the persistent names of the
|
||||
/// edge and the face (found first); `pick` is a point on the edge as it was picked.
|
||||
Edge {
|
||||
pick: Vec3,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
edge: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
face: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
corner: Option<String>,
|
||||
},
|
||||
/// A point, z along `z` (default +Z).
|
||||
Point {
|
||||
@@ -82,103 +57,6 @@ pub enum Snap {
|
||||
},
|
||||
}
|
||||
|
||||
/// What sets a joint origin's x axis (its turn about z): a direction, or a face found by its
|
||||
/// persistent name, so a round origin keeps its turn when the design changes.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum XRef {
|
||||
/// A direction (component coordinates); its part square to z is taken.
|
||||
Dir(Vec3),
|
||||
/// A face: a plane's outward normal, a cylinder's way from the origin to its axis (another
|
||||
/// hole), any other face's way from the origin to the point of it nearest `pick` (component
|
||||
/// coordinates; it also tells the pieces of a split face apart, default the origin).
|
||||
Face {
|
||||
face: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pick: Option<Vec3>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Snap {
|
||||
/// The persistent face and edge names the snap refers to (its face, its end's, its x reference's;
|
||||
/// an edge origin's edge, face and corner edge).
|
||||
pub fn face_names(&self) -> Vec<&String> {
|
||||
let mut out = Vec::new();
|
||||
if let Snap::FaceCenter { face, x, .. } | Snap::CircleCenter { face, x, .. } = self {
|
||||
out.extend(face.iter());
|
||||
if let Some(XRef::Face { face, .. }) = x {
|
||||
out.push(face);
|
||||
}
|
||||
}
|
||||
if let Snap::CircleCenter { end: Some(end), .. } = self {
|
||||
out.push(end);
|
||||
}
|
||||
if let Snap::Edge { edge, face, corner, .. } = self {
|
||||
out.extend(edge.iter().chain(face.iter()).chain(corner.iter()));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// [`Snap::face_names`], to rewrite them (a copied component's faces have new names).
|
||||
pub fn face_names_mut(&mut self) -> Vec<&mut String> {
|
||||
let mut out = Vec::new();
|
||||
match self {
|
||||
Snap::FaceCenter { face, x, .. } => {
|
||||
out.extend(face.iter_mut());
|
||||
if let Some(XRef::Face { face, .. }) = x {
|
||||
out.push(face);
|
||||
}
|
||||
}
|
||||
Snap::CircleCenter { face, end, x, .. } => {
|
||||
out.extend(face.iter_mut());
|
||||
out.extend(end.iter_mut());
|
||||
if let Some(XRef::Face { face, .. }) = x {
|
||||
out.push(face);
|
||||
}
|
||||
}
|
||||
Snap::Edge { edge, face, corner, .. } => out.extend(edge.iter_mut().chain(face.iter_mut()).chain(corner.iter_mut())),
|
||||
_ => {}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The direction a face or circle snap keeps as its frame's x (`XRef::Dir`), in the
|
||||
/// component's coordinates; `None` for a face reference or none.
|
||||
pub fn frame_x(&self) -> Option<Vec3> {
|
||||
match self {
|
||||
Snap::FaceCenter { x: Some(XRef::Dir(d)), .. } | Snap::CircleCenter { x: Some(XRef::Dir(d)), .. } => Some(*d),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// A face or circle snap given no x reference keeps `x` (the frame's x as it is now): each
|
||||
/// solve lays it onto the face, or square to the hole's axis, as the geometry is then, so a
|
||||
/// part on a face that tilts keeps its turn about z. Without it (designs made before) x
|
||||
/// follows from z alone, and turns through a right angle where z crosses 0.9 along the
|
||||
/// component's X. A snap with a reference keeps it.
|
||||
pub fn with_frame_x(mut self, x: Vec3) -> Snap {
|
||||
if let Snap::FaceCenter { x: slot @ None, .. } | Snap::CircleCenter { x: slot @ None, .. } = &mut self {
|
||||
*slot = Some(XRef::Dir(x));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Whether every point and direction in the snap is finite.
|
||||
pub fn is_finite(&self) -> bool {
|
||||
let x_ok = |x: &Option<XRef>| match x {
|
||||
Some(XRef::Dir(d)) => d.is_finite(),
|
||||
Some(XRef::Face { pick, .. }) => pick.is_none_or(|p| p.is_finite()),
|
||||
None => true,
|
||||
};
|
||||
match self {
|
||||
Snap::Frame { origin, z, x } => origin.is_finite() && z.is_finite() && x.is_none_or(|x| x.is_finite()),
|
||||
Snap::FaceCenter { pick, x, .. } | Snap::CircleCenter { pick, x, .. } => pick.is_finite() && x_ok(x),
|
||||
Snap::Point { point, z } => point.is_finite() && z.is_none_or(|z| z.is_finite()),
|
||||
Snap::Edge { pick, .. } => pick.is_finite(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A joint origin: a snap on an occurrence's component (occurrence 0 = the root design).
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct JointOrigin {
|
||||
@@ -260,92 +138,6 @@ pub struct Joint {
|
||||
pub angle: f64,
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub suppressed: bool,
|
||||
/// The expressions the offset, the angle and the values were typed as: they are worked out
|
||||
/// again whenever the design changes, so a joint follows the parameters it is defined by.
|
||||
#[serde(default, skip_serializing_if = "JointExprs::is_empty")]
|
||||
pub exprs: JointExprs,
|
||||
}
|
||||
|
||||
/// Expressions a joint's numbers follow (over the design's parameters): the offset (a length),
|
||||
/// the angle and each value (angles or lengths as the joint's values are). A number typed in
|
||||
/// place of one drops it.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct JointExprs {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub offset: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub angle: Option<String>,
|
||||
/// By value index, as in `Joint::values`.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub values: Vec<Option<String>>,
|
||||
}
|
||||
|
||||
impl JointExprs {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.offset.is_none() && self.angle.is_none() && self.values.iter().all(Option::is_none)
|
||||
}
|
||||
|
||||
/// The expression value `i` follows.
|
||||
pub fn value(&self, i: usize) -> Option<&str> {
|
||||
self.values.get(i).and_then(Option::as_deref)
|
||||
}
|
||||
|
||||
/// Value `i` follows `e` (or nothing).
|
||||
pub fn set_value(&mut self, i: usize, e: Option<String>) {
|
||||
if e.is_some() && self.values.len() <= i {
|
||||
self.values.resize(i + 1, None);
|
||||
}
|
||||
if let Some(slot) = self.values.get_mut(i) {
|
||||
*slot = e;
|
||||
}
|
||||
while self.values.last().is_some_and(Option::is_none) {
|
||||
self.values.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Largest offset or value an expression may give a joint (mm, or radians for an angle).
|
||||
const MAX_EXPR_VALUE: f64 = 1e9;
|
||||
|
||||
/// A joint's offset, angle and values worked out from its expressions (those without one keep
|
||||
/// their numbers), or why one can't be: an unknown name, a wrong unit, or a number too large.
|
||||
pub fn expressed(doc: &Document, j: &Joint) -> Result<(f64, f64, Vec<f64>)> {
|
||||
let eval = |e: &str, kind: crate::expr::Kind, what: &str| -> Result<f64> {
|
||||
let v = doc.eval(e, kind).map_err(|err| DocError::Invalid(format!("joint {}: {what} `{e}`: {err}", j.name)))?;
|
||||
if v.is_finite() && v.abs() < MAX_EXPR_VALUE { Ok(v) } else { Err(DocError::Invalid(format!("joint {}: {what} `{e}` gives {v}", j.name))) }
|
||||
};
|
||||
let x = &j.exprs;
|
||||
let offset = match &x.offset {
|
||||
Some(e) => eval(e, crate::expr::Kind::Length, "offset")?,
|
||||
None => j.offset,
|
||||
};
|
||||
let angle = match &x.angle {
|
||||
Some(e) => eval(e, crate::expr::Kind::Angle, "angle")?,
|
||||
None => j.angle,
|
||||
};
|
||||
let mut values = j.values.clone();
|
||||
for (i, e) in x.values.iter().enumerate().take(j.kind.dofs().len()) {
|
||||
let Some(e) = e else { continue };
|
||||
let kind = if j.kind.is_angle(i) { crate::expr::Kind::Angle } else { crate::expr::Kind::Length };
|
||||
let name = j.kind.dofs().get(i).copied().unwrap_or("value");
|
||||
let v = eval(e, kind, name)?;
|
||||
if values.len() <= i {
|
||||
values.resize(i + 1, 0.0);
|
||||
}
|
||||
if let Some(slot) = values.get_mut(i) {
|
||||
*slot = v;
|
||||
}
|
||||
}
|
||||
Ok((offset, angle, values))
|
||||
}
|
||||
|
||||
/// Values the loop solver leaves alone: those a motion link sets and those an expression does.
|
||||
fn set_values(asm: &Assembly) -> BTreeSet<(u64, usize)> {
|
||||
let mut set: BTreeSet<(u64, usize)> = asm.links.iter().map(|l| (l.b, l.ib)).collect();
|
||||
for j in &asm.joints {
|
||||
set.extend(j.exprs.values.iter().enumerate().filter(|(_, e)| e.is_some()).map(|(i, _)| (j.id, i)));
|
||||
}
|
||||
set
|
||||
}
|
||||
|
||||
/// Value of joint `b` (index `ib`) = ratio · value of joint `a` (index `ia`) + offset, or for a
|
||||
@@ -465,7 +257,7 @@ fn size_of(doc: &Document, st: &ModelState, s: &Size) -> Result<f64> {
|
||||
let v = match s {
|
||||
Size::Length(v) => *v,
|
||||
Size::Expr(e) => doc.eval(e, crate::expr::Kind::Length)?,
|
||||
Size::Circle(o) => circle_at(doc, st, o)?.radius,
|
||||
Size::Circle(o) => circle_at(doc, st, o)?.1.radius,
|
||||
};
|
||||
if v.is_finite() && v > 1e-9 && v <= MAX_SIZE {
|
||||
Ok(v)
|
||||
@@ -532,49 +324,6 @@ pub struct Assembly {
|
||||
}
|
||||
|
||||
impl Assembly {
|
||||
/// Every expression the joints and motion links keep, with the joint (or the driven joint of
|
||||
/// a link) and what it sets: `offset`, `angle`, a value's name, or a coupling's length.
|
||||
pub fn exprs(&self) -> Vec<(u64, String, &str)> {
|
||||
let mut out = Vec::new();
|
||||
for j in &self.joints {
|
||||
let x = &j.exprs;
|
||||
out.extend(x.offset.as_deref().map(|e| (j.id, "offset".to_string(), e)));
|
||||
out.extend(x.angle.as_deref().map(|e| (j.id, "angle".to_string(), e)));
|
||||
for (i, e) in x.values.iter().enumerate() {
|
||||
if let Some(e) = e {
|
||||
out.push((j.id, j.kind.dofs().get(i).copied().unwrap_or("value").to_string(), e.as_str()));
|
||||
}
|
||||
}
|
||||
}
|
||||
for l in &self.links {
|
||||
for s in l.coupling.as_ref().map(Coupling::sizes).unwrap_or_default() {
|
||||
if let Size::Expr(e) = s {
|
||||
out.push((l.b, "coupling".to_string(), e.as_str()));
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The expressions of `exprs`, to rewrite (a renamed parameter).
|
||||
pub fn exprs_mut(&mut self) -> Vec<&mut String> {
|
||||
let mut out = Vec::new();
|
||||
for j in &mut self.joints {
|
||||
let x = &mut j.exprs;
|
||||
out.extend(x.offset.as_mut());
|
||||
out.extend(x.angle.as_mut());
|
||||
out.extend(x.values.iter_mut().flatten());
|
||||
}
|
||||
for l in &mut self.links {
|
||||
for s in l.coupling.as_mut().map(Coupling::sizes_mut).unwrap_or_default() {
|
||||
if let Size::Expr(e) = s {
|
||||
out.push(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.joints.is_empty()
|
||||
&& self.origins.is_empty()
|
||||
@@ -636,85 +385,58 @@ fn component_of(doc: &Document, occ: u64) -> Result<u64> {
|
||||
doc.occurrences.iter().find(|o| o.id == occ).map(|o| o.component).ok_or_else(|| DocError::Unknown(format!("occurrence {occ}")))
|
||||
}
|
||||
|
||||
/// Resolve a joint origin to a frame in its component's coordinates.
|
||||
/// A snap's pick point moved onto its named face (an error when the face is gone: a joint must
|
||||
/// not quietly attach elsewhere).
|
||||
fn named_pick(doc: &Document, st: &ModelState, pick: Vec3, face: Option<&str>) -> Result<Vec3> {
|
||||
fn named_pick(st: &ModelState, pick: Vec3, face: Option<&str>) -> Result<Vec3> {
|
||||
match face {
|
||||
Some(n) => face::named_face(doc, st, pick, n).map(|f| f.point),
|
||||
Some(n) => crate::naming::point_on_face(st, n, pick)
|
||||
.map(|(q, _)| q)
|
||||
.ok_or_else(|| DocError::Invalid(format!("joint origin: the face `{n}` no longer exists (edit the joint to pick again)"))),
|
||||
None => Ok(pick),
|
||||
}
|
||||
}
|
||||
|
||||
/// The centre and outward normal of the planar face nearest `pick` (component coordinates; a
|
||||
/// body moved in from another component placed by its offset, as a named face is).
|
||||
fn nearest_plane(doc: &Document, st: &ModelState, o: &JointOrigin, pick: Vec3) -> Result<(Vec3, Vec3)> {
|
||||
let comp = component_of(doc, o.occurrence)?;
|
||||
let mut best: Option<(f64, Vec3, Vec3)> = None;
|
||||
for b in component_bodies(doc, st, comp) {
|
||||
let offset = doc.model_body_offset(b);
|
||||
let tol = (b.body.size() * 2e-3).max(1e-3);
|
||||
for f in b.body.faces(tol)? {
|
||||
let Some(n) = f.plane_normal else { continue };
|
||||
let (c, n) = match offset {
|
||||
Some(m) => (apply_point(m, f.centroid), apply_vector(m, n)),
|
||||
None => (f.centroid, n),
|
||||
};
|
||||
let d = (pick - c).dot(n).abs() + 1e-3 * (pick - c).len();
|
||||
if best.as_ref().is_none_or(|(bd, _, _)| d < *bd) {
|
||||
best = Some((d, c, n));
|
||||
}
|
||||
}
|
||||
}
|
||||
best.map(|(_, c, n)| (c, n)).ok_or_else(|| DocError::Invalid("joint origin: no planar face there".into()))
|
||||
}
|
||||
|
||||
/// Resolve a joint origin to a frame in its component's coordinates.
|
||||
pub fn resolve_origin(doc: &Document, st: &ModelState, o: &JointOrigin) -> Result<Mat> {
|
||||
let bad = || DocError::Invalid("joint origin: degenerate frame".into());
|
||||
match &o.snap {
|
||||
Snap::Frame { origin, z, x } => frame(*origin, *z, *x).ok_or_else(bad),
|
||||
Snap::Point { point, z } => frame(*point, z.unwrap_or(Vec3::Z), None).ok_or_else(bad),
|
||||
Snap::FaceCenter { pick, face, x } => {
|
||||
// A named planar face gives its own centre and normal; another face (or none) the
|
||||
// planar face nearest the point.
|
||||
let named = match face.as_deref() {
|
||||
Some(n) => Some(face::named_face(doc, st, *pick, n)?),
|
||||
None => None,
|
||||
};
|
||||
let (c, n) = match named.as_ref().map(face::NamedFace::plane).transpose()?.flatten() {
|
||||
Some(cn) => cn,
|
||||
None => nearest_plane(doc, st, o, named.as_ref().map_or(*pick, |f| f.point))?,
|
||||
};
|
||||
frame(c, n, face::x_axis(doc, st, x.as_ref(), c)?).ok_or_else(bad)
|
||||
Snap::FaceCenter { pick, face } => {
|
||||
let pick = &named_pick(st, *pick, face.as_deref())?;
|
||||
let comp = component_of(doc, o.occurrence)?;
|
||||
let mut best: Option<(f64, Vec3, Vec3)> = None;
|
||||
for b in component_bodies(doc, st, comp) {
|
||||
let tol = (b.body.size() * 2e-3).max(1e-3);
|
||||
for f in b.body.faces(tol)? {
|
||||
let Some(n) = f.plane_normal else { continue };
|
||||
let d = (*pick - f.centroid).dot(n).abs() + 1e-3 * (*pick - f.centroid).len();
|
||||
if best.as_ref().is_none_or(|(bd, _, _)| d < *bd) {
|
||||
best = Some((d, f.centroid, n));
|
||||
}
|
||||
}
|
||||
}
|
||||
let (_, c, n) = best.ok_or_else(|| DocError::Invalid("joint origin: no planar face there".into()))?;
|
||||
frame(c, n, None).ok_or_else(bad)
|
||||
}
|
||||
Snap::Edge { .. } => edge::edge_frame(doc, st, component_of(doc, o.occurrence)?, &o.snap),
|
||||
Snap::CircleCenter { x, .. } => {
|
||||
let r = round_of(doc, st, o)?;
|
||||
frame(r.centre, r.z, face::x_axis(doc, st, x.as_ref(), r.centre)?).ok_or_else(bad)
|
||||
Snap::CircleCenter { .. } => {
|
||||
let (pick, cyl) = circle_at(doc, st, o)?;
|
||||
let s = (pick - cyl.axis_point).dot(cyl.axis);
|
||||
let (at, dir) = if (s - cyl.start).abs() <= (cyl.end - s).abs() { (cyl.start, -1.0) } else { (cyl.end, 1.0) };
|
||||
frame(cyl.axis_point + cyl.axis * at, cyl.axis * dir, None).ok_or_else(bad)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A circle-centre snap's cylinder and the end it takes (component coordinates): through its
|
||||
/// named face when it names a cylinder, else the cylindrical face at its pick and the end nearer
|
||||
/// it, as older builds found them. Any other snap is refused.
|
||||
fn round_of(doc: &Document, st: &ModelState, o: &JointOrigin) -> Result<face::Round> {
|
||||
let Snap::CircleCenter { pick, face, end, .. } = &o.snap else { return Err(DocError::Invalid("pick a round face".into())) };
|
||||
if let Some(n) = face.as_deref()
|
||||
&& let Some(r) = face::named_round(doc, st, *pick, n, end.as_deref())?
|
||||
{
|
||||
return Ok(r);
|
||||
}
|
||||
let pick = named_pick(doc, st, *pick, face.as_deref())?;
|
||||
/// The pick of a circle-centre snap (on its named face) and the cylindrical face there
|
||||
/// (component coordinates). Any other snap is refused.
|
||||
fn circle_at(doc: &Document, st: &ModelState, o: &JointOrigin) -> Result<(Vec3, solidcraft_kernel::CylinderFace)> {
|
||||
let Snap::CircleCenter { pick, face } = &o.snap else { return Err(DocError::Invalid("pick a round face".into())) };
|
||||
let pick = named_pick(st, *pick, face.as_deref())?;
|
||||
let comp = component_of(doc, o.occurrence)?;
|
||||
// A body moved in from another component: looked at in its own coordinates, its cylinder
|
||||
// placed by its offset (as a named face is).
|
||||
let cyl = component_bodies(doc, st, comp)
|
||||
.into_iter()
|
||||
.filter_map(|b| match doc.model_body_offset(b) {
|
||||
Some(m) => solidcraft_kernel::cylinder_face_at(&b.body, apply_point(&mat_inverse(m)?, pick)).map(|c| face::placed(m, c)),
|
||||
None => solidcraft_kernel::cylinder_face_at(&b.body, pick),
|
||||
})
|
||||
.filter_map(|b| solidcraft_kernel::cylinder_face_at(&b.body, pick))
|
||||
.min_by(|a, b| {
|
||||
let off = |c: &solidcraft_kernel::CylinderFace| {
|
||||
let v = pick - c.axis_point;
|
||||
@@ -723,14 +445,7 @@ fn round_of(doc: &Document, st: &ModelState, o: &JointOrigin) -> Result<face::Ro
|
||||
off(a).total_cmp(&off(b))
|
||||
})
|
||||
.ok_or_else(|| DocError::Invalid("joint origin: no cylindrical face there".into()))?;
|
||||
let s = (pick - cyl.axis_point).dot(cyl.axis);
|
||||
let (at, dir) = if (s - cyl.start).abs() <= (cyl.end - s).abs() { (cyl.start, -1.0) } else { (cyl.end, 1.0) };
|
||||
Ok(face::Round { cylinder: cyl, centre: cyl.axis_point + cyl.axis * at, z: cyl.axis * dir })
|
||||
}
|
||||
|
||||
/// The cylindrical face of a circle-centre snap (component coordinates).
|
||||
fn circle_at(doc: &Document, st: &ModelState, o: &JointOrigin) -> Result<solidcraft_kernel::CylinderFace> {
|
||||
round_of(doc, st, o).map(|r| r.cylinder)
|
||||
Ok((pick, cyl))
|
||||
}
|
||||
|
||||
/// World transform of an occurrence (through its parents).
|
||||
@@ -987,7 +702,7 @@ fn close_loops(tree: &Tree, closing: &[u64], asm: &Assembly, held: &Held, vals:
|
||||
if closing.len() > MAX_LOOPS {
|
||||
return (Values::new(), stuck);
|
||||
}
|
||||
let linked = set_values(asm);
|
||||
let linked: BTreeSet<(u64, usize)> = asm.links.iter().map(|l| (l.b, l.ib)).collect();
|
||||
let groups = loop_groups(tree, closing, asm, &linked);
|
||||
let spent = std::cell::Cell::new(0usize);
|
||||
for (group, around, touches) in &groups {
|
||||
@@ -1182,7 +897,7 @@ fn loop_dof(tree: &Tree, closing: &[u64], asm: &Assembly, vals: &Values, dof: &m
|
||||
if closing.len() > MAX_LOOPS {
|
||||
return;
|
||||
}
|
||||
let linked = set_values(asm);
|
||||
let linked: BTreeSet<(u64, usize)> = asm.links.iter().map(|l| (l.b, l.ib)).collect();
|
||||
let groups = loop_groups(tree, closing, asm, &linked);
|
||||
// The joints loops depend on: those around them, those whose values they read, and those a
|
||||
// motion link carries any of these values into.
|
||||
@@ -1595,14 +1310,6 @@ fn build<'a>(doc: &'a Document, st: &ModelState, vals: &Values, sol: &mut Soluti
|
||||
// Joint frames in component coordinates (errors leave the joint out).
|
||||
let mut frames: BTreeMap<u64, (Mat, Mat)> = BTreeMap::new();
|
||||
for j in &joints {
|
||||
// A joint whose expressions can't be worked out (a parameter gone) is left out rather
|
||||
// than placed by numbers that no longer say what it was defined by.
|
||||
if !j.exprs.is_empty()
|
||||
&& let Err(e) = expressed(doc, j)
|
||||
{
|
||||
sol.errors.push((j.id, j.name.clone(), e.to_string()));
|
||||
continue;
|
||||
}
|
||||
match (resolve_origin(doc, st, &j.a), resolve_origin(doc, st, &j.b)) {
|
||||
(Ok(fa), Ok(fb)) => {
|
||||
frames.insert(j.id, (fa, fb));
|
||||
@@ -1725,38 +1432,10 @@ impl Document {
|
||||
#[path = "joints_cam.rs"]
|
||||
pub mod cam;
|
||||
|
||||
#[path = "joints_edge.rs"]
|
||||
mod edge;
|
||||
pub use edge::edge_snap;
|
||||
|
||||
#[path = "joints_reach.rs"]
|
||||
mod reach;
|
||||
|
||||
#[path = "joints_face.rs"]
|
||||
mod face;
|
||||
pub use face::{end_faces, face_name_at, named_face_spot, pin_end, round_end_at};
|
||||
pub use reach::{REACH_TOL, Reach, Reached, reach};
|
||||
|
||||
/// The rest angle that turns B least as joint `j` is made, as B sits now: none or a half turn
|
||||
/// about the joint's z. Origins whose x is chosen for them (a face, a circle, a point without
|
||||
/// x) get opposite x when their z are opposite, so two faces that already meet would, at angle
|
||||
/// 0, spin B a half turn about their shared normal.
|
||||
pub fn least_turn_angle(doc: &Document, st: &ModelState, j: &Joint) -> f64 {
|
||||
let (Ok(fa), Ok(fb)) = (resolve_origin(doc, st, &j.a), resolve_origin(doc, st, &j.b)) else { return 0.0 };
|
||||
let (ta, tb) = (occurrence_world(doc, j.a.occurrence), occurrence_world(doc, j.b.occurrence));
|
||||
let mut q = j.values.clone();
|
||||
q.resize(j.kind.dofs().len(), 0.0);
|
||||
clamp(j, &mut q);
|
||||
let turn = |angle: f64| {
|
||||
let k = Joint { angle, ..j.clone() };
|
||||
through(&k, &(fa, fb), &q, true, &ta).map(|m| mismatch(&m, &tb).1)
|
||||
};
|
||||
match (turn(0.0), turn(std::f64::consts::PI)) {
|
||||
(Some(none), Some(half)) if half + 1e-9 < none => std::f64::consts::PI,
|
||||
_ => 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// The frame matrix for an explicit origin / z / x (public for commands).
|
||||
pub fn frame_of(o: Vec3, z: Vec3, x: Option<Vec3>) -> Option<Mat> {
|
||||
frame(o, z, x)
|
||||
@@ -1788,7 +1467,6 @@ mod tests {
|
||||
offset: 0.0,
|
||||
angle: 0.0,
|
||||
suppressed: false,
|
||||
exprs: Default::default(),
|
||||
})
|
||||
.collect();
|
||||
let links =
|
||||
|
||||
@@ -967,7 +967,6 @@ mod tests {
|
||||
offset: 0.0,
|
||||
angle: 0.0,
|
||||
suppressed: false,
|
||||
exprs: Default::default(),
|
||||
};
|
||||
let occ = |id: u64, grounded: bool| crate::Occurrence { id, component: id, parent: 0, name: format!("O{id}"), transform: IDENTITY, grounded };
|
||||
// 1 frame (grounded), 2 cam, 3-6 a rigid chain to the guide 7, 8 the piston, 9 crank, 10 rod.
|
||||
|
||||
@@ -1,193 +0,0 @@
|
||||
//! Joint origins on an edge of a planar face (`Snap::Edge`): the edge's middle, or a corner where
|
||||
//! it meets another edge of the face. The edge, the face and the corner's other edge are found
|
||||
//! again by name on every solve, so the origin follows the face as it grows, shrinks or moves:
|
||||
//! hardware set 40 mm in from a panel's corner stays 40 mm in from that corner at any size.
|
||||
//!
|
||||
//! The frame never depends on which way an edge's polyline runs: z is the face's outward normal;
|
||||
//! at the middle y points into the face, square to the edge, and x = y × z runs along it; at a
|
||||
//! corner x and y run along the two edges, away from the corner into the face, in the order that
|
||||
//! makes the frame right-handed.
|
||||
|
||||
use solidcraft_geom::Vec3;
|
||||
|
||||
use super::{Snap, component_bodies, frame};
|
||||
use crate::{DocError, Document, Mat, ModelBody, ModelState, Result};
|
||||
|
||||
/// Points that close are one corner (as a share of the body's size, at least 1 µm).
|
||||
fn tol(b: &ModelBody) -> f64 {
|
||||
(b.body.size() * 2e-3).max(1e-3)
|
||||
}
|
||||
|
||||
/// The edge of `b` nearest `p`, and how far it is.
|
||||
fn nearest_edge(b: &ModelBody, p: Vec3) -> Option<(usize, f64)> {
|
||||
b.mesh()
|
||||
.edges
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, e)| (i, e.windows(2).map(|w| p.dist_to_segment(w[0], w[1])).fold(f64::INFINITY, f64::min)))
|
||||
.filter(|(_, d)| d.is_finite())
|
||||
.min_by(|a, b| a.1.total_cmp(&b.1))
|
||||
}
|
||||
|
||||
/// Edge `i` of `b`: by its name (`None` when the body has no such edge), else the edge at `pick`.
|
||||
fn edge_index(b: &ModelBody, name: Option<&str>, pick: Vec3) -> Option<usize> {
|
||||
match name {
|
||||
Some(n) => crate::name_lookup::locate_edge(b, n, pick).ok().map(|h| h.edge),
|
||||
None => nearest_edge(b, pick).filter(|(_, d)| *d <= tol(b)).map(|(i, _)| i),
|
||||
}
|
||||
}
|
||||
|
||||
/// The planar face beside edge `i` the origin sits on: the named one, else the larger planar face
|
||||
/// the edge bounds. Its index, outward normal and centroid.
|
||||
fn face_of(b: &ModelBody, i: usize, name: Option<&str>, pick: Vec3) -> Result<(usize, Vec3, Vec3)> {
|
||||
let m = b.mesh();
|
||||
let beside: Vec<usize> = m.edge_faces.get(i).map(|fs| fs.iter().map(|f| *f as usize).collect()).unwrap_or_default();
|
||||
let infos = b.body.faces(tol(b))?;
|
||||
let planar = |f: usize| infos.iter().find(|x| x.index == f).and_then(|x| x.plane_normal.map(|n| (x.index, n, x.centroid, x.area)));
|
||||
let chosen = match name {
|
||||
Some(n) => {
|
||||
let (f, _, _) = crate::naming::face_on_body(b, n, pick)
|
||||
.ok_or_else(|| DocError::Invalid(format!("joint origin: the face `{n}` no longer exists (edit the joint to pick again)")))?;
|
||||
if !beside.contains(&f) {
|
||||
return Err(DocError::Invalid(format!("joint origin: the face `{n}` no longer meets its edge (edit the joint to pick again)")));
|
||||
}
|
||||
planar(f)
|
||||
}
|
||||
None => beside.iter().filter_map(|f| planar(*f)).max_by(|a, b| a.3.total_cmp(&b.3)),
|
||||
};
|
||||
chosen.map(|(f, n, c, _)| (f, n, c)).ok_or_else(|| DocError::Invalid("joint origin: no planar face beside that edge".into()))
|
||||
}
|
||||
|
||||
/// Edge `i`'s middle (along its polyline) and the direction it runs there.
|
||||
fn middle(e: &[Vec3]) -> Option<(Vec3, Vec3)> {
|
||||
let total: f64 = e.windows(2).map(|w| w[0].dist(w[1])).sum();
|
||||
let mut acc = 0.0;
|
||||
for w in e.windows(2) {
|
||||
let l = w[0].dist(w[1]);
|
||||
if l > 0.0 && acc + l >= total / 2.0 {
|
||||
let t = (total / 2.0 - acc) / l;
|
||||
return Some((w[0] + (w[1] - w[0]) * t, (w[1] - w[0]) / l));
|
||||
}
|
||||
acc += l;
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// An edge's end at `p` (within `tol`) and the direction it leaves the end in.
|
||||
fn leaving(e: &[Vec3], p: Vec3, tol: f64) -> Option<Vec3> {
|
||||
let (first, last) = (*e.first()?, *e.last()?);
|
||||
let away = |q: Option<&Vec3>, from: Vec3| q.and_then(|q| (*q - from).normalized());
|
||||
if first.dist(p) <= tol {
|
||||
away(e.iter().find(|q| q.dist(first) > tol * 0.5), first)
|
||||
} else if last.dist(p) <= tol {
|
||||
away(e.iter().rev().find(|q| q.dist(last) > tol * 0.5), last)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// The corner where edges `e` and `f` meet: the closest pair of their ends, when within `tol`.
|
||||
fn meeting(e: &[Vec3], f: &[Vec3], tol: f64) -> Option<Vec3> {
|
||||
let ends = |x: &[Vec3]| [x.first().copied(), x.last().copied()];
|
||||
ends(e)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.flat_map(|a| ends(f).into_iter().flatten().map(move |b| (a, a.dist(b))))
|
||||
.filter(|(_, d)| *d <= tol)
|
||||
.min_by(|a, b| a.1.total_cmp(&b.1))
|
||||
.map(|(a, _)| a)
|
||||
}
|
||||
|
||||
/// The frame of an edge snap in its component's coordinates.
|
||||
pub(super) fn edge_frame(doc: &Document, st: &ModelState, comp: u64, snap: &Snap) -> Result<Mat> {
|
||||
let Snap::Edge { pick, edge, face, corner } = snap else { return Err(DocError::Invalid("not an edge origin".into())) };
|
||||
let bad = || DocError::Invalid("joint origin: degenerate frame".into());
|
||||
let gone = |what: &str| DocError::Invalid(format!("joint origin: {what} no longer exists (edit the joint to pick again)"));
|
||||
let found = component_bodies(doc, st, comp).into_iter().find_map(|b| edge_index(b, edge.as_deref(), *pick).map(|i| (b, i)));
|
||||
let (b, i) = found.ok_or_else(|| match edge {
|
||||
Some(n) => gone(&format!("the edge `{n}`")),
|
||||
None => DocError::Invalid("joint origin: no edge there".into()),
|
||||
})?;
|
||||
let m = b.mesh();
|
||||
let e = m.edges.get(i).ok_or_else(bad)?;
|
||||
let (_, z, centroid) = face_of(b, i, face.as_deref(), *pick)?;
|
||||
let flat = |v: Vec3| (v - z * v.dot(z)).normalized();
|
||||
match corner {
|
||||
None => {
|
||||
let (mid, along) = middle(e).ok_or_else(bad)?;
|
||||
// Into the face: toward its centroid, square to the edge; a face whose centroid lies on
|
||||
// the edge's line (only a sliver) falls back to the side z × along gives.
|
||||
let to_c = centroid - mid;
|
||||
let y = flat(to_c - along * to_c.dot(along)).unwrap_or_else(|| z.cross(along));
|
||||
frame(mid, z, Some(y.cross(z))).ok_or_else(bad)
|
||||
}
|
||||
Some(other) => {
|
||||
let j = crate::name_lookup::locate_edge(b, other, *pick).map_err(|_| gone(&format!("the edge `{other}`")))?.edge;
|
||||
let f = m.edges.get(j).ok_or_else(bad)?;
|
||||
let t = tol(b);
|
||||
let at = meeting(e, f, t).ok_or_else(|| DocError::Invalid(format!("joint origin: its edges no longer meet at a corner (`{other}`)")))?;
|
||||
let (de, df) = (leaving(e, at, t).and_then(flat).ok_or_else(bad)?, leaving(f, at, t).and_then(flat).ok_or_else(bad)?);
|
||||
let x = if z.dot(de.cross(df)) >= 0.0 { de } else { df };
|
||||
frame(at, z, Some(x)).ok_or_else(bad)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An edge snap for an edge picked at `pick` on component `comp` (its own coordinates): on the
|
||||
/// planar face holding `on` (default: the larger planar face beside the edge), at the edge's
|
||||
/// middle or, with `corner`, at the end nearest it. The edge, the face and the corner's other
|
||||
/// edge are kept by name.
|
||||
pub fn edge_snap(doc: &Document, st: &ModelState, comp: u64, pick: Vec3, on: Option<Vec3>, corner: Option<Vec3>) -> Result<Snap> {
|
||||
let (b, i) = component_bodies(doc, st, comp)
|
||||
.into_iter()
|
||||
.filter_map(|b| nearest_edge(b, pick).filter(|(_, d)| *d <= tol(b)).map(|(i, _)| (b, i)))
|
||||
.next()
|
||||
.ok_or_else(|| DocError::Invalid("joint origin: no edge there".into()))?;
|
||||
let names = crate::naming::edge_names(b);
|
||||
let face_names = crate::naming::face_names(b);
|
||||
let m = b.mesh();
|
||||
let beside: Vec<usize> = m.edge_faces.get(i).map(|fs| fs.iter().map(|f| *f as usize).collect()).unwrap_or_default();
|
||||
let face_index = match on {
|
||||
Some(p) => {
|
||||
let f = crate::naming::face_name_at_body(b, p)
|
||||
.and_then(|n| crate::naming::face_on_body(b, &n, p))
|
||||
.map(|(f, _, _)| f)
|
||||
.ok_or_else(|| DocError::Invalid("joint origin: `on` is not on a face of that body".into()))?;
|
||||
if !beside.contains(&f) {
|
||||
return Err(DocError::Invalid("joint origin: `on` picks a face the edge doesn't bound".into()));
|
||||
}
|
||||
f
|
||||
}
|
||||
None => face_of(b, i, None, pick)?.0,
|
||||
};
|
||||
let face = face_names.get(face_index).cloned().filter(|n| !n.is_empty());
|
||||
let corner = match corner {
|
||||
None => None,
|
||||
Some(c) => {
|
||||
let e = m.edges.get(i).cloned().unwrap_or_default();
|
||||
let t = tol(b);
|
||||
let end = [e.first().copied(), e.last().copied()]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.min_by(|a, b| a.dist(c).total_cmp(&b.dist(c)))
|
||||
.ok_or_else(|| DocError::Invalid("joint origin: that edge has no ends".into()))?;
|
||||
// The other edge of the face that ends there.
|
||||
let other = m
|
||||
.edges
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(j, _)| *j != i && m.edge_faces.get(*j).is_some_and(|fs| fs.contains(&(face_index as u32))))
|
||||
.find(|(_, f)| meeting(&[end], f, t).is_some())
|
||||
.map(|(j, _)| j)
|
||||
.ok_or_else(|| DocError::Invalid("joint origin: no other edge of the face meets that end (a round edge has no corner)".into()))?;
|
||||
Some(
|
||||
names
|
||||
.get(other)
|
||||
.cloned()
|
||||
.filter(|n| !n.is_empty())
|
||||
.ok_or_else(|| DocError::Invalid("joint origin: the corner's other edge has no name".into()))?,
|
||||
)
|
||||
}
|
||||
};
|
||||
Ok(Snap::Edge { pick, edge: names.get(i).cloned().filter(|n| !n.is_empty()), face, corner })
|
||||
}
|
||||
@@ -1,328 +0,0 @@
|
||||
//! Joint origins on named faces: a face found by its persistent name gives its own plane or
|
||||
//! cylinder (axis, radius, extent) directly, instead of searching again for the face nearest a
|
||||
//! point (on a thin plate the face nearest a point on a hole's wall is often the plate's face).
|
||||
//! A round origin's end is kept by the name of the face across the rim at that end, so it is the
|
||||
//! same end after the design changes; an x reference keeps the origin's turn about its axis.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use solidcraft_geom::Vec3;
|
||||
use solidcraft_kernel::CylinderFace;
|
||||
|
||||
use super::{Snap, XRef, component_bodies};
|
||||
use crate::history_names::Match;
|
||||
use crate::{DocError, Document, IDENTITY, Mat, ModelBody, ModelState, Result, apply_point, apply_vector, mat_inverse};
|
||||
|
||||
/// Rim points sampled to find the face across a cylinder's rim.
|
||||
const RIM_SAMPLES: usize = 16;
|
||||
|
||||
/// A face found by its persistent name: its body, its index there, the point of it nearest the
|
||||
/// pick (the component's coordinates) and the body's offset (a body moved in from another
|
||||
/// component keeps its geometry where it was made; the offset places it).
|
||||
pub(super) struct NamedFace<'a> {
|
||||
pub body: &'a ModelBody,
|
||||
pub face: usize,
|
||||
pub point: Vec3,
|
||||
pub offset: Mat,
|
||||
}
|
||||
|
||||
fn gone(name: &str) -> DocError {
|
||||
DocError::Invalid(format!("joint origin: the face `{name}` no longer exists (edit the joint to pick again)"))
|
||||
}
|
||||
|
||||
/// The face called `name` nearest `pick` (an error when it is gone: a joint must not quietly
|
||||
/// attach elsewhere).
|
||||
pub(super) fn named_face<'a>(doc: &Document, st: &'a ModelState, pick: Vec3, name: &str) -> Result<NamedFace<'a>> {
|
||||
let hit = crate::name_lookup::locate_face(st, name, pick).ok_or_else(|| gone(name))?;
|
||||
let body = st.bodies.get(hit.body).ok_or_else(|| gone(name))?;
|
||||
let Some(offset) = doc.model_body_offset(body).copied() else {
|
||||
return Ok(NamedFace { body, face: hit.face, point: hit.point, offset: IDENTITY });
|
||||
};
|
||||
let inv = mat_inverse(&offset).ok_or_else(|| gone(name))?;
|
||||
let (face, at, _) = crate::naming::face_on_body(body, name, apply_point(&inv, pick)).ok_or_else(|| gone(name))?;
|
||||
Ok(NamedFace { body, face, point: apply_point(&offset, at), offset })
|
||||
}
|
||||
|
||||
impl NamedFace<'_> {
|
||||
/// A point of the component's coordinates in the body's own.
|
||||
fn local(&self, p: Vec3) -> Vec3 {
|
||||
if self.offset == IDENTITY {
|
||||
return p;
|
||||
}
|
||||
mat_inverse(&self.offset).map_or(p, |inv| apply_point(&inv, p))
|
||||
}
|
||||
|
||||
/// The face as a cylinder, in the body's own coordinates (`None`: not a cylinder).
|
||||
fn own_cylinder(&self) -> Option<CylinderFace> {
|
||||
solidcraft_kernel::cylinder_face(&self.body.body, &self.body.mesh(), self.face)
|
||||
}
|
||||
|
||||
/// The face as a cylinder, in the component's coordinates.
|
||||
pub fn cylinder(&self) -> Option<CylinderFace> {
|
||||
self.own_cylinder().map(|c| placed(&self.offset, c))
|
||||
}
|
||||
|
||||
/// The face's centre and outward normal (the component's coordinates), when it is planar.
|
||||
pub fn plane(&self) -> Result<Option<(Vec3, Vec3)>> {
|
||||
let tol = (self.body.body.size() * 2e-3).max(1e-3);
|
||||
let faces = self.body.body.faces(tol)?;
|
||||
Ok(faces.get(self.face).and_then(|f| f.plane_normal.map(|n| (apply_point(&self.offset, f.centroid), apply_vector(&self.offset, n)))))
|
||||
}
|
||||
}
|
||||
|
||||
/// A cylinder of a body's own coordinates placed by the body's (rigid) offset.
|
||||
pub(super) fn placed(m: &Mat, c: CylinderFace) -> CylinderFace {
|
||||
CylinderFace { axis_point: apply_point(m, c.axis_point), axis: apply_vector(m, *c.axis).unit().unwrap_or(c.axis), ..c }
|
||||
}
|
||||
|
||||
/// The face across the rim at the `at` end (along the axis) of cylinder `c`, face `face` of `b`
|
||||
/// (the body's own coordinates): of the body's other faces, the one nearest the rim's points.
|
||||
fn rim_neighbour(b: &ModelBody, face: usize, c: &CylinderFace, at: f64) -> Option<usize> {
|
||||
let mesh = b.mesh();
|
||||
let slack = (c.end - c.start).abs() * 1e-3 + 1e-9;
|
||||
let mut rim: Vec<Vec3> = Vec::new();
|
||||
for (t, f) in mesh.triangles.iter().zip(&mesh.tri_face) {
|
||||
if *f as usize != face {
|
||||
continue;
|
||||
}
|
||||
for k in t {
|
||||
if let Some(q) = mesh.positions.get(*k as usize)
|
||||
&& ((*q - c.axis_point).dot(c.axis) - at).abs() <= slack
|
||||
&& !rim.iter().any(|r| r.dist(*q) <= slack)
|
||||
{
|
||||
rim.push(*q);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Evenly through the rim's points, at most RIM_SAMPLES of them.
|
||||
let step = rim.len().div_ceil(RIM_SAMPLES).max(1);
|
||||
let rim: Vec<Vec3> = rim.into_iter().step_by(step).collect();
|
||||
if rim.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut near: BTreeMap<usize, Vec<f64>> = BTreeMap::new();
|
||||
for (t, f) in mesh.triangles.iter().zip(&mesh.tri_face) {
|
||||
let f = *f as usize;
|
||||
if f == face {
|
||||
continue;
|
||||
}
|
||||
let Some([p, q, r]) = mesh.tri(t) else { continue };
|
||||
let slot = near.entry(f).or_insert_with(|| vec![f64::INFINITY; rim.len()]);
|
||||
for (d, x) in slot.iter_mut().zip(&rim) {
|
||||
*d = d.min(crate::naming::closest(*x, p, q, r).dist(*x));
|
||||
}
|
||||
}
|
||||
// The face that meets the whole rim (the other half of a cylinder split in two meets only
|
||||
// the rim's points at its seams).
|
||||
near.into_iter().map(|(f, d)| (d.iter().sum::<f64>(), f)).min_by(|a, b| a.0.total_cmp(&b.0)).map(|(_, f)| f)
|
||||
}
|
||||
|
||||
/// The ends of a cylinder: (place along the axis, which way out of the face).
|
||||
fn ends(c: &CylinderFace) -> [(f64, f64); 2] {
|
||||
[(c.start, -1.0), (c.end, 1.0)]
|
||||
}
|
||||
|
||||
/// The end of `c` nearer `pick` along the axis. On a tie, the end older builds took: the start
|
||||
/// of the axis as the kernel turns it (toward +x, else +y, else +z), whichever way the face's
|
||||
/// own surface runs.
|
||||
fn nearer_end(c: &CylinderFace, pick: Vec3) -> (f64, f64) {
|
||||
let s = (pick - c.axis_point).dot(c.axis);
|
||||
let [start, end] = ends(c);
|
||||
let (to_start, to_end) = ((s - c.start).abs(), (c.end - s).abs());
|
||||
let reversed = c.axis.x + c.axis.y * 1e-3 + c.axis.z * 1e-6 < 0.0;
|
||||
if to_start < to_end || (to_start == to_end && !reversed) { start } else { end }
|
||||
}
|
||||
|
||||
/// The end a named round face takes without an `end` (a design saved before origins named
|
||||
/// it): the one nearer the pick (moved onto the face) along the axis, decided as those builds
|
||||
/// decided it, by the kernel's reading of the cylinder at the pick, so a pick midway along the
|
||||
/// wall takes the same end as it did then. `own` is the face's cylinder (the body's own
|
||||
/// coordinates).
|
||||
fn end_by_pick(nf: &NamedFace, own: &CylinderFace) -> (f64, f64) {
|
||||
let pick = nf.local(nf.point);
|
||||
let [start, end] = ends(own);
|
||||
match solidcraft_kernel::cylinder_face_at(&nf.body.body, pick).filter(|k| k.axis.dot(own.axis).abs() > 0.5) {
|
||||
Some(k) => {
|
||||
let s = (pick - k.axis_point).dot(k.axis);
|
||||
let out = if (s - k.start).abs() <= (k.end - s).abs() { -k.axis } else { k.axis };
|
||||
if out.dot(own.axis) < 0.0 { start } else { end }
|
||||
}
|
||||
None => nearer_end(own, pick),
|
||||
}
|
||||
}
|
||||
|
||||
/// A round joint origin worked out: its cylinder, and the centre and outward axis of the end
|
||||
/// it takes (the component's coordinates).
|
||||
pub(super) struct Round {
|
||||
pub cylinder: CylinderFace,
|
||||
pub centre: Vec3,
|
||||
pub z: Vec3,
|
||||
}
|
||||
|
||||
impl Round {
|
||||
fn at(c: CylinderFace, (at, dir): (f64, f64)) -> Round {
|
||||
Round { cylinder: c, centre: c.axis_point + c.axis * at, z: c.axis * dir }
|
||||
}
|
||||
}
|
||||
|
||||
/// A circle-centre snap's cylinder found through its named face (`None`: the snap names no
|
||||
/// face, or the face is not a cylinder any more; the caller looks by point, as older builds
|
||||
/// did). The end is the one whose rim meets the face called `end`, else the one nearer `pick`.
|
||||
pub(super) fn named_round(doc: &Document, st: &ModelState, pick: Vec3, face: &str, end: Option<&str>) -> Result<Option<Round>> {
|
||||
let nf = named_face(doc, st, pick, face)?;
|
||||
let Some(own) = nf.own_cylinder() else { return Ok(None) };
|
||||
let chosen = end.and_then(|name| end_named(nf.body, nf.face, &own, name)).unwrap_or_else(|| end_by_pick(&nf, &own));
|
||||
Ok(Some(Round::at(placed(&nf.offset, own), chosen)))
|
||||
}
|
||||
|
||||
/// The end of cylinder `own` (face `face` of `b`) whose rim meets the face called `name` better
|
||||
/// than the other end's rim does: the very face, or one merged into it, else a piece of it whose
|
||||
/// neighbours have changed (the face round a hole in a sheet is told apart from its other pieces
|
||||
/// by the faces around it, and those change when the hole moves). `None` when neither rim, or
|
||||
/// both, meet it as well.
|
||||
fn end_named(b: &ModelBody, face: usize, own: &CylinderFace, name: &str) -> Option<(f64, f64)> {
|
||||
let names = crate::naming::names(b);
|
||||
let how = |at: f64| {
|
||||
let f = rim_neighbour(b, face, own, at)?;
|
||||
if crate::name_lookup::legacy_name(name).is_some() {
|
||||
return crate::name_lookup::face_has_name(b, f, name).then_some(Match::Exact);
|
||||
}
|
||||
crate::history_names::matches(&names, f, name)
|
||||
};
|
||||
let [start, end] = ends(own);
|
||||
match (how(start.0), how(end.0)) {
|
||||
(s, e) if s > e => Some(start),
|
||||
(s, e) if e > s => Some(end),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The x axis an x reference gives a frame at `o` with axis `z` (`None`: no reference).
|
||||
pub(super) fn x_axis(doc: &Document, st: &ModelState, x: Option<&XRef>, o: Vec3) -> Result<Option<Vec3>> {
|
||||
Ok(match x {
|
||||
None => None,
|
||||
Some(XRef::Dir(d)) => Some(*d),
|
||||
Some(XRef::Face { face, pick }) => {
|
||||
let nf = named_face(doc, st, pick.unwrap_or(o), face)?;
|
||||
if let Some((_, n)) = nf.plane()? {
|
||||
Some(n)
|
||||
} else if let Some(c) = nf.cylinder() {
|
||||
let v = c.axis_point - o;
|
||||
Some(v - c.axis * v.dot(c.axis))
|
||||
} else {
|
||||
Some(nf.point - o)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// The persistent name of the face of a component's bodies at `pick` (the component's
|
||||
/// coordinates), measured to each face's triangles and, among the faces as near as the mesh
|
||||
/// lets them be told apart, to the face's own surface (a point on a thin plate's hole wall lies
|
||||
/// as near the facets of the plate's face round the rim). `round` prefers a cylinder on a tie
|
||||
/// (a pick on a hole's rim names its wall). `None`: no named face there.
|
||||
pub fn face_name_at(doc: &Document, st: &ModelState, component: u64, pick: Vec3, round: bool) -> Option<String> {
|
||||
let mut best: Option<(f64, &ModelBody, usize)> = None;
|
||||
for b in component_bodies(doc, st, component) {
|
||||
if b.body.is_mesh() {
|
||||
continue;
|
||||
}
|
||||
let offset = doc.model_body_offset(b).copied().unwrap_or(IDENTITY);
|
||||
let p = apply_point(&mat_inverse(&offset)?, pick);
|
||||
let mesh = b.mesh();
|
||||
let tol = (b.body.size() * 1e-3).max(1e-3).max(b.display_tol());
|
||||
let mut near: BTreeMap<usize, f64> = BTreeMap::new();
|
||||
for (t, f) in mesh.triangles.iter().zip(&mesh.tri_face) {
|
||||
let Some([x, y, z]) = mesh.tri(t) else { continue };
|
||||
let d = crate::naming::closest(p, x, y, z).dist(p);
|
||||
near.entry(*f as usize).and_modify(|n| *n = n.min(d)).or_insert(d);
|
||||
}
|
||||
let nearest = near.values().copied().fold(f64::INFINITY, f64::min);
|
||||
if nearest > tol * 5.0 {
|
||||
continue;
|
||||
}
|
||||
for (f, d) in near.into_iter().filter(|(_, d)| *d <= nearest + tol) {
|
||||
let (off, cylinder) = match solidcraft_kernel::face_surface(&b.body, f) {
|
||||
Some(solidcraft_kernel::FaceSurface::Plane { normal, point }) => (d.min((p - point).dot(normal).abs()), false),
|
||||
Some(solidcraft_kernel::FaceSurface::Cylinder { .. }) => {
|
||||
(solidcraft_kernel::cylinder_face(&b.body, &mesh, f).map_or(d, |c| d.min(c.off(p))), true)
|
||||
}
|
||||
_ => (d, false),
|
||||
};
|
||||
let score = if round && cylinder { off - tol } else { off };
|
||||
if best.as_ref().is_none_or(|(s, ..)| score < *s) {
|
||||
best = Some((score, b, f));
|
||||
}
|
||||
}
|
||||
}
|
||||
let (_, b, f) = best?;
|
||||
crate::naming::face_names(b).get(f).filter(|n| !n.is_empty()).cloned()
|
||||
}
|
||||
|
||||
/// A circle-centre snap for the round end of a component's cylindrical face centred at
|
||||
/// `centre` (the component's coordinates; a circle given by its centre, where no face lies),
|
||||
/// with the face's name and its end's: `None` when no named cylinder ends there.
|
||||
pub fn round_end_at(doc: &Document, st: &ModelState, component: u64, centre: Vec3) -> Option<Snap> {
|
||||
let mut best: Option<(f64, &ModelBody, usize, CylinderFace, f64)> = None;
|
||||
for b in component_bodies(doc, st, component) {
|
||||
if b.body.is_mesh() {
|
||||
continue;
|
||||
}
|
||||
let offset = doc.model_body_offset(b).copied().unwrap_or(IDENTITY);
|
||||
let p = apply_point(&mat_inverse(&offset)?, centre);
|
||||
let mesh = b.mesh();
|
||||
let tol = (b.body.size() * 1e-3).max(1e-3);
|
||||
for f in 0..b.body.face_count() {
|
||||
let Some(c) = solidcraft_kernel::cylinder_face(&b.body, &mesh, f) else { continue };
|
||||
let v = p - c.axis_point;
|
||||
let along = v.dot(c.axis);
|
||||
for (at, _) in ends(&c) {
|
||||
let off = (v - c.axis * along).len() + (along - at).abs();
|
||||
if off <= tol && best.as_ref().is_none_or(|(o, ..)| off < *o) {
|
||||
best = Some((off, b, f, c, at));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let (_, b, f, c, at) = best?;
|
||||
let names = crate::naming::face_names(b);
|
||||
let face = names.get(f).filter(|n| !n.is_empty()).cloned()?;
|
||||
let end = rim_neighbour(b, f, &c, at).and_then(|n| names.get(n)).filter(|n| !n.is_empty()).cloned();
|
||||
Some(Snap::CircleCenter { pick: centre, face: Some(face), end, x: None })
|
||||
}
|
||||
|
||||
/// Name the end a named circle-centre snap takes now (the face across the rim there), so it
|
||||
/// keeps that end when the design changes. Other snaps, and one whose end is named already, are
|
||||
/// left as they are.
|
||||
pub fn pin_end(doc: &Document, st: &ModelState, snap: &mut Snap) {
|
||||
let Snap::CircleCenter { pick, face: Some(face), end: end @ None, .. } = snap else { return };
|
||||
let Ok(nf) = named_face(doc, st, *pick, face) else { return };
|
||||
let Some(own) = nf.own_cylinder() else { return };
|
||||
let (at, _) = end_by_pick(&nf, &own);
|
||||
*end = rim_neighbour(nf.body, nf.face, &own, at).and_then(|n| crate::naming::face_names(nf.body).get(n).cloned()).filter(|n| !n.is_empty());
|
||||
}
|
||||
|
||||
/// The names of the faces across the rims at the two ends of a named circle-centre snap's
|
||||
/// cylinder (what its `end` can be); empty when the snap names no cylinder.
|
||||
pub fn end_faces(doc: &Document, st: &ModelState, snap: &Snap) -> Vec<String> {
|
||||
let Snap::CircleCenter { pick, face: Some(face), .. } = snap else { return Vec::new() };
|
||||
let Ok(nf) = named_face(doc, st, *pick, face) else { return Vec::new() };
|
||||
let Some(own) = nf.own_cylinder() else { return Vec::new() };
|
||||
let names = crate::naming::face_names(nf.body);
|
||||
ends(&own)
|
||||
.iter()
|
||||
.filter_map(|(at, _)| rim_neighbour(nf.body, nf.face, &own, *at).and_then(|n| names.get(n).cloned()))
|
||||
.filter(|n| !n.is_empty())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The face `model.faces` calls `name` (on the body called `body`, or any body): the component
|
||||
/// it belongs to and a point on it in that component's coordinates, for a joint origin given by
|
||||
/// a face's name. `Err` says why there is no one such face.
|
||||
pub fn named_face_spot(doc: &Document, st: &ModelState, body: Option<&str>, name: &str) -> std::result::Result<(u64, Vec3), String> {
|
||||
let hit = crate::name_lookup::locate_one(st, body, name)?;
|
||||
let b = st.bodies.get(hit.body).ok_or_else(|| format!("no face is named `{name}`"))?;
|
||||
let spot = crate::naming::face_spot(b, hit.face).ok_or_else(|| format!("face `{name}` has no surface to pick"))?;
|
||||
let offset = doc.model_body_offset(b).copied().unwrap_or(IDENTITY);
|
||||
Ok((doc.model_body_component(b), apply_point(&offset, spot.point)))
|
||||
}
|
||||
@@ -17,7 +17,6 @@ pub use clipboard::{clone_expression, remap_topology};
|
||||
pub mod config;
|
||||
pub mod construct;
|
||||
pub mod display_quality;
|
||||
pub mod display_units;
|
||||
mod document;
|
||||
mod eval;
|
||||
pub mod expr;
|
||||
|
||||
@@ -486,9 +486,6 @@ pub enum ParamUser {
|
||||
Dimension { feature: u64, name: String, constraint: String },
|
||||
/// A configured column or an expression saved in a configuration row.
|
||||
Configuration { row: Option<String>, column: String },
|
||||
/// A joint's offset, angle or value, or a motion link's gear, rack or screw length (`input`
|
||||
/// says which; a link is named by the joint it drives).
|
||||
Joint { joint: u64, name: String, input: String },
|
||||
}
|
||||
|
||||
impl ParamUser {
|
||||
@@ -501,7 +498,6 @@ impl ParamUser {
|
||||
Some(row) => format!("configuration `{row}` ({column})"),
|
||||
None => format!("configuration column {column}"),
|
||||
},
|
||||
ParamUser::Joint { name, input, .. } => format!("joint {name} ({input})"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -619,12 +615,6 @@ impl Document {
|
||||
}
|
||||
}
|
||||
}
|
||||
for (joint, input, e) in self.assembly.exprs() {
|
||||
if expr::references(e).iter().any(|r| r == name) {
|
||||
let jname = self.assembly.joints.iter().find(|j| j.id == joint).map(|j| j.name.clone()).unwrap_or_else(|| joint.to_string());
|
||||
out.push(ParamUser::Joint { joint, name: jname, input });
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
@@ -729,13 +719,6 @@ impl Document {
|
||||
}
|
||||
}
|
||||
}
|
||||
for e in self.assembly.exprs_mut() {
|
||||
let r = expr::rename_reference(e, old, new);
|
||||
if r != *e {
|
||||
*e = r;
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
if let Some(c) = self.param_comments.remove(old) {
|
||||
self.param_comments.insert(new.to_string(), c);
|
||||
}
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
//! Reference triangles belong to a definition, even while its occurrences move. Keep one lazy
|
||||
//! source mesh and map it into each pose; retain the exact placed B-rep separately in ModelBody.
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
use solidcraft_geom::{Mesh, Vec3};
|
||||
use solidcraft_kernel::{self as kernel, Body};
|
||||
|
||||
use super::ModelBody;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct PlacedMesh {
|
||||
body: Body,
|
||||
mesh: Arc<OnceLock<Arc<Mesh>>>,
|
||||
transform: crate::Mat,
|
||||
pub(super) tolerance: f64,
|
||||
}
|
||||
|
||||
impl PlacedMesh {
|
||||
/// Share the original definition, never a chain of previously placed meshes. A source's
|
||||
/// OnceLock lives with its immutable geometry, so edits get a new source automatically.
|
||||
pub(super) fn new(source: &ModelBody, transform: &crate::Mat) -> Option<Self> {
|
||||
if !proper_rigid(transform) {
|
||||
return None;
|
||||
}
|
||||
Some(match source.mesh_placement.as_deref() {
|
||||
Some(original) => Self {
|
||||
body: original.body.clone(),
|
||||
mesh: original.mesh.clone(),
|
||||
transform: crate::mat_mul(transform, &original.transform),
|
||||
tolerance: original.tolerance,
|
||||
},
|
||||
None => Self { body: source.body.clone(), mesh: source.mesh.clone(), transform: *transform, tolerance: source.display_tol() },
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn mesh(&self) -> Arc<Mesh> {
|
||||
let source = reference_mesh(&self.body, &self.mesh, self.tolerance);
|
||||
Arc::new(Mesh {
|
||||
positions: source.positions.iter().map(|p| crate::apply_point(&self.transform, *p)).collect(),
|
||||
normals: source.normals.iter().map(|n| crate::apply_vector(&self.transform, *n)).collect(),
|
||||
edges: source.edges.iter().map(|edge| edge.iter().map(|p| crate::apply_point(&self.transform, *p)).collect()).collect(),
|
||||
triangles: source.triangles.clone(),
|
||||
tri_face: source.tri_face.clone(),
|
||||
edge_faces: source.edge_faces.clone(),
|
||||
seams: source.seams.clone(),
|
||||
smooth_edges: source.smooth_edges.clone(),
|
||||
sharp_edges: source.sharp_edges.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn reference_mesh(body: &Body, mesh: &OnceLock<Arc<Mesh>>, tolerance: f64) -> Arc<Mesh> {
|
||||
mesh.get_or_init(|| Arc::new(body.reference_display_mesh_angular(tolerance, kernel::DISPLAY_ANGLE).unwrap_or_default())).clone()
|
||||
}
|
||||
|
||||
/// Only proper rotations and translations preserve triangle winding, normal lengths and the
|
||||
/// reference chord tolerance. Scale, shear, reflection and malformed matrices use the mesher.
|
||||
fn proper_rigid(m: &crate::Mat) -> bool {
|
||||
if m.iter().flatten().any(|v| !v.is_finite()) || m[0][3] != 0. || m[1][3] != 0. || m[2][3] != 0. || m[3][3] != 1. {
|
||||
return false;
|
||||
}
|
||||
let [x, y, z] = [Vec3::new(m[0][0], m[0][1], m[0][2]), Vec3::new(m[1][0], m[1][1], m[1][2]), Vec3::new(m[2][0], m[2][1], m[2][2])];
|
||||
[x, y, z].iter().all(|v| (v.dot(*v) - 1.).abs() < 1e-9)
|
||||
&& x.dot(y).abs() < 1e-9
|
||||
&& x.dot(z).abs() < 1e-9
|
||||
&& y.dot(z).abs() < 1e-9
|
||||
&& (x.cross(y).dot(z) - 1.).abs() < 1e-9
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn placed_mesh_is_lazy_and_composes_from_one_immutable_source() {
|
||||
let source = ModelBody::new("Part".into(), kernel::box_solid(Vec3::ZERO, Vec3::new(2., 3., 4.)).unwrap(), 1);
|
||||
let a = crate::rigid(Vec3::new(10., 20., 30.), Vec3::ZERO, Vec3::Z, 0.7);
|
||||
let b = crate::rigid(Vec3::new(3., -5., 2.), Vec3::ZERO, Vec3::X, 0.4);
|
||||
let mut first = ModelBody::new("First".into(), kernel::transform_matrix(&source.body, a).unwrap(), 1);
|
||||
first.mesh_placement = Some(Arc::new(PlacedMesh::new(&source, &a).unwrap()));
|
||||
let mut nested = ModelBody::new("Nested".into(), kernel::transform_matrix(&first.body, b).unwrap(), 1);
|
||||
nested.mesh_placement = Some(Arc::new(PlacedMesh::new(&first, &b).unwrap()));
|
||||
assert_eq!(first.display_tol(), source.display_tol());
|
||||
assert_eq!(nested.display_tol(), source.display_tol());
|
||||
assert!(source.cached_mesh().is_none());
|
||||
assert!(first.cached_mesh().is_none());
|
||||
let mesh = nested.mesh();
|
||||
assert!(source.cached_mesh().is_some(), "the canonical source is warmed on demand");
|
||||
assert!(first.cached_mesh().is_none(), "composition does not retain or evaluate intermediate poses");
|
||||
let local = source.mesh();
|
||||
let matrix = crate::mat_mul(&b, &a);
|
||||
for (p, q) in local.positions.iter().zip(&mesh.positions) {
|
||||
assert!(crate::apply_point(&matrix, *p).dist(*q) < 1e-8);
|
||||
}
|
||||
let (other, work) = kernel::profile::capture(|| first.mesh());
|
||||
assert_eq!(work.tessellate_ms, 0.);
|
||||
assert_eq!(other.tri_face, local.tri_face);
|
||||
assert!(Arc::ptr_eq(&mesh, &nested.mesh()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn world_instances_warm_one_definition_and_an_edit_replaces_it() {
|
||||
let mut doc = crate::Document::new("instances");
|
||||
let component = doc.add_component(Some("Part"), 0).unwrap();
|
||||
doc.body_components.insert("Pin".into(), component);
|
||||
let first = doc.occurrences[0].id;
|
||||
doc.occurrences[0].transform = crate::rigid(Vec3::new(10., 0., 0.), Vec3::ZERO, Vec3::Y, 0.3);
|
||||
doc.add_occurrence(component, 0, crate::rigid(Vec3::new(0., 20., 0.), Vec3::ZERO, Vec3::X, 0.6)).unwrap();
|
||||
let mut state = crate::ModelState::default();
|
||||
state.bodies.push(ModelBody::new("Pin".into(), kernel::cylinder(Vec3::ZERO, Vec3::Z, 2., 8.).unwrap(), 1));
|
||||
let mut cache = super::super::PlacedBodies::default();
|
||||
let before = super::super::world_state_reusing(&doc, &state, &mut cache);
|
||||
assert!(state.bodies[0].cached_mesh().is_none(), "placing bodies must remain lazy");
|
||||
let old = before.bodies[0].mesh();
|
||||
assert!(state.bodies[0].cached_mesh().is_some());
|
||||
let (_, work) = kernel::profile::capture(|| before.bodies[1].mesh());
|
||||
assert_eq!(work.tessellate_ms, 0., "the second instance shares the first one's triangulation");
|
||||
doc.occurrences.iter_mut().find(|o| o.id == first).unwrap().transform = crate::rigid(Vec3::new(30., 0., 0.), Vec3::ZERO, Vec3::Y, 0.9);
|
||||
let moved = super::super::world_state_reusing(&doc, &state, &mut cache);
|
||||
let (_, work) = kernel::profile::capture(|| moved.bodies[0].mesh());
|
||||
assert_eq!(work.tessellate_ms, 0., "a new pose shares the same definition");
|
||||
state.bodies[0] = ModelBody::new("Pin".into(), kernel::cylinder(Vec3::ZERO, Vec3::Z, 4., 8.).unwrap(), 1);
|
||||
let edited = super::super::world_state_reusing(&doc, &state, &mut cache);
|
||||
assert!(state.bodies[0].cached_mesh().is_none());
|
||||
let new = edited.bodies[0].mesh();
|
||||
assert!(state.bodies[0].cached_mesh().is_some(), "changed geometry warms its new definition cache");
|
||||
assert_ne!(new.bounds(), moved.bodies[0].mesh().bounds());
|
||||
assert!(Arc::ptr_eq(&old, &before.bodies[0].mesh()), "editing does not mutate older snapshots");
|
||||
}
|
||||
#[test]
|
||||
fn only_proper_rigid_affine_matrices_reuse_reference_meshes() {
|
||||
let source = ModelBody::new("Part".into(), kernel::box_solid(Vec3::ZERO, Vec3::new(2., 3., 4.)).unwrap(), 1);
|
||||
assert!(PlacedMesh::new(&source, &crate::IDENTITY).is_some());
|
||||
for (col, row, value) in [(0, 0, -1.), (0, 0, 2.), (0, 1, 0.5), (0, 3, 0.1), (3, 3, 2.), (3, 0, f64::NAN), (3, 2, f64::INFINITY)] {
|
||||
let mut matrix = crate::IDENTITY;
|
||||
matrix[col][row] = value;
|
||||
assert!(PlacedMesh::new(&source, &matrix).is_none(), "{matrix:?}");
|
||||
}
|
||||
// A positive unit determinant alone is insufficient: this scale changes normals and tolerance.
|
||||
let mut scale = crate::IDENTITY;
|
||||
scale[0][0] = 2.;
|
||||
scale[1][1] = 0.5;
|
||||
assert!(PlacedMesh::new(&source, &scale).is_none());
|
||||
}
|
||||
}
|
||||
@@ -370,7 +370,7 @@ mod tests {
|
||||
sketch.add_circle(Vec2::new(0., 0.), 3., None, Some("circle")).unwrap();
|
||||
let report = solve(&mut sketch);
|
||||
let profiles = find_profiles(&sketch);
|
||||
let ss = SolvedSketch { feature: 1, name: "ordinary circle".into(), plane: Plane::XY, sketch, report, profiles, ends: Default::default() };
|
||||
let ss = SolvedSketch { feature: 1, name: "ordinary circle".into(), plane: Plane::XY, sketch, report, profiles };
|
||||
assert_eq!(ss.profiles.len(), 1);
|
||||
let indices = vec![0; 600];
|
||||
let selection = ProfileSel::Indices { indices: indices.clone() };
|
||||
|
||||
+27
-61
@@ -3,16 +3,13 @@
|
||||
//!
|
||||
//! Model geometry is found again on every recompute: a face or an edge by the persistent name
|
||||
//! its link keeps, anything else (and a link saved without a name) from the reference point
|
||||
//! stored in the link. An edge of a solid lying in a plane parallel to the sketch's comes in as
|
||||
//! its exact curve (a line, an arc, the Bézier pieces of a free-form curve such as a letter
|
||||
//! cut from a font), so a profile drawn round it meets the solid's faces exactly. Anything
|
||||
//! else is fitted to its polyline from the tessellation, which lies exactly on the curve:
|
||||
//! lines and circular arcs are recovered exactly, the rest (an ellipse from a tilted circle,
|
||||
//! a spline edge running out of the plane) becomes a chain of lines.
|
||||
//! stored in the link. Edge polylines from the tessellation lie exactly on their curves,
|
||||
//! so lines and circular arcs are recovered exactly by fitting; anything else (an ellipse from
|
||||
//! a tilted circle, a spline edge) becomes a chain of lines.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use solidcraft_geom::{Mesh, Plane, Seg2, Vec2, Vec3};
|
||||
use solidcraft_geom::{Mesh, Plane, Vec2, Vec3};
|
||||
use solidcraft_sketch::{CurveKind, LinkGeom, LinkKind, LinkSource, Sketch};
|
||||
|
||||
use crate::eval::ModelState;
|
||||
@@ -120,8 +117,7 @@ fn resolve_uncached(
|
||||
let name = st.bodies.get(bi).map(|b| b.name.clone()).unwrap_or_else(|| body.clone());
|
||||
source = LinkSource::Edge { body: name, at: q };
|
||||
edge_index = Some(e);
|
||||
let exact = st.bodies.get(bi).and_then(|b| exact_edges(b, plane, &[e]).pop().flatten());
|
||||
edge_geom(plane, kind, exact, mesh.edges.get(e).cloned().unwrap_or_default(), &mut wires)
|
||||
edge_geom(plane, kind, mesh.edges.get(e).cloned().unwrap_or_default(), &mut wires)
|
||||
}
|
||||
LinkSource::Face { body, at } => {
|
||||
let (bi, mesh) = body_mesh(st, body, *at)?;
|
||||
@@ -137,8 +133,13 @@ fn resolve_uncached(
|
||||
match kind {
|
||||
LinkKind::Intersect => section(plane, &mesh, Some(f)),
|
||||
_ => {
|
||||
let mut g = Vec::new();
|
||||
let target = st.bodies.get(bi).ok_or_else(|| lost("the projection body"))?;
|
||||
let g = projected_edges(target, &mesh, plane, &projection_face_edges(target, &mesh, f, plane)?);
|
||||
for e in projection_face_edges(target, &mesh, f, plane)? {
|
||||
if let Some(pts) = mesh.edges.get(e) {
|
||||
g.extend(fit_world(plane, pts));
|
||||
}
|
||||
}
|
||||
if certified_planar_construction(target, f as usize) { merge_family_lines(g)? } else { g }
|
||||
}
|
||||
}
|
||||
@@ -281,62 +282,19 @@ fn off_plane(plane: &Plane, pts: &[Vec3]) -> bool {
|
||||
pts.iter().any(|p| plane.height(*p).abs() > size * 1e-7)
|
||||
}
|
||||
|
||||
/// What a body edge (its `exact` curve when read, and its polyline `pts`) gives a sketch: its
|
||||
/// section with the sketch plane, itself as a 3D curve (into `wires`) when it is included and
|
||||
/// leaves the plane, else its projection (the exact curve, or the polyline fitted).
|
||||
fn edge_geom(plane: &Plane, kind: LinkKind, exact: Option<Vec<LinkGeom>>, pts: Vec<Vec3>, wires: &mut Vec<Vec<Vec3>>) -> Vec<LinkGeom> {
|
||||
/// What a body edge (its points) gives a sketch: its section with the sketch plane, itself as a
|
||||
/// 3D curve (into `wires`) when it is included and leaves the plane, else its projection.
|
||||
fn edge_geom(plane: &Plane, kind: LinkKind, pts: Vec<Vec3>, wires: &mut Vec<Vec<Vec3>>) -> Vec<LinkGeom> {
|
||||
match kind {
|
||||
LinkKind::Intersect => section_of_polyline(plane, &pts),
|
||||
LinkKind::Include if off_plane(plane, &pts) => {
|
||||
wires.push(pts);
|
||||
Vec::new()
|
||||
}
|
||||
_ => exact.unwrap_or_else(|| fit_world(plane, &pts)),
|
||||
_ => fit_world(plane, &pts),
|
||||
}
|
||||
}
|
||||
|
||||
/// Edges `edges` of `body` (indices into its mesh's `edges`) on `plane` as their exact curves:
|
||||
/// lines, circles and arcs, and the Bézier pieces of a free-form curve. `None` for an edge that
|
||||
/// doesn't lie in a plane parallel to `plane` or isn't read exactly (a mesh body's): it is
|
||||
/// fitted to its polyline instead.
|
||||
fn exact_edges(body: &crate::ModelBody, plane: &Plane, edges: &[usize]) -> Vec<Option<Vec<LinkGeom>>> {
|
||||
body.body.planar_edges(plane, edges).into_iter().map(|segs| segs?.iter().map(link_geom).collect()).collect()
|
||||
}
|
||||
|
||||
/// `edges` of `body` projected on `plane`: each exactly ([`exact_edges`]) or fitted to its
|
||||
/// polyline in `mesh`.
|
||||
fn projected_edges(body: &crate::ModelBody, mesh: &Mesh, plane: &Plane, edges: &[usize]) -> Vec<LinkGeom> {
|
||||
let mut out = Vec::new();
|
||||
for (e, exact) in edges.iter().zip(exact_edges(body, plane, edges)) {
|
||||
match exact {
|
||||
Some(g) => out.extend(g),
|
||||
None => {
|
||||
if let Some(pts) = mesh.edges.get(*e) {
|
||||
out.extend(fit_world(plane, pts));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// A boundary segment as the link geometry that is the same curve.
|
||||
fn link_geom(s: &Seg2) -> Option<LinkGeom> {
|
||||
Some(match *s {
|
||||
Seg2::Line { a, b } => LinkGeom::Line(a, b),
|
||||
Seg2::Arc { center, radius, sweep, .. } if sweep.abs() >= std::f64::consts::TAU - 1e-9 => LinkGeom::Circle(center, radius),
|
||||
Seg2::Arc { center, radius, start, sweep } => {
|
||||
let at = |t: f64| center + Vec2::new(t.cos(), t.sin()) * radius;
|
||||
// Counter-clockwise from `a` to `b`.
|
||||
let (a, b) = if sweep > 0. { (at(start), at(start + sweep)) } else { (at(start + sweep), at(start)) };
|
||||
LinkGeom::Arc { c: center, a, b }
|
||||
}
|
||||
Seg2::Conic { a, apex, b, w } => LinkGeom::Conic { a, apex, b, rho: w / (1. + w) },
|
||||
Seg2::Cubic { p0, p1, p2, p3 } => LinkGeom::Spline { pts: vec![p0, p1, p2, p3], control: true, degree: 3, periodic: false },
|
||||
Seg2::Bezier { n, p } => LinkGeom::Spline { pts: p.get(..=usize::from(n))?.to_vec(), control: true, degree: n, periodic: false },
|
||||
})
|
||||
}
|
||||
|
||||
/// The persistent name of the edge an `Edge` reference resolved to, for its link to keep and
|
||||
/// be found by from then on. None on a mesh body, whose edges have no names.
|
||||
pub fn resolved_edge_name(st: &ModelState, r: &Resolved) -> Option<String> {
|
||||
@@ -373,8 +331,7 @@ fn resolve_named_edge(st: &ModelState, plane: &Plane, kind: LinkKind, body: &str
|
||||
.min_by(|a, b| a.dist(near).total_cmp(&b.dist(near)))
|
||||
.ok_or_else(|| lost("named projection edge"))?;
|
||||
let mut wires = Vec::new();
|
||||
let exact = exact_edges(target, plane, &[index]).pop().flatten();
|
||||
let geom = edge_geom(plane, kind, exact, pts, &mut wires);
|
||||
let geom = edge_geom(plane, kind, pts, &mut wires);
|
||||
if geom.is_empty() && wires.is_empty() {
|
||||
return Err(DocError::Invalid("named edge projects to nothing on this sketch plane".into()));
|
||||
}
|
||||
@@ -393,7 +350,12 @@ fn resolve_named_face(st: &ModelState, plane: &Plane, kind: LinkKind, body: &str
|
||||
let geom = if kind == LinkKind::Intersect {
|
||||
section(plane, &mesh, Some(index))
|
||||
} else {
|
||||
let geom = projected_edges(target, &mesh, plane, &projection_face_edges(target, &mesh, index, plane)?);
|
||||
let mut geom = Vec::new();
|
||||
for edge in projection_face_edges(target, &mesh, index, plane)? {
|
||||
if let Some(points) = mesh.edges.get(edge) {
|
||||
geom.extend(fit_world(plane, points));
|
||||
}
|
||||
}
|
||||
let geom = merge_split_circles(geom);
|
||||
if certified_planar_construction(target, index as usize) { merge_family_lines(geom)? } else { geom }
|
||||
};
|
||||
@@ -463,7 +425,11 @@ pub fn resolve_face_indices(st: &ModelState, plane: &Plane, kind: LinkKind, body
|
||||
} else {
|
||||
order::family_edges(&mesh, &selected, plane)?
|
||||
};
|
||||
geometry.extend(projected_edges(body, &mesh, plane, &edges));
|
||||
for edge in edges {
|
||||
if let Some(points) = mesh.edges.get(edge) {
|
||||
geometry.extend(fit_world(plane, points));
|
||||
}
|
||||
}
|
||||
}
|
||||
if kind != LinkKind::Intersect && (indices.len() > 1 || indices.first().is_some_and(|index| certified_planar_construction(body, *index))) {
|
||||
geometry = merge_family_lines(geometry)?;
|
||||
|
||||
@@ -290,7 +290,7 @@ fn rule_hem_gap() -> String {
|
||||
}
|
||||
|
||||
impl SheetRule {
|
||||
/// Default: Steel (mm), 2.5 mm, K 0.44, bend radius = Thickness.
|
||||
/// Fusion's default: Steel (mm), 2.5 mm, K 0.44, bend radius = Thickness.
|
||||
pub fn steel() -> SheetRule {
|
||||
SheetRule {
|
||||
name: "Steel (mm)".into(),
|
||||
@@ -1989,7 +1989,7 @@ impl SheetBody {
|
||||
.sum()
|
||||
}
|
||||
|
||||
/// Topology counted by faces and loops: a round hole is one face bounded by two closed circular
|
||||
/// Topology counted as Fusion does: a round hole is one face bounded by two closed circular
|
||||
/// edges (one vertex each), where the sewn solid splits it into arcs. Returns the change to
|
||||
/// the sewn (faces, edges, vertices).
|
||||
pub fn round_hole_correction(&self) -> (i64, i64, i64) {
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::f64::consts::{FRAC_PI_2, PI, TAU};
|
||||
|
||||
use solidcraft_geom::{Loop2, Mesh, Plane, Region2, Seg2, UnitVec3, Vec2, Vec3};
|
||||
use solidcraft_geom::{Loop2, Mesh, Plane, Region2, Seg2, Vec2, Vec3};
|
||||
use solidcraft_kernel::{Body, FaceSurface, face_surface};
|
||||
|
||||
use super::{FlatWork, ReliefOverride, RuleValues, SheetBody, SheetFlange, base_flange, point_in, polys_cross, signed_area, snap};
|
||||
@@ -71,7 +71,7 @@ fn at(p: Vec3) -> String {
|
||||
/// arc where that face is a round wall (`round[i]`: the circle's centre and the arc's middle).
|
||||
/// `holes`: its inner loops, in `plane`, each with the faces of its walls.
|
||||
struct Face {
|
||||
n: UnitVec3,
|
||||
n: Vec3,
|
||||
pts: Vec<Vec3>,
|
||||
across: Vec<Option<usize>>,
|
||||
round: Vec<Option<(Vec3, Vec3)>>,
|
||||
@@ -89,7 +89,7 @@ struct Panel {
|
||||
}
|
||||
|
||||
/// Area of a loop in space, positive when it runs counter-clockwise about `n`.
|
||||
fn area_about(pts: &[Vec3], n: UnitVec3) -> f64 {
|
||||
fn area_about(pts: &[Vec3], n: Vec3) -> f64 {
|
||||
let Some(&o) = pts.first() else { return 0.0 };
|
||||
pts.windows(2).map(|w| (w[0] - o).cross(w[1] - o).dot(n)).sum::<f64>() / 2.0
|
||||
}
|
||||
@@ -227,7 +227,7 @@ fn outline(b: &Body, mesh: &Mesh, f: usize, tol: f64) -> Result<Face> {
|
||||
loops.push(lp);
|
||||
}
|
||||
let mut walls = std::collections::HashMap::new();
|
||||
let mut wall = |g: u32| *walls.entry(g).or_insert_with(|| round_wall(b, &plane, *n, g));
|
||||
let mut wall = |g: u32| *walls.entry(g).or_insert_with(|| round_wall(b, &plane, n, g));
|
||||
// Each loop with the faces of its walls.
|
||||
let mut shaped: Vec<(Vec<(Seg2, Option<u32>)>, Vec<usize>)> = loops
|
||||
.iter()
|
||||
@@ -388,7 +388,7 @@ fn bend_cylinder(b: &Body, mesh: &Mesh, f: usize, tol: f64) -> Option<Chamfer> {
|
||||
let Some(FaceSurface::Plane { normal: n, .. }) = face_surface(b, g) else { continue };
|
||||
let radial = (p - origin) - axis * (p - origin).dot(axis);
|
||||
if radial.normalized().is_some_and(|out| n.dot(out).abs() >= 1.0 - 1e-6) {
|
||||
sides.push((g, *n));
|
||||
sides.push((g, n));
|
||||
}
|
||||
}
|
||||
let &[(a, na), (bb, nb)] = sides.as_slice() else { return None };
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user