mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 10:35:58 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 133569e695 |
@@ -1,5 +1,6 @@
|
||||
# cargo-nextest settings for CI (`cargo xtask test` does not use nextest).
|
||||
# CI builds the tests once and runs them in one job on a 32 vCPU runner (.github/workflows/ci.yml).
|
||||
# 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).
|
||||
|
||||
[profile.ci]
|
||||
# Heavy tests (one to twelve minutes each on a CI runner) that pull requests skip. They run on
|
||||
@@ -33,14 +34,12 @@ slow-timeout = { period = "60s" }
|
||||
[profile.ci.junit]
|
||||
path = "junit.xml"
|
||||
|
||||
# The action matrix sample runs its designs in parallel workers (ACTION_MATRIX_JOBS, set to match in
|
||||
# ci.yml). Its workers reserve that many of nextest's threads, so the cores are not oversubscribed
|
||||
# (a pi-case hole that takes 8 s alone went past the matrix's 90 s action limit when they were),
|
||||
# and it starts first, beside the other slow tests.
|
||||
# The action matrix sample runs its designs in parallel worker processes, one per CPU. Run with
|
||||
# other tests beside it (the heavy ones on main), a pi-case hole that takes 8 s alone (#952 makes
|
||||
# it a real hole) went past the matrix's 90 s action limit. It runs on its own.
|
||||
[[profile.ci.overrides]]
|
||||
filter = "binary(action_matrix) & test(=action_matrix_sample)"
|
||||
threads-required = 12
|
||||
priority = 100
|
||||
threads-required = "num-cpus"
|
||||
|
||||
|
||||
# The slowest tests start first, so a shard does not end with one of them running alone on a
|
||||
|
||||
@@ -7,5 +7,4 @@ self-hosted-runner:
|
||||
- blacksmith-4vcpu-ubuntu-2404
|
||||
- blacksmith-8vcpu-ubuntu-2404
|
||||
- blacksmith-16vcpu-ubuntu-2404
|
||||
- blacksmith-32vcpu-ubuntu-2404
|
||||
- blacksmith-8vcpu-windows-2025
|
||||
|
||||
+47
-116
@@ -8,13 +8,12 @@
|
||||
# whole workspace, as do pushes to main that change code, the nightly run and Run workflow.
|
||||
# Documentation, packaging, the other workflows and the GitHub bots' scripts select nothing:
|
||||
# no native job runs for them, on pull requests or on main.
|
||||
# Tests are built and run in one job on a big runner, slowest first, starting beside the repository
|
||||
# checks (no archive to upload and download). A few heavy tests (see .config/nextest.toml) are
|
||||
# Tests are built once and run in 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), the reference tests and the doc tests run on main and nightly only, not on
|
||||
# pull requests (the fast lane, in Select checks); a break there is fixed forward on main. Only
|
||||
# main saves the build caches; pull requests restore them. sccache is on a Blacksmith sticky disk that
|
||||
# only main commits.
|
||||
# main saves the build caches; pull requests restore them. On pull requests the test build and the
|
||||
# shards start only after the format and repository checks pass.
|
||||
# A push to main runs only the repository checks when a passing merge-queue run tested that very
|
||||
# commit, or when nothing a build reads changed since the last push to main whose run passed.
|
||||
# Otherwise it runs everything.
|
||||
@@ -30,13 +29,14 @@
|
||||
# reads `ci` from the newest run of a commit, so a run without it would block a PR that passed.
|
||||
# Cross-compilation is opt-in through Run workflow; release builds remain in release.yml.
|
||||
# Every Linux job runs on Blacksmith's Ubuntu 24.04 runners (blacksmith-<N>vcpu-ubuntu-2404), sized
|
||||
# per job: 32 vCPU for the tests, 16 for Clippy, the archive build (nightly) and the merge queue's, 8 for compiles, doc and reference tests, 4 for the light checks, 2 for Select checks and ci.
|
||||
# per job: 16 vCPU for Build tests, Clippy and the merge queue's test shards, 8 for compiles, the
|
||||
# other test shards, doc and reference tests, 4 for the light checks, 2 for Select checks and ci.
|
||||
# The Windows shader job runs on Blacksmith's Windows Server 2025 runners (blacksmith-8vcpu-windows-2025);
|
||||
# the Windows app startup smoke stays on windows-latest, which has a desktop session that presents frames.
|
||||
# Pull requests, forks included, the merge queue, pushes to main and the nightly run all use them:
|
||||
# the runners are ephemeral, fork runs get no secrets, and main saves the build caches that every
|
||||
# other run restores read-only. Nightly and by hand, Build tests also makes an archive the matrix
|
||||
# jobs run; Blacksmith's image matches ubuntu-24.04, so the archive and the recorded goldens behave the
|
||||
# other run restores read-only. The tests are built once (Build tests) into an archive the shards
|
||||
# run; Blacksmith's image matches ubuntu-24.04, so the archive and the recorded goldens behave the
|
||||
# same.
|
||||
name: CI
|
||||
|
||||
@@ -49,7 +49,7 @@ on:
|
||||
# The merge queue: each queued group runs the full suite before it lands on main.
|
||||
merge_group:
|
||||
push:
|
||||
branches: [main, kernel-smlib] # kernel-smlib: full suite after each merge into the kernel branch
|
||||
branches: [main]
|
||||
schedule:
|
||||
- cron: "17 6 * * *"
|
||||
workflow_dispatch:
|
||||
@@ -178,12 +178,9 @@ jobs:
|
||||
git diff --no-renames --name-only -z "$BASE_SHA...$HEAD_SHA" > "$RUNNER_TEMP/ci-paths"
|
||||
while IFS= read -r -d '' path; do
|
||||
case "$path" in crates/kernel/*|vendor/truck-*) kernel=true ;; esac
|
||||
# What a pull request re-checks the goldens for: the kernel, truck and the goldens
|
||||
# themselves (a re-record is checked before it is queued). The goldens are frozen, so
|
||||
# on any other change they would only re-check unchanged results; the merge queue,
|
||||
# main and the nightly run every seed before anything lands.
|
||||
# What can change a robustness golden: the kernel and the crates a design is built with.
|
||||
case "$path" in
|
||||
crates/kernel/*|vendor/truck-*|tests/robustness/*) goldens=true ;;
|
||||
crates/kernel/*|crates/engine/*|crates/doc/*|crates/sketch/*|crates/geom/*|vendor/truck-*|tests/robustness/*) goldens=true ;;
|
||||
esac
|
||||
# What the Windows D3D12 jobs exercise: the GPU crate and its shaders, the app and
|
||||
# its startup, the viewport's wgpu glue, the vendored egui-wgpu, their scripts.
|
||||
@@ -340,33 +337,25 @@ jobs:
|
||||
|
||||
build-tests:
|
||||
name: Build tests
|
||||
# The archive is for the action matrix and the property tests, which run only nightly and by
|
||||
# hand. Pull requests, the merge queue and main build and run their tests in the `tests` job.
|
||||
needs: changes
|
||||
if: ${{ !cancelled() && needs.changes.result == 'success' && needs.changes.outputs.native == 'true' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') }}
|
||||
# Fail fast on pull requests: the test archive (and so the shards) waits for the repository
|
||||
# checks, the most common cheap failure; heavy jobs used to run on to the end beside a failed
|
||||
# format check. Main, nightly and Run workflow build regardless, so the action matrix and the
|
||||
# property tests that hang off this job keep their signal.
|
||||
needs: [changes, checks]
|
||||
if: ${{ !cancelled() && needs.changes.result == 'success' && needs.changes.outputs.native == 'true' && (github.event_name != 'pull_request' || needs.checks.result == 'success') }}
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
# Fork builds start from main's cache, read-only; give them room to finish and upload.
|
||||
timeout-minutes: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) && 60 || 45 }}
|
||||
env:
|
||||
CARGO_INCREMENTAL: "0"
|
||||
RUSTC_WRAPPER: sccache
|
||||
SCCACHE_DIR: /home/runner/.cache/sccache
|
||||
SCCACHE_CACHE_SIZE: 20G
|
||||
SCCACHE_GHA_ENABLED: "true"
|
||||
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
|
||||
# sccache lives on a Blacksmith sticky disk (an ext4 volume cloned from the last snapshot,
|
||||
# no 10 GB limit and no download). Only main commits it; pull requests, the merge queue and
|
||||
# forks mount a clone and never write back, so nothing they build can reach main's cache.
|
||||
- name: Mount the compiler cache
|
||||
uses: useblacksmith/stickydisk@3f6be1451e3cad893b778bc93495c80452a5da26 # v1.7.2
|
||||
continue-on-error: true
|
||||
with:
|
||||
key: ${{ github.repository }}-sccache-archive
|
||||
path: /home/runner/.cache/sccache
|
||||
commit: ${{ github.ref == 'refs/heads/main' && 'on-change' || 'false' }}
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: native-tests
|
||||
@@ -398,94 +387,63 @@ jobs:
|
||||
run: sccache --show-stats
|
||||
|
||||
tests:
|
||||
name: Native Linux tests
|
||||
needs: changes
|
||||
if: ${{ !cancelled() && needs.changes.result == 'success' && needs.changes.outputs.native == 'true' }}
|
||||
# Builds the tests and runs them in one job on a big runner: there is no archive to upload
|
||||
# and download (3 GB on a full run), and the job starts beside the repository checks, not
|
||||
# after them. nextest hands tests to its threads as they free up, the slowest first (priority
|
||||
# in .config/nextest.toml), so they start at once and no core idles at the end.
|
||||
runs-on: blacksmith-32vcpu-ubuntu-2404
|
||||
name: Native Linux tests (${{ matrix.shard }}/4)
|
||||
needs: [changes, checks, build-tests]
|
||||
if: ${{ !cancelled() && needs.changes.outputs.native == 'true' && (github.event_name != 'pull_request' || needs.checks.result == 'success') && needs.build-tests.result == 'success' }}
|
||||
runs-on: ${{ github.event_name == 'merge_group' && 'blacksmith-16vcpu-ubuntu-2404' || 'blacksmith-8vcpu-ubuntu-2404' }}
|
||||
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]') }}
|
||||
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
|
||||
# would skip for want of an adapter fails instead.
|
||||
SOLIDCRAFT_GPU: software
|
||||
SOLIDCRAFT_REQUIRE_GPU: "1"
|
||||
ACTION_MATRIX_JOBS: "12"
|
||||
CARGO_INCREMENTAL: "0"
|
||||
RUSTC_WRAPPER: sccache
|
||||
SCCACHE_DIR: /home/runner/.cache/sccache
|
||||
SCCACHE_CACHE_SIZE: 20G
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Install a software Vulkan driver (lavapipe)
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends mesa-vulkan-drivers libvulkan1
|
||||
- name: Size parallel compiles to the job's memory
|
||||
run: scripts/ci/build-jobs.sh
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
# sccache lives on a Blacksmith sticky disk (an ext4 volume cloned from the last snapshot,
|
||||
# no 10 GB limit and no download). Only main commits it; pull requests, the merge queue and
|
||||
# forks mount a clone and never write back, so nothing they build can reach main's cache.
|
||||
- name: Mount the compiler cache
|
||||
uses: useblacksmith/stickydisk@3f6be1451e3cad893b778bc93495c80452a5da26 # v1.7.2
|
||||
continue-on-error: true
|
||||
with:
|
||||
key: ${{ github.repository }}-sccache-tests
|
||||
path: /home/runner/.cache/sccache
|
||||
commit: ${{ github.ref == 'refs/heads/main' && 'on-change' || 'false' }}
|
||||
# Main saves the cache; every other run restores it, read-only.
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: native-tests
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: Set up compiler cache
|
||||
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
|
||||
with:
|
||||
version: v0.16.0
|
||||
- uses: taiki-e/install-action@1168f6810a3cb30a3514bf5756a3b7f0422c6df8 # v2.87.28
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
# Each shard runs the archive the Build tests job made.
|
||||
- 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 }}
|
||||
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); `default()`
|
||||
# below is that filter, so a pull request still runs only what it ran before.
|
||||
base='default()'
|
||||
if [ "$EVENT_NAME" = pull_request ]; then
|
||||
# The action matrix sample (about 6 minutes on all cores) runs in the merge queue, on
|
||||
# main and nightly, not on every pull request push.
|
||||
base='default() & not (binary(action_matrix) & test(=action_matrix_sample))'
|
||||
fi
|
||||
# Pull requests skip the heavy tests (default-filter in .config/nextest.toml).
|
||||
filter=()
|
||||
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.
|
||||
base='not (package(solidcraft-kernel) & binary(boolean_props))'
|
||||
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.
|
||||
base="(default() & not (binary(action_matrix) & test(=action_matrix_sample))) | (package(solidcraft-engine) & test(/^tests::robustness_goldens::/) & not test(/^tests::robustness_goldens::seed_($SLOW_SEEDS)\$/))"
|
||||
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
|
||||
rm -f "$junit"
|
||||
# Pull requests test the affected packages (scripts/ci/affected.py).
|
||||
scope=(--workspace)
|
||||
if [ "$FULL" != true ]; then scope=(); for p in $PACKAGES; do scope+=(-p "$p"); done; fi
|
||||
status=0
|
||||
cargo nextest run "${scope[@]}" --cargo-profile ci --locked --profile ci --no-tests=pass \
|
||||
--ignore-default-filter -E "$base" || status=1
|
||||
if [ "$status" != 0 ]; then
|
||||
# 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)
|
||||
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:"
|
||||
@@ -493,14 +451,11 @@ jobs:
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
- name: Compiler cache statistics
|
||||
if: always()
|
||||
run: sccache --show-stats
|
||||
- name: Upload test timings
|
||||
if: always()
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: junit
|
||||
name: junit-${{ matrix.shard }}
|
||||
path: ${{ env.CARGO_TARGET_DIR || 'target' }}/nextest/ci/junit.xml
|
||||
retention-days: 7
|
||||
|
||||
@@ -605,23 +560,13 @@ jobs:
|
||||
env:
|
||||
CARGO_INCREMENTAL: "0"
|
||||
RUSTC_WRAPPER: sccache
|
||||
SCCACHE_DIR: /home/runner/.cache/sccache
|
||||
SCCACHE_CACHE_SIZE: 20G
|
||||
SCCACHE_GHA_ENABLED: "true"
|
||||
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
|
||||
# sccache lives on a Blacksmith sticky disk (an ext4 volume cloned from the last snapshot,
|
||||
# no 10 GB limit and no download). Only main commits it; pull requests, the merge queue and
|
||||
# forks mount a clone and never write back, so nothing they build can reach main's cache.
|
||||
- name: Mount the compiler cache
|
||||
uses: useblacksmith/stickydisk@3f6be1451e3cad893b778bc93495c80452a5da26 # v1.7.2
|
||||
continue-on-error: true
|
||||
with:
|
||||
key: ${{ github.repository }}-sccache-doc
|
||||
path: /home/runner/.cache/sccache
|
||||
commit: ${{ github.ref == 'refs/heads/main' && 'on-change' || 'false' }}
|
||||
# Main saves the cache; every other run restores it, read-only.
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
@@ -663,23 +608,13 @@ jobs:
|
||||
env:
|
||||
CARGO_INCREMENTAL: "0"
|
||||
RUSTC_WRAPPER: sccache
|
||||
SCCACHE_DIR: /home/runner/.cache/sccache
|
||||
SCCACHE_CACHE_SIZE: 20G
|
||||
SCCACHE_GHA_ENABLED: "true"
|
||||
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
|
||||
# sccache lives on a Blacksmith sticky disk (an ext4 volume cloned from the last snapshot,
|
||||
# no 10 GB limit and no download). Only main commits it; pull requests, the merge queue and
|
||||
# forks mount a clone and never write back, so nothing they build can reach main's cache.
|
||||
- name: Mount the compiler cache
|
||||
uses: useblacksmith/stickydisk@3f6be1451e3cad893b778bc93495c80452a5da26 # v1.7.2
|
||||
continue-on-error: true
|
||||
with:
|
||||
key: ${{ github.repository }}-sccache-oracle
|
||||
path: /home/runner/.cache/sccache
|
||||
commit: ${{ github.ref == 'refs/heads/main' && 'on-change' || 'false' }}
|
||||
# Main saves the cache; every other run restores it, read-only.
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
@@ -811,7 +746,6 @@ jobs:
|
||||
DOCTESTS: ${{ needs.changes.outputs.doctests }}
|
||||
CLIPPY: ${{ needs.clippy.result }}
|
||||
BUILD: ${{ needs.build-tests.result }}
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
TESTS: ${{ needs.tests.result }}
|
||||
DOCS: ${{ needs.doc-tests.result }}
|
||||
ORACLE: ${{ needs.oracle.result }}
|
||||
@@ -826,10 +760,7 @@ jobs:
|
||||
fi
|
||||
if [ "$NATIVE" = true ]; then
|
||||
test "$CLIPPY" = success
|
||||
# The archive for the action matrix and property tests exists nightly and by hand only.
|
||||
if [ "$EVENT_NAME" = schedule ] || [ "$EVENT_NAME" = workflow_dispatch ]; then
|
||||
test "$BUILD" = success
|
||||
fi
|
||||
test "$BUILD" = success
|
||||
test "$TESTS" = success
|
||||
fi
|
||||
if [ "$DOCTESTS" = true ]; then
|
||||
|
||||
@@ -57,23 +57,6 @@ Editable drawing code, native projects and shipped exports are collected under `
|
||||
| docs/book/src/generated/tool-preview.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/tool-preview.json`; gitignored) | MIT OR Apache-2.0 |
|
||||
| assets/fonts/liberation-sans/LiberationSans-Regular.ttf | Red Hat, Inc. (Liberation Fonts 2.x; digitized data Google) | https://github.com/liberationfonts (Debian fonts-liberation), unmodified; licence text in assets/fonts/liberation-sans/OFL.txt; used for sketch Text outlines | SIL OFL 1.1 |
|
||||
| assets/fonts/biz-udp-gothic/BIZUDPGothic-Regular.ttf | The BIZ UDGothic Project Authors | https://github.com/googlefonts/morisawa-biz-ud-gothic (Google Fonts), unmodified; licence text in assets/fonts/biz-udp-gothic/OFL.txt; used for web UI Japanese font | SIL OFL 1.1 |
|
||||
| tests/step-conformance/cone_point.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
|
||||
| tests/step-conformance/cone_point_vertex.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
|
||||
| tests/step-conformance/cylinder.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
|
||||
| tests/step-conformance/cylinder_inch.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
|
||||
| tests/step-conformance/cylinder_nurbs.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
|
||||
| tests/step-conformance/cylinder_placed.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
|
||||
| tests/step-conformance/cylinder_seam.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
|
||||
| tests/step-conformance/cylinder_twice.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
|
||||
| tests/step-conformance/dome.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
|
||||
| tests/step-conformance/dome_nurbs.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
|
||||
| tests/step-conformance/far_cylinder.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
|
||||
| tests/step-conformance/grooved_shaft.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
|
||||
| tests/step-conformance/plate_100_holes.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
|
||||
| tests/step-conformance/sheet_disc.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
|
||||
| tests/step-conformance/tiny_pin.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
|
||||
| tests/step-conformance/torus_ring_nurbs.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
|
||||
| tests/step-conformance/washer.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
|
||||
| tests/oracle/17-loft/reference.stl | SolidCraft contributors | triangulation of our own recipe (tests/oracle/17-loft/recipe.json) built with FreeCAD as an external tool by tools/freecad-oracle | MIT OR Apache-2.0 |
|
||||
| tests/oracle/23-bracket-multifeature/reference.stl | SolidCraft contributors | triangulation of our own recipe (tests/oracle/23-bracket-multifeature/recipe.json) built with FreeCAD as an external tool by tools/freecad-oracle | MIT OR Apache-2.0 |
|
||||
| tests/oracle/28-blind-pocket-fillets/reference.stl | SolidCraft contributors | triangulation of our own recipe (tests/oracle/28-blind-pocket-fillets/recipe.json) built with FreeCAD as an external tool by tools/freecad-oracle | MIT OR Apache-2.0 |
|
||||
@@ -123,23 +106,17 @@ 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/hu.json | SolidCraft contributors | Original Hungarian translation of SolidCraft's English catalog; no commercial CAD text used | MIT OR Apache-2.0 |
|
||||
| assets/locales/id.json | SolidCraft contributors | Original Indonesian translation of SolidCraft's English catalog; no commercial CAD text used | 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/nl.json | SolidCraft contributors | Original Dutch translation of SolidCraft's English catalog; no commercial CAD text used | MIT OR Apache-2.0 |
|
||||
| assets/locales/pl.json | cyryllo | Polish translation of the English catalog, contributed upstream | MIT OR Apache-2.0 |
|
||||
| assets/locales/ru.json | Alex Pro (del000) | Russian translation of the English catalog, contributed upstream | MIT OR Apache-2.0 |
|
||||
| assets/locales/sv.json | SolidCraft contributors | Original Swedish translation of SolidCraft's English catalog; no commercial CAD text used | 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/ro.json | SolidCraft contributors | Original Romanian translation of SolidCraft's English catalog; no commercial CAD text used | MIT OR Apache-2.0 |
|
||||
| assets/locales/tr.json | macrulz54 | Turkish translation of the English catalog, contributed upstream | MIT OR Apache-2.0 |
|
||||
| assets/locales/uk.json | Igor Bronovskyi (BrunIF) | Ukrainian translation of the English catalog, contributed upstream | MIT OR Apache-2.0 |
|
||||
| assets/locales/vi.json | vandang890615 | Vietnamese translation of the English catalog, contributed upstream | MIT OR Apache-2.0 |
|
||||
| assets/locales/zh.json | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
| assets/locales/zh-TW.json | SolidCraft contributors | Original Traditional Chinese (Taiwan) translation of SolidCraft's English catalog; no commercial CAD text used | MIT OR Apache-2.0 |
|
||||
| assets/materials/library.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
| assets/platforms/linux/ai.storyteller.solidcraft.desktop | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
|
||||
| assets/platforms/linux/ai.storyteller.solidcraft.metainfo.xml.in | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
|
||||
|
||||
+1
-1
@@ -199,7 +199,7 @@ Check that validation after merging. They are listed in
|
||||
`.config/nextest.toml` and the [testing page](docs/book/src/dev/testing.md):
|
||||
|
||||
- `solidcraft-engine`: `tests::hostile_params_never_panic`, everything in `tests::robustness` and
|
||||
the seeds of `tests::robustness_goldens` (`seed_00` … `seed_95`; pull requests that change the kernel, truck or the goldens run all but the slowest, `SLOW_SEEDS` in `ci.yml`),
|
||||
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`),
|
||||
`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
|
||||
|
||||
Generated
+17
-17
@@ -4162,7 +4162,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft"
|
||||
version = "0.18.1"
|
||||
version = "0.18.0"
|
||||
dependencies = [
|
||||
"block2 0.6.2",
|
||||
"eframe",
|
||||
@@ -4193,11 +4193,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-3dx-client"
|
||||
version = "0.18.1"
|
||||
version = "0.18.0"
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-cli"
|
||||
version = "0.18.1"
|
||||
version = "0.18.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"serde_json",
|
||||
@@ -4208,7 +4208,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-derive"
|
||||
version = "0.18.1"
|
||||
version = "0.18.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -4217,7 +4217,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-doc"
|
||||
version = "0.18.1"
|
||||
version = "0.18.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
@@ -4234,7 +4234,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-engine"
|
||||
version = "0.18.1"
|
||||
version = "0.18.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"schemars",
|
||||
@@ -4253,7 +4253,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-geom"
|
||||
version = "0.18.1"
|
||||
version = "0.18.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -4262,7 +4262,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-gpu"
|
||||
version = "0.18.1"
|
||||
version = "0.18.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"naga",
|
||||
@@ -4279,7 +4279,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-io"
|
||||
version = "0.18.1"
|
||||
version = "0.18.0"
|
||||
dependencies = [
|
||||
"cadmpeg-core",
|
||||
"cadmpeg-ir",
|
||||
@@ -4302,7 +4302,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-kernel"
|
||||
version = "0.18.1"
|
||||
version = "0.18.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"proptest",
|
||||
@@ -4322,7 +4322,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-mcp"
|
||||
version = "0.18.1"
|
||||
version = "0.18.0"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"solidcraft-engine",
|
||||
@@ -4331,7 +4331,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-render"
|
||||
version = "0.18.1"
|
||||
version = "0.18.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"moxcms",
|
||||
@@ -4345,7 +4345,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-sketch"
|
||||
version = "0.18.1"
|
||||
version = "0.18.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -4356,14 +4356,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-space-mouse"
|
||||
version = "0.18.1"
|
||||
version = "0.18.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-ui-egui"
|
||||
version = "0.18.1"
|
||||
version = "0.18.0"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"egui",
|
||||
@@ -4390,7 +4390,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-web"
|
||||
version = "0.18.1"
|
||||
version = "0.18.0"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"egui",
|
||||
@@ -5920,7 +5920,7 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.18.1"
|
||||
version = "0.18.0"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"sha2 0.10.9",
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ members = ["crates/*", "apps/*", "xtask"]
|
||||
exclude = ["vendor"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.18.1"
|
||||
version = "0.18.0"
|
||||
edition = "2024"
|
||||
license = "MIT OR Apache-2.0"
|
||||
rust-version = "1.95"
|
||||
|
||||
@@ -59,34 +59,6 @@ pub fn make_id(ms: u64, random: u64) -> String {
|
||||
format!("{ms:x}-{random:x}")
|
||||
}
|
||||
|
||||
/// How long a remembered file may go unused, and unreferenced by the recent designs and the
|
||||
/// autosave, before the browser's copy of its handle is dropped.
|
||||
pub const HANDLE_MAX_AGE_MS: f64 = 90.0 * 24.0 * 3600.0 * 1000.0;
|
||||
|
||||
/// What to do with the remembered handles.
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub struct PrunePlan {
|
||||
/// Handles to forget.
|
||||
pub delete: Vec<String>,
|
||||
/// Handles seen unused for the first time: start their clock.
|
||||
pub stamp: Vec<String>,
|
||||
}
|
||||
|
||||
/// Decide which of the remembered handles `ids` (id, when it was last stamped) to forget: those
|
||||
/// that no recent design or autosave (`keep`) names and that have gone unused for `max_age`. One
|
||||
/// never stamped starts its clock now, so a file another tab has just saved is never dropped.
|
||||
pub fn plan_prune(ids: &[(String, Option<f64>)], keep: &std::collections::HashSet<String>, now: f64, max_age: f64) -> PrunePlan {
|
||||
let mut plan = PrunePlan::default();
|
||||
for (id, stamped) in ids.iter().filter(|(id, _)| !keep.contains(id)) {
|
||||
match stamped {
|
||||
None => plan.stamp.push(id.clone()),
|
||||
Some(at) if now - at > max_age => plan.delete.push(id.clone()),
|
||||
Some(_) => {}
|
||||
}
|
||||
}
|
||||
plan
|
||||
}
|
||||
|
||||
/// Where a design saved with Save As goes.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum SaveTarget {
|
||||
@@ -140,7 +112,7 @@ mod web {
|
||||
use wasm_bindgen_futures::{JsFuture, spawn_local};
|
||||
use web_sys::{FileSystemFileHandle, IdbDatabase, IdbObjectStore, IdbOpenDbRequest, IdbRequest, IdbTransactionMode};
|
||||
|
||||
use super::{HANDLE_MAX_AGE_MS, is_abort, make_id, opens_design, parse, path, plan_prune, save_type};
|
||||
use super::{is_abort, make_id, opens_design, parse, path, save_type};
|
||||
use crate::files::{Files, err, read_handle};
|
||||
|
||||
const DB: &str = "solidcraft-disk-handles";
|
||||
@@ -425,106 +397,10 @@ mod web {
|
||||
db.transaction_with_str_and_mode(STORE, mode).map_err(err)?.object_store(STORE).map_err(err)
|
||||
}
|
||||
|
||||
/// Key of the time a handle was last used (the handle's own key is its id).
|
||||
fn stamp_key(id: &str) -> String {
|
||||
format!("at:{id}")
|
||||
}
|
||||
|
||||
async fn remember(id: &str, handle: &FileSystemFileHandle) -> Result<(), String> {
|
||||
let db = open_db().await?;
|
||||
let store = store(&db, IdbTransactionMode::Readwrite)?;
|
||||
result(&store.put_with_key(handle, &id.into()).map_err(err)?).await?;
|
||||
result(&store.put_with_key(&js_sys::Date::now().into(), &stamp_key(id).into()).map_err(err)?).await.map(|_| ())
|
||||
}
|
||||
|
||||
/// Note that the file was used now.
|
||||
async fn touch(id: &str) {
|
||||
let Ok(db) = open_db().await else { return };
|
||||
if let Ok(store) = store(&db, IdbTransactionMode::Readwrite)
|
||||
&& let Ok(req) = store.put_with_key(&js_sys::Date::now().into(), &stamp_key(id).into())
|
||||
{
|
||||
let _ = result(&req).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// The ids of every remembered file.
|
||||
pub async fn known_ids() -> Result<Vec<String>, String> {
|
||||
let db = open_db().await?;
|
||||
let req = store(&db, IdbTransactionMode::Readonly)?.get_all_keys().map_err(err)?;
|
||||
let keys: Array = result(&req).await?.dyn_into().map_err(|_| "no keys".to_string())?;
|
||||
Ok(keys.iter().filter_map(|k| k.as_string()).filter(|k| !k.starts_with("at:")).collect())
|
||||
}
|
||||
|
||||
async fn forget(ids: &[String]) -> Result<(), String> {
|
||||
let db = open_db().await?;
|
||||
let store = store(&db, IdbTransactionMode::Readwrite)?;
|
||||
for id in ids {
|
||||
result(&store.delete(&id.as_str().into()).map_err(err)?).await?;
|
||||
result(&store.delete(&stamp_key(id).into()).map_err(err)?).await?;
|
||||
HANDLES.with(|h| h.borrow_mut().remove(id));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Forget the remembered files nothing refers to any more (`keep`: the ids the recent designs
|
||||
/// and the autosave name), once they have gone unused for a long while.
|
||||
pub fn prune(keep: std::collections::HashSet<String>) {
|
||||
spawn_local(async move {
|
||||
let r = async {
|
||||
// A transaction ends when the page awaits anything but its own requests, so each
|
||||
// step opens its own.
|
||||
let known = known_ids().await?;
|
||||
let db = open_db().await?;
|
||||
let reading = store(&db, IdbTransactionMode::Readonly)?;
|
||||
let mut ids = Vec::new();
|
||||
for id in known {
|
||||
let at = result(&reading.get(&stamp_key(&id).into()).map_err(err)?).await?.as_f64();
|
||||
ids.push((id, at));
|
||||
}
|
||||
let plan = plan_prune(&ids, &keep, js_sys::Date::now(), HANDLE_MAX_AGE_MS);
|
||||
for id in &plan.stamp {
|
||||
touch(id).await;
|
||||
}
|
||||
forget(&plan.delete).await
|
||||
};
|
||||
if let Err(e) = r.await {
|
||||
log::warn!("remembered files weren't tidied: {e}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Open a design remembered from an earlier session (a recent one, after a reload): ask once
|
||||
/// for access to the file (call in the click), read it into the engine's files and queue it
|
||||
/// to open. A file that is gone is forgotten and dropped from the recent designs.
|
||||
pub fn restore(files: &Rc<Files>, path: &str) {
|
||||
let Some((id, name)) = parse(path) else { return };
|
||||
let (files, path, id, name) = (files.clone(), path.to_string(), id.to_string(), name.to_string());
|
||||
spawn_local(async move {
|
||||
let r = async {
|
||||
let handle = handle_for(&id).await?;
|
||||
ensure_readwrite(&handle).await?;
|
||||
read_handle(&handle).await
|
||||
};
|
||||
match r.await {
|
||||
Ok(bytes) => {
|
||||
vfs::insert(&path, bytes);
|
||||
files.opened.borrow_mut().push(path);
|
||||
files.wake();
|
||||
touch(&id).await;
|
||||
}
|
||||
Err(e) => {
|
||||
let gone = e.contains("NotFoundError") || e.contains("could not be found") || e.contains("no longer remembers");
|
||||
if gone {
|
||||
let _ = forget(std::slice::from_ref(&id)).await;
|
||||
files.disk_known.borrow_mut().remove(&id);
|
||||
files.gone.borrow_mut().push(path);
|
||||
files.say(format!("{name} isn't where it was saved any more"), true);
|
||||
} else {
|
||||
files.say(format!("{name}: {e}"), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
let req = store(&db, IdbTransactionMode::Readwrite)?.put_with_key(handle, &id.into()).map_err(err)?;
|
||||
result(&req).await.map(|_| ())
|
||||
}
|
||||
|
||||
async fn recall(id: &str) -> Result<Option<JsValue>, String> {
|
||||
@@ -579,22 +455,6 @@ mod tests {
|
||||
assert_eq!(route("/upload/part.step"), WriteTo::Memory);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_unreferenced_handles_unused_for_long_are_forgotten() {
|
||||
let keep: std::collections::HashSet<String> = ["recent".to_string()].into();
|
||||
let day = 24.0 * 3600.0 * 1000.0;
|
||||
let now = 1000.0 * day;
|
||||
let ids = vec![
|
||||
("recent".to_string(), Some(now - 200.0 * day)),
|
||||
("old".to_string(), Some(now - 100.0 * day)),
|
||||
("fresh".to_string(), Some(now - 5.0 * day)),
|
||||
("unstamped".to_string(), None),
|
||||
];
|
||||
let plan = plan_prune(&ids, &keep, now, HANDLE_MAX_AGE_MS);
|
||||
assert_eq!(plan, PrunePlan { delete: vec!["old".to_string()], stamp: vec!["unstamped".to_string()] });
|
||||
assert_eq!(plan_prune(&[], &keep, now, HANDLE_MAX_AGE_MS), PrunePlan::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handle_ids_differ_by_time_and_by_chance() {
|
||||
assert_ne!(make_id(1, 5), make_id(2, 5));
|
||||
|
||||
@@ -57,10 +57,6 @@ pub struct Files {
|
||||
pub incoming: RefCell<Vec<(String, Vec<u8>, Intent)>>,
|
||||
/// Designs opened from the computer's disk: their paths in the engine's files.
|
||||
pub opened: RefCell<Vec<String>>,
|
||||
/// The ids of the files remembered from earlier sessions (see `disk::restore`).
|
||||
pub disk_known: RefCell<std::collections::HashSet<String>>,
|
||||
/// Recent designs whose file is gone: to drop from the recent list.
|
||||
pub gone: RefCell<Vec<String>>,
|
||||
/// Disk designs whose last write failed: they are not saved.
|
||||
pub unsaved: RefCell<Vec<String>>,
|
||||
/// Messages for the status line (errors and confirmations).
|
||||
@@ -94,7 +90,7 @@ impl Files {
|
||||
}
|
||||
|
||||
/// Run a frame (the app sleeps until something happens).
|
||||
pub fn wake(&self) {
|
||||
fn wake(&self) {
|
||||
if let Some(c) = self.ctx.borrow().as_ref() {
|
||||
c.request_repaint();
|
||||
}
|
||||
@@ -271,10 +267,6 @@ pub fn start(files: &Rc<Files>) {
|
||||
}
|
||||
let files = files.clone();
|
||||
spawn_local(async move {
|
||||
match disk::known_ids().await {
|
||||
Ok(ids) => files.disk_known.borrow_mut().extend(ids),
|
||||
Err(e) => log::info!("no remembered files: {e}"),
|
||||
}
|
||||
match read_all().await {
|
||||
Ok(all) => {
|
||||
if local_only() {
|
||||
|
||||
@@ -136,11 +136,6 @@ mod web {
|
||||
self.app.open_path(&path);
|
||||
self.files.panel.set(false);
|
||||
}
|
||||
// Recent designs whose file is gone are dropped from the list.
|
||||
for path in self.files.gone.borrow_mut().drain(..) {
|
||||
self.app.home.recent.retain(|r| *r != path);
|
||||
self.app.home.opened_at.remove(&path);
|
||||
}
|
||||
for (text, error) in self.files.messages.borrow_mut().drain(..) {
|
||||
self.app.set_status(text, error);
|
||||
}
|
||||
@@ -208,9 +203,7 @@ mod web {
|
||||
let mut entries = self.files.entries.borrow().clone();
|
||||
entries.sort_by(|a, b| b.modified.total_cmp(&a.modified));
|
||||
let recent = &mut self.app.home.recent;
|
||||
let known = self.files.disk_known.borrow().clone();
|
||||
// A design on the computer's disk stays listed while its file is remembered.
|
||||
recent.retain(|p| vfs::exists(p) || disk::parse(p).is_some_and(|(id, _)| known.contains(id)));
|
||||
recent.retain(|p| vfs::exists(p));
|
||||
for e in entries.iter().filter(|e| solidcraft_engine::io::is_design_path(&e.name)) {
|
||||
let p = format!("{}{}", files::OPFS, e.name);
|
||||
if !recent.contains(&p) {
|
||||
@@ -218,10 +211,6 @@ mod web {
|
||||
}
|
||||
}
|
||||
recent.truncate(solidcraft_ui_egui::home::MAX_RECENT);
|
||||
// Remembered files that no recent design or autosave names are forgotten in time.
|
||||
let recovered = self.files.recovery.borrow().as_ref().and_then(|(_, p)| p.clone());
|
||||
let keep = recent.iter().chain(recovered.as_ref()).filter_map(|p| disk::parse(p)).map(|(id, _)| id.to_string()).collect();
|
||||
disk::prune(keep);
|
||||
}
|
||||
|
||||
/// The stored designs: open, insert, rename, delete, download; open from the computer.
|
||||
@@ -574,7 +563,6 @@ mod web {
|
||||
let files = Rc::new(Files::default());
|
||||
files::start(&files);
|
||||
let (panel, opener, saver, shown) = (files.clone(), files.clone(), files.clone(), files.clone());
|
||||
let (restorable, restorer) = (files.clone(), files.clone());
|
||||
let services = Services {
|
||||
// Open shows the browser's open dialog for a design where it has one
|
||||
// (the file keeps its handle, so Save writes back to it); otherwise,
|
||||
@@ -599,12 +587,6 @@ mod web {
|
||||
SaveTarget::Download => Box::pin(std::future::ready(Some(format!("{}{name}", files::DOWNLOADS)))),
|
||||
}
|
||||
})),
|
||||
// A recent design on the disk, from an earlier session, opens by asking
|
||||
// the browser for its remembered file.
|
||||
can_restore: Some(Box::new(move |path: &str| {
|
||||
disk::parse(path).is_some_and(|(id, _)| restorable.disk_known.borrow().contains(id))
|
||||
})),
|
||||
restore_file: Some(Box::new(move |path: &str| disk::restore(&restorer, path))),
|
||||
download_design: Some(Box::new(files::download)),
|
||||
browser_files: Some(Box::new(move || shown.panel.set(true))),
|
||||
install_app: Some(crate::install::service(&cc.egui_ctx)),
|
||||
|
||||
@@ -210,8 +210,6 @@ fn services(cc: &eframe::CreationContext<'_>) -> Services {
|
||||
http_download: Some(std::sync::Arc::new(http_download)),
|
||||
// Already an installed app: only the browser build offers to install.
|
||||
install_app: None,
|
||||
can_restore: None,
|
||||
restore_file: None,
|
||||
download_design: None,
|
||||
browser_files: None,
|
||||
agent_http: Some(std::sync::Arc::new(agent_http)),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -4689,11 +4689,6 @@ fn replay(
|
||||
// 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 };
|
||||
// A join that merged its targets left only the first of them (the others now live in
|
||||
// it): the copies change the bodies still there (#3378). When none is, the names stay
|
||||
// and say which body is missing.
|
||||
let present: Vec<String> = targets.iter().filter(|t| st.bodies.iter().any(|b| &b.name == *t)).cloned().collect();
|
||||
let targets = if present.is_empty() { targets } else { present };
|
||||
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
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
//! Faces that are picked as one (#3745 follow-up): a modelled thread is thousands of small faces
|
||||
//! (its flanks, and the pieces of the surface it was cut into), but it is one surface to point at,
|
||||
//! light and select; so are the pieces of one face an imported file had (a face wrapping all the
|
||||
//! way round, split in two for the kernel).
|
||||
//!
|
||||
//! The groups come from what made the faces, never from their shape: a face made by a Thread
|
||||
//! feature is named after it (`F<id>:…`), and a piece of a face the thread cut names it among the
|
||||
//! faces around it (`…[F<id>:…]`). Working the names out for a large body takes a while, so it is
|
||||
//! done on a worker thread, once per body, and asked for without waiting.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::{Document, FeatureKind, ModelBody};
|
||||
|
||||
/// Each face's group (an index into `groups`), and the groups: faces picked together, in order.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct FaceGroups {
|
||||
of_face: Vec<Option<u32>>,
|
||||
groups: Vec<Arc<Vec<usize>>>,
|
||||
}
|
||||
|
||||
impl FaceGroups {
|
||||
/// The group of `face` (`None`: it is picked alone).
|
||||
pub fn group_of(&self, face: usize) -> Option<u32> {
|
||||
self.of_face.get(face).copied().flatten()
|
||||
}
|
||||
|
||||
/// The faces picked together with `face` (`None`: it is picked alone).
|
||||
pub fn of(&self, face: usize) -> Option<Arc<Vec<usize>>> {
|
||||
let g = self.of_face.get(face).copied().flatten()?;
|
||||
self.groups.get(g as usize).cloned()
|
||||
}
|
||||
|
||||
/// Work out the groups of `b` (the names of its faces are worked out here: slow for a big body).
|
||||
pub fn of_body(threads: &[u64], b: &ModelBody) -> FaceGroups {
|
||||
let mut keyed: HashMap<(u8, u64), Vec<usize>> = HashMap::new();
|
||||
if !threads.is_empty() {
|
||||
let names = crate::naming::face_names(b);
|
||||
let marks: Vec<(u64, String, String)> = threads.iter().map(|id| (*id, format!("F{id}:"), format!("[F{id}:"))).collect();
|
||||
for (i, name) in names.iter().enumerate() {
|
||||
// Made by the thread, or a piece of a face the thread cut (it names the thread's faces
|
||||
// around it).
|
||||
let own = |p: &str, inside: &str| name.starts_with(p) || name.contains(inside) || name.contains(&format!(",{p}"));
|
||||
if let Some((id, _, _)) = marks.iter().find(|(_, p, inside)| own(p, inside)) {
|
||||
keyed.entry((0, *id)).or_default().push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Pieces of one face of an imported file.
|
||||
if let Some(sources) = b.body.face_sources() {
|
||||
for (i, s) in sources.iter().enumerate() {
|
||||
if let Some(solidcraft_kernel::FaceSource::Imported { face, .. }) = s
|
||||
&& !keyed.values().any(|faces| faces.contains(&i))
|
||||
{
|
||||
keyed.entry((1, *face)).or_default().push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut out = FaceGroups { of_face: vec![None; b.body.face_count()], groups: Vec::new() };
|
||||
let mut groups: Vec<Vec<usize>> = keyed.into_values().filter(|faces| faces.len() > 1).collect();
|
||||
groups.sort();
|
||||
for faces in groups {
|
||||
let g = u32::try_from(out.groups.len()).unwrap_or(u32::MAX);
|
||||
for &f in &faces {
|
||||
if let Some(slot) = out.of_face.get_mut(f) {
|
||||
*slot = Some(g);
|
||||
}
|
||||
}
|
||||
out.groups.push(Arc::new(faces));
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
/// The modelled threads of `doc`, by feature id.
|
||||
pub fn thread_features(doc: &Document) -> Vec<u64> {
|
||||
doc.features.iter().filter(|f| matches!(f.kind, FeatureKind::Thread { modeled: true, .. })).map(|f| f.id).collect()
|
||||
}
|
||||
|
||||
/// Groups worked out (or being worked out: `None`), by the identity of a body's names and the
|
||||
/// threads they were grouped by.
|
||||
type Cache = HashMap<(usize, Vec<u64>), Option<Arc<FaceGroups>>>;
|
||||
static CACHE: Mutex<Option<Cache>> = Mutex::new(None);
|
||||
/// Bodies whose groups are kept; the oldest go first.
|
||||
const KEPT: usize = 256;
|
||||
|
||||
/// The groups of `b` in `doc` if they are worked out; otherwise they are worked out on a worker
|
||||
/// thread (once) and `None` comes back meanwhile. The browser build has no threads: there they
|
||||
/// are worked out now.
|
||||
pub fn face_groups_soon(doc: &Document, b: &ModelBody) -> Option<Arc<FaceGroups>> {
|
||||
let threads = thread_features(doc);
|
||||
if threads.is_empty() && b.body.face_sources().is_none() {
|
||||
return Some(Arc::new(FaceGroups::default()));
|
||||
}
|
||||
let key = (b.names.id(), threads.clone());
|
||||
{
|
||||
let Ok(mut cache) = CACHE.lock() else { return None };
|
||||
let cache = cache.get_or_insert_with(HashMap::new);
|
||||
if let Some(entry) = cache.get(&key) {
|
||||
return entry.clone();
|
||||
}
|
||||
if cache.len() >= KEPT {
|
||||
cache.clear();
|
||||
}
|
||||
cache.insert(key.clone(), None);
|
||||
}
|
||||
let work = move |b: ModelBody| {
|
||||
let groups = Arc::new(FaceGroups::of_body(&threads, &b));
|
||||
if let Ok(mut cache) = CACHE.lock()
|
||||
&& let Some(slot) = cache.as_mut().and_then(|c| c.get_mut(&key))
|
||||
{
|
||||
*slot = Some(groups.clone());
|
||||
}
|
||||
groups
|
||||
};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let body = b.clone();
|
||||
let work = work.clone();
|
||||
if std::thread::Builder::new().name("face-groups".into()).spawn(move || drop(work(body))).is_ok() {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Some(work(b.clone()))
|
||||
}
|
||||
@@ -22,6 +22,7 @@ mod document;
|
||||
mod eval;
|
||||
pub mod expr;
|
||||
pub mod face_anchor;
|
||||
pub mod face_groups;
|
||||
mod feature_geometry;
|
||||
mod feature_refs;
|
||||
pub use face_anchor::{FaceAnchor, FaceAttachmentStatus};
|
||||
|
||||
@@ -549,29 +549,3 @@ fn copies_of_a_primitive_cut_only_its_targets() {
|
||||
assert!(rel(removed, holes * PI * 9.0 * 10.0) < 1e-3, "{id}: {removed} mm³ cut from Plaque");
|
||||
}
|
||||
}
|
||||
|
||||
/// #3378: a pattern or mirror of a join whose targets the join merged into one body copies onto
|
||||
/// that body, as before #2755, instead of looking for the target the join used up.
|
||||
#[test]
|
||||
fn copies_of_a_join_that_merged_its_targets_join_the_merged_body() {
|
||||
for (copy, n) in [
|
||||
(json!(["solid.pattern.circular", {"features": ["spoke"], "axis": "Y", "count": 5}]), 5.0),
|
||||
(json!(["solid.mirror", {"features": ["spoke"], "plane": {"origin": [0, 0, 0], "normal": [1, 0, 0]}}]), 2.0),
|
||||
] {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.cylinder", json!({"radius": 66, "height": 3, "base": [0, 0, 0], "axis": "Y", "body_name": "rim"}));
|
||||
run(&mut s, "solid.cylinder", json!({"radius": 63, "height": 3, "base": [0, 0, 0], "axis": "Y", "operation": "cut", "targets": ["rim"]}));
|
||||
run(&mut s, "solid.cylinder", json!({"radius": 12, "height": 3, "base": [0, 0, 0], "axis": "Y", "body_name": "hub"}));
|
||||
let v0 = volume(&mut s);
|
||||
let spoke = json!({"length": 52, "width": 3, "height": 2.5, "corner": [12, 0, -1.25], "operation": "join", "targets": ["rim", "hub"], "name": "spoke"});
|
||||
run(&mut s, "solid.box", spoke);
|
||||
let v1 = volume(&mut s);
|
||||
let (id, p) = (copy[0].as_str().unwrap_or_default(), copy[1].clone());
|
||||
run(&mut s, id, p);
|
||||
let names = run(&mut s, "inspect.measure", json!({}))["bodies"].as_array().map(|b| b.iter().map(|x| x["name"].clone()).collect::<Vec<_>>());
|
||||
assert_eq!(names, Some(vec![json!("rim")]), "{id}: one body, the merged rim and hub");
|
||||
// Every copy adds what the seed spoke added (curved bodies measure to about 0.02 %; a
|
||||
// missing or doubled spoke is about 5 %).
|
||||
assert!(rel(volume(&mut s), v0 + n * (v1 - v0)) < 1e-3, "{id}: {} vs {}", volume(&mut s), v0 + n * (v1 - v0));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1864,11 +1864,8 @@ fn c_symmetry(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cb = sk.curve_index(rb).ok_or_else(|| bad(cmd, format!("unknown entity `{rb}`")))?;
|
||||
match (sk.curves.get(ca).map(|c| c.kind.clone()), sk.curves.get(cb).map(|c| c.kind.clone())) {
|
||||
(Some(CurveKind::Line { a: a0, b: a1 }), Some(CurveKind::Line { a: b0, b: b1 })) => {
|
||||
// Opposite sides of a rectangle run in opposite directions: pair each end
|
||||
// with the end of the other line nearest its mirror image (#2961, #2940).
|
||||
let (p0, p1) = if mirror_pairing_is_crossed(sk, l, [a0, a1], [b0, b1]) { (b1, b0) } else { (b0, b1) };
|
||||
out.push(add_c(sk, ConstraintKind::Symmetric { p: a0, q: p0, l })?);
|
||||
out.push(add_c(sk, ConstraintKind::Symmetric { p: a1, q: p1, l })?);
|
||||
out.push(add_c(sk, ConstraintKind::Symmetric { p: a0, q: b0, l })?);
|
||||
out.push(add_c(sk, ConstraintKind::Symmetric { p: a1, q: b1, l })?);
|
||||
}
|
||||
(Some(_), Some(_)) => {
|
||||
let pa = round_center(sk, ca).ok_or_else(|| bad(cmd, "symmetry needs two lines or two circles/arcs"))?;
|
||||
@@ -1889,25 +1886,6 @@ fn c_symmetry(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(json!({"constraints": ids, "sketch": info}))
|
||||
}
|
||||
|
||||
/// True when the ends of line `a` mirror (about the line `l`) closer onto the ends of `b` crossed
|
||||
/// (`a0` with `b1`, `a1` with `b0`) than in order. A degenerate symmetry line keeps the order.
|
||||
fn mirror_pairing_is_crossed(sk: &Sketch, l: usize, a: [usize; 2], b: [usize; 2]) -> bool {
|
||||
let Some((o, e)) = line_pts(sk, l) else { return false };
|
||||
let d = e - o;
|
||||
let len2 = d.x * d.x + d.y * d.y;
|
||||
if len2 < 1e-18 {
|
||||
return false;
|
||||
}
|
||||
let pt = |i: usize| sk.point(i).unwrap_or_default();
|
||||
let near = |i: usize, j: usize| {
|
||||
let v = pt(i) - o;
|
||||
let t = (v.x * d.x + v.y * d.y) / len2;
|
||||
let foot = o + d * t;
|
||||
(foot * 2.0 - pt(i)).dist(pt(j))
|
||||
};
|
||||
near(a[0], b[1]) + near(a[1], b[0]) < near(a[0], b[0]) + near(a[1], b[1])
|
||||
}
|
||||
|
||||
/// Symmetry about point `c`: two points, two circles/arcs (their centres, and equal radii) or two
|
||||
/// lines (each end with the end of the other line nearest its mirror image). The constraints
|
||||
/// (the construction lines show in the response's `created`).
|
||||
@@ -2535,11 +2513,9 @@ fn move_point(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "sketch.move_point";
|
||||
let to = req_vec2(cmd, p, "to")?;
|
||||
let id = target_sketch(s, p, cmd)?;
|
||||
let state = s.model.result(id).map(|r| r.state.clone());
|
||||
let last = state.as_ref().and_then(|state| state.sketch(id));
|
||||
let (_, rep) = edit_solved(s, id, false, Analysis::Freedom, |sk, _| {
|
||||
let q = point_ref(sk, p.get("point"), cmd, "point")?;
|
||||
Ok(((), drag_entity(sk, Ent::Point(q), &[], to, to, false, last).1.solve_again()))
|
||||
Ok(((), drag_entity(sk, Ent::Point(q), &[], to, to, false).1.solve_again()))
|
||||
})?;
|
||||
Ok(json!({"sketch": rep.as_ref().map(report_json)}))
|
||||
}
|
||||
@@ -2562,8 +2538,7 @@ fn drag_frame(s: &mut Session, p: &Value, live: bool) -> Result<(Value, Option<c
|
||||
};
|
||||
let id = target_sketch(s, p, cmd)?;
|
||||
// The sketch's report as last evaluated: a live frame keeps its degrees of freedom.
|
||||
let state = s.model.result(id).map(|r| r.state.clone());
|
||||
let last = state.as_ref().and_then(|state| state.sketch(id));
|
||||
let last = s.model.result(id).and_then(|r| r.state.sketch(id)).map(|ss| ss.report.clone());
|
||||
let analysis = if live { Analysis::Full } else { Analysis::Freedom };
|
||||
let (d, rep) = edit_solved(s, id, false, analysis, |sk, doc| {
|
||||
let r = p.get("entity").and_then(Value::as_str).ok_or_else(|| bad(cmd, "`entity` must be a sketch point or curve"))?;
|
||||
@@ -2590,7 +2565,7 @@ fn drag_frame(s: &mut Session, p: &Value, live: bool) -> Result<(Value, Option<c
|
||||
}
|
||||
// The held solves work on the dimensions' current values (a solved step applies them).
|
||||
doc.apply_dimension_values(&doc.param_values().0, sk)?;
|
||||
let (d, left) = drag_entity(sk, e, &with, from, to, live, last);
|
||||
let (d, left) = drag_entity(sk, e, &with, from, to, live);
|
||||
// A live frame's held solve is its only one; otherwise the sketch's own solve reports
|
||||
// its degrees of freedom.
|
||||
Ok((d, if live && last.is_some() { left } else { left.solve_again() }))
|
||||
@@ -2599,7 +2574,7 @@ fn drag_frame(s: &mut Session, p: &Value, live: bool) -> Result<(Value, Option<c
|
||||
// average displacement of their moving points, as before.
|
||||
let sk = s.doc.sketch(id)?;
|
||||
let at = d.at(sk, from);
|
||||
let rep = rep.or_else(|| last.map(|ss| SolveReport { status: solidcraft_sketch::SolveStatus::Solved, failing: Vec::new(), ..ss.report.clone() }));
|
||||
let rep = rep.or_else(|| last.map(|last| SolveReport { status: solidcraft_sketch::SolveStatus::Solved, failing: Vec::new(), ..last }));
|
||||
let out = json!({"sketch": rep.as_ref().map(report_json), "reached": d.reached, "at": [at.x, at.y], "retry": d.retry});
|
||||
Ok((out, rep.filter(|_| live).map(|report| crate::LiveEdit { sketch: id, report })))
|
||||
}
|
||||
|
||||
@@ -374,66 +374,6 @@ fn symmetry_moves_a_free_line_between_two_fixed_lines() {
|
||||
}
|
||||
}
|
||||
|
||||
/// A 20 x 10 rectangle (sides l1..l4; left = l4 and right = l2 run in opposite directions) and a
|
||||
/// free vertical line at x = -20 as the symmetry line (#2940, #2961).
|
||||
fn rectangle_and_axis(dimension_left: bool) -> (Session, Vec<String>) {
|
||||
let mut s = new_sketch();
|
||||
let r = run(&mut s, "sketch.rectangle.two_point", json!({"p0": [-12, 0], "p1": [8, 10]}));
|
||||
let sides = ids(&r["curves"]);
|
||||
run(&mut s, "sketch.line", json!({"points": [[-20, -5], [-20, 15]], "construction": true, "ids": ["axis"]}));
|
||||
if dimension_left {
|
||||
run(&mut s, "sketch.dimension", json!({"entities": [sides[3].clone()], "value": 10}));
|
||||
}
|
||||
(s, sides)
|
||||
}
|
||||
|
||||
/// Both sides keep their 10 mm height and their ends mirror in x about `axis_x`.
|
||||
fn assert_sides_mirror_with_height(s: &Session, left: &str, right: &str, axis_x: f64) {
|
||||
let sk = sketch(s);
|
||||
let at = |r: String| sk.point(sk.resolve_point(&r).unwrap()).unwrap();
|
||||
for side in [left, right] {
|
||||
let (a, b) = (at(format!("{side}.start")), at(format!("{side}.end")));
|
||||
assert!((a.y - b.y).abs() > 9.999 && (a.y - b.y).abs() < 10.001, "{side} height {:?} {:?}", a, b);
|
||||
assert!((a.x - b.x).abs() < 1e-7, "{side} not vertical: {:?} {:?}", a, b);
|
||||
}
|
||||
let (l, r) = (at(format!("{left}.start")), at(format!("{right}.start")));
|
||||
assert!((l.x + r.x - 2.0 * axis_x).abs() < 1e-6, "sides not mirrored: {l:?} {r:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn symmetry_of_rectangle_sides_keeps_their_height() {
|
||||
// #2940: the opposite sides of a rectangle run in opposite directions, so pairing their ends
|
||||
// by index forced both heights to 0.
|
||||
let (mut s, sides) = rectangle_and_axis(false);
|
||||
run(&mut s, "sketch.constraint.symmetry", json!({"a": sides[3], "b": sides[1], "line": "axis"}));
|
||||
assert_sides_mirror_with_height(&s, &sides[3], &sides[1], -20.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn symmetry_of_a_dimensioned_rectangle_side_is_accepted() {
|
||||
// #2961: with a length on a side, the same symmetry was refused as "neither entity can move".
|
||||
let (mut s, sides) = rectangle_and_axis(true);
|
||||
run(&mut s, "sketch.constraint.symmetry", json!({"a": sides[3], "b": sides[1], "line": "axis"}));
|
||||
assert_sides_mirror_with_height(&s, &sides[3], &sides[1], -20.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn symmetry_of_rectangle_sides_with_swapped_arguments() {
|
||||
let (mut s, sides) = rectangle_and_axis(true);
|
||||
run(&mut s, "sketch.constraint.symmetry", json!({"a": sides[1], "b": sides[3], "line": "axis"}));
|
||||
assert_sides_mirror_with_height(&s, &sides[3], &sides[1], -20.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn symmetry_of_rectangle_sides_about_the_y_axis() {
|
||||
let mut s = new_sketch();
|
||||
let r = run(&mut s, "sketch.rectangle.two_point", json!({"p0": [-12, 0], "p1": [8, 10]}));
|
||||
let sides = ids(&r["curves"]);
|
||||
run(&mut s, "sketch.dimension", json!({"entities": [sides[3].clone()], "value": 10}));
|
||||
run(&mut s, "sketch.constraint.symmetry", json!({"a": sides[3], "b": sides[1], "line": "y"}));
|
||||
assert_sides_mirror_with_height(&s, &sides[3], &sides[1], 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn midpoint_alignment_persists_undoes_and_survives_endpoint_edits() {
|
||||
use solidcraft_sketch::ConstraintKind;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_geom::Vec2;
|
||||
use solidcraft_sketch::{ConstraintKind, CurveKind, LinkKind, LinkSource, Shape, Sketch, TextAlign, TextExprs, TextStyle};
|
||||
use solidcraft_sketch::{ConstraintKind, CurveKind, LinkKind, LinkSource, Sketch, TextAlign, TextExprs, TextStyle};
|
||||
|
||||
/// The style parameters of `sketch.text` and `sketch.edit_text`.
|
||||
macro_rules! text_style_doc {
|
||||
@@ -822,39 +822,6 @@ fn guess_radius(sk: &Sketch, curves: &[usize], at: Vec2) -> f64 {
|
||||
if r.is_finite() && r > 1e-6 { r } else { 1.0 }
|
||||
}
|
||||
|
||||
/// Where the centre of a circle of radius `d` touching `sh` can lie: the line moved `d` to either
|
||||
/// side, or the circle grown and shrunk by `d` (arcs as their full circle).
|
||||
fn centre_paths(sh: &Shape, d: f64) -> Vec<Shape> {
|
||||
match *sh {
|
||||
Shape::Line { a, b } => {
|
||||
(b - a).normalized().map(|u| [d, -d].map(|s| Shape::Line { a: a + u.perp() * s, b: b + u.perp() * s }).to_vec()).unwrap_or_default()
|
||||
}
|
||||
Shape::Round { c, r, .. } => [r + d, (r - d).abs()]
|
||||
.into_iter()
|
||||
.filter(|x| *x > 1e-12)
|
||||
.map(|r| Shape::Round { c, r, start: 0.0, sweep: std::f64::consts::TAU })
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The centre nearest `near` of a circle of radius `r` touching curves `a` and `b` (#3141): the
|
||||
/// solver starts from the answer instead of from the click, which lies on a curve, where the
|
||||
/// tangency has no side yet and the solve fails or moves the curves.
|
||||
fn tangent_centre(sk: &Sketch, a: usize, b: usize, r: f64, near: Vec2) -> Option<Vec2> {
|
||||
let (sa, sb) = (sk.shape(a)?, sk.shape(b)?);
|
||||
let mut best: Option<Vec2> = None;
|
||||
for x in centre_paths(&sa, r) {
|
||||
for y in centre_paths(&sb, r) {
|
||||
for q in solidcraft_sketch::intersections_within(&x, &y, 1e-9 * (1.0 + r)) {
|
||||
if q.x.is_finite() && q.y.is_finite() && best.is_none_or(|b| q.dist(near) < b.dist(near)) {
|
||||
best = Some(q);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
best
|
||||
}
|
||||
|
||||
fn tangent_curves(sk: &Sketch, p: &Value, cmd: &str, n: usize) -> Result<Vec<usize>> {
|
||||
let ids = string_list(p, "curves");
|
||||
if ids.len() != n {
|
||||
@@ -875,14 +842,10 @@ fn tangent_circle(s: &mut Session, p: &Value, cmd: &str, n: usize, radius: Optio
|
||||
if !(r0 > 1e-9 && r0 < 1e8) {
|
||||
return Err(bad(cmd, "radius must be positive"));
|
||||
}
|
||||
// Start where the circle already touches when that can be worked out (two curves and a
|
||||
// radius), so that only the new circle moves.
|
||||
let start = match cs.as_slice() {
|
||||
[a, b] if radius.is_some() => tangent_centre(sk, *a, *b, r0, near).unwrap_or(near),
|
||||
_ => near,
|
||||
};
|
||||
// Start where the circle already nearly touches: move the centre so the guessed circle
|
||||
// sits between the curves.
|
||||
let first_new = sk.points.len();
|
||||
let c = sk.add_circle(start, r0, None, None)?;
|
||||
let c = sk.add_circle(near, r0, None, None)?;
|
||||
let mut cons = Vec::new();
|
||||
for k in &cs {
|
||||
cons.push(add_c(sk, ConstraintKind::Tangent { a: *k, b: c })?);
|
||||
|
||||
@@ -125,33 +125,7 @@ impl Path {
|
||||
///
|
||||
/// `live`: one frame of an interactive drag, whose held solves and cursor correction share
|
||||
/// one frame's work budget ([`Budget::frame`]); otherwise each has a solve's own.
|
||||
///
|
||||
/// `solved`: the sketch as last evaluated. When it is unchanged and solved, a grabbed entity it
|
||||
/// determined is not dragged.
|
||||
pub(super) fn drag_entity(
|
||||
sk: &mut Sketch,
|
||||
e: Ent,
|
||||
with: &[Ent],
|
||||
from: Vec2,
|
||||
to: Vec2,
|
||||
live: bool,
|
||||
solved: Option<&solidcraft_doc::SolvedSketch>,
|
||||
) -> (Dragged, Left) {
|
||||
// An unchanged, solved entity with no freedom cannot follow a drag. Holding it at a
|
||||
// different position only makes its constraints conflict, and a large pattern would spend
|
||||
// minutes trying dense solves before returning to the same geometry (#2006).
|
||||
if solved.is_some_and(|ss| {
|
||||
ss.report.ok()
|
||||
&& ss.sketch == *sk
|
||||
&& match e {
|
||||
Ent::Point(q) => ss.report.point_determined.get(q),
|
||||
Ent::Curve(c) => ss.report.curve_determined.get(c),
|
||||
}
|
||||
.copied()
|
||||
.unwrap_or(false)
|
||||
}) {
|
||||
return (Dragged::default(), Left::Solved);
|
||||
}
|
||||
pub(super) fn drag_entity(sk: &mut Sketch, e: Ent, with: &[Ent], from: Vec2, to: Vec2, live: bool) -> (Dragged, Left) {
|
||||
let Some((path, hold, n, short)) = prepare(sk, e, with, from, to) else {
|
||||
return (Dragged::default(), Left::Unsolved);
|
||||
};
|
||||
|
||||
@@ -7,52 +7,6 @@ use solidcraft_geom::Vec3;
|
||||
use super::solve_counts;
|
||||
use crate::Session;
|
||||
|
||||
#[test]
|
||||
fn determined_pattern_geometry_does_not_attempt_conflicting_held_solves() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY"}));
|
||||
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [5, 5], "p1": [6.5, 6.5]}));
|
||||
run(&mut s, "sketch.dimension", json!({"entities": ["l1"], "value": 1.5}));
|
||||
run(&mut s, "sketch.dimension", json!({"entities": ["l2"], "value": 1.5}));
|
||||
run(&mut s, "sketch.constraint.fix", json!({"entities": ["l1.start"]}));
|
||||
let pattern = run(
|
||||
&mut s,
|
||||
"sketch.pattern.rectangular",
|
||||
json!({"entities": ["l1", "l2", "l3", "l4"], "count": 3, "count2": 3, "spacing": 2.2, "spacing2": -2.2}),
|
||||
);
|
||||
let line = pattern["curves"][0].as_str().expect("a patterned line");
|
||||
let endpoint = format!("{line}.start");
|
||||
let before = sketch_of(&s);
|
||||
assert!(!before.curve_locked(before.curve_index(line).expect("patterned line index")));
|
||||
for entity in [line, endpoint.as_str()] {
|
||||
for live in [true, false] {
|
||||
reset(&mut s);
|
||||
let result = run(&mut s, "sketch.drag", json!({"entity": entity, "from": [5, 5], "to": [9, 12], "live": live}));
|
||||
assert_eq!(result["reached"], json!(0.0));
|
||||
assert_eq!(result["at"], json!([5.0, 5.0]));
|
||||
assert_eq!(result["sketch"]["solved"], json!(true));
|
||||
let full = usize::from(!live);
|
||||
assert_eq!(solve_counts::take(), (full, full), "no held solve for determined geometry");
|
||||
assert_eq!(sketch_of(&s), before);
|
||||
}
|
||||
}
|
||||
reset(&mut s);
|
||||
run(&mut s, "sketch.move_point", json!({"point": endpoint, "to": [9, 12]}));
|
||||
assert_eq!(solve_counts::take(), (1, 1), "move_point also skips held solves");
|
||||
assert_eq!(sketch_of(&s), before);
|
||||
|
||||
// A cached determined report must not suppress an outstanding dimension edit.
|
||||
let dimension = before
|
||||
.constraints
|
||||
.iter()
|
||||
.find(|c| matches!(c.kind, solidcraft_sketch::ConstraintKind::Length { l: 0, .. }))
|
||||
.and_then(|c| c.param.as_ref())
|
||||
.expect("the first line's dimension");
|
||||
s.doc_mut().params.iter_mut().find(|p| &p.name == dimension).expect("dimension parameter").expr = "3".into();
|
||||
run(&mut s, "sketch.drag", json!({"entity": line, "from": [5, 5], "to": [9, 12], "live": true}));
|
||||
assert!((pt(&s, "l1.start").dist(pt(&s, "l1.end")) - 3.0).abs() < 1e-6);
|
||||
}
|
||||
|
||||
fn run(s: &mut Session, id: &str, p: Value) -> Value {
|
||||
match s.execute(id, &p) {
|
||||
Ok(v) => v,
|
||||
|
||||
@@ -438,51 +438,6 @@ fn new_creation_tools() {
|
||||
assert!(a.iter().any(|x| close(*x, pi * 5.0 * 2.0 + pi)), "{a:?}");
|
||||
}
|
||||
|
||||
/// #3141: a 2-tangent circle asked for near a point on one of its curves (where a click on that
|
||||
/// curve puts it) is solved from the touching circle nearest that point: it lands in the corner,
|
||||
/// the rectangle stays as drawn, and fixed sides no longer make it fail.
|
||||
#[test]
|
||||
fn two_tangent_circle_from_a_click_on_a_curve_leaves_the_curves_as_drawn() {
|
||||
let side = |s: &Session, sides: &[String], on: fn(Vec2) -> bool| -> String {
|
||||
let sk = sketch(s);
|
||||
sides
|
||||
.iter()
|
||||
.find(|c| {
|
||||
let i = sk.curve_index(c).unwrap();
|
||||
matches!(sk.shape(i), Some(solidcraft_sketch::Shape::Line { a, b }) if on(a) && on(b))
|
||||
})
|
||||
.unwrap()
|
||||
.clone()
|
||||
};
|
||||
let points = |s: &Session| sketch(s).points.iter().map(|p| p.pos).collect::<Vec<_>>();
|
||||
for fixed in [false, true] {
|
||||
let mut s = new_sketch();
|
||||
let r = run(&mut s, "sketch.rectangle.two_point", json!({"p0": [-800, 0], "p1": [-300, 500]}));
|
||||
let sides = ids(&r["curves"]);
|
||||
if fixed {
|
||||
run(&mut s, "sketch.constraint.fix", json!({"entities": sides}));
|
||||
}
|
||||
let before = points(&s);
|
||||
let bottom = side(&s, &sides, |p| p.y.abs() < 1e-9);
|
||||
let left = side(&s, &sides, |p| (p.x + 800.0).abs() < 1e-9);
|
||||
// Midway between a click on the bottom side and one on the left side.
|
||||
let r = run(&mut s, "sketch.circle.two_tangent", json!({"curves": [bottom, left], "radius": 50, "near": [-675, 125]}));
|
||||
let c = ids(&r["curves"])[0].clone();
|
||||
let sk = sketch(&s);
|
||||
let ci = sk.curve_index(&c).unwrap();
|
||||
assert!(sk.center(ci).unwrap().dist(Vec2::new(-750.0, 50.0)) < 1e-6, "fixed {fixed}: {:?}", sk.center(ci));
|
||||
assert!(close(sk.radius(ci).unwrap(), 50.0));
|
||||
let after = points(&s);
|
||||
assert!(before.iter().zip(&after).all(|(a, b)| a.dist(*b) < 1e-9), "fixed {fixed}: the rectangle did not move: {after:?}");
|
||||
// Asked for right on the left side: still a touching circle next to it.
|
||||
let r = run(&mut s, "sketch.circle.two_tangent", json!({"curves": [bottom, left], "radius": 20, "near": [-800, 30]}));
|
||||
let c = ids(&r["curves"])[0].clone();
|
||||
let sk = sketch(&s);
|
||||
let o = sk.center(sk.curve_index(&c).unwrap()).unwrap();
|
||||
assert!(close(o.y, 20.0) && close((o.x + 800.0).abs(), 20.0), "fixed {fixed}: {o:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ellipse_splines_conic() {
|
||||
let pi = std::f64::consts::PI;
|
||||
|
||||
@@ -431,24 +431,16 @@ impl Session {
|
||||
|
||||
/// Become `fresh` (another design opened, recovered or started), keeping the settings that
|
||||
/// are the user's rather than the design's: `auto_project` is a preference (#680).
|
||||
/// Checkpoints survive too: a batch that failed after `file.new` or `doc.open` goes back
|
||||
/// from the checkpoint it took before its first step, and that checkpoint belongs to the
|
||||
/// design before the batch, which is what the batch promised (#3242). The design is what a
|
||||
/// checkpoint keeps, not the replaced session's file path (docs/mcp.md).
|
||||
pub fn replace_design(&mut self, fresh: Session) {
|
||||
let auto_project = self.auto_project;
|
||||
let param_check = self.param_check;
|
||||
let render_viewport = self.render_viewport.clone();
|
||||
let render_jobs = self.render_jobs.scratch();
|
||||
let checkpoints = std::mem::take(&mut self.checkpoints);
|
||||
let next_checkpoint = self.next_checkpoint;
|
||||
*self = fresh;
|
||||
self.auto_project = auto_project;
|
||||
self.param_check = param_check;
|
||||
self.render_viewport = render_viewport;
|
||||
self.render_jobs = render_jobs;
|
||||
self.checkpoints = checkpoints;
|
||||
self.next_checkpoint = next_checkpoint;
|
||||
}
|
||||
|
||||
/// The design has changes not in its file: in the document, or in what is hidden.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Autosave and crash recovery.
|
||||
//!
|
||||
//! Each open design keeps its own recovery entry in the recovery folder: `<id>.solidcraft` (the design
|
||||
//! A running app keeps one recovery entry in the recovery folder: `<id>.solidcraft` (the design
|
||||
//! as of the last autosave), `<id>.json` (its file path, name and time) and `<id>.lock`, which
|
||||
//! the app holds locked while it runs. Autosaves are written atomically, only when the design
|
||||
//! has unsaved changes it hasn't written yet; a design with nothing unsaved has no entry. An
|
||||
@@ -163,7 +163,7 @@ pub fn remove(dir: &Path, id: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// One open design's autosaver. Keep it with that design when switching tabs.
|
||||
/// One app's autosaver.
|
||||
pub struct Autosaver {
|
||||
pub dir: PathBuf,
|
||||
pub id: String,
|
||||
@@ -232,7 +232,7 @@ impl Snapshot {
|
||||
}
|
||||
|
||||
impl Autosaver {
|
||||
/// Start autosaving into `dir` (created if needed), holding this design's lock.
|
||||
/// Start autosaving into `dir` (created if needed), holding this app's lock.
|
||||
pub fn new(dir: &Path, interval: Duration) -> Result<Autosaver> {
|
||||
let io = |e: std::io::Error| EngineError::Other(format!("{}: {e}", dir.display()));
|
||||
fs::create_dir_all(dir).map_err(io)?;
|
||||
@@ -361,20 +361,6 @@ impl Autosaver {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The user closed this design (saving it or choosing Don't Save): discard only its entry.
|
||||
pub fn discard(mut self) {
|
||||
// A write still running would recreate the entry, without a lock, after it is removed:
|
||||
// the next launch would offer a design the user closed.
|
||||
if let Some(w) = self.writing.take() {
|
||||
let _ = w.done.recv();
|
||||
}
|
||||
for ext in ["json", solidcraft_io::DESIGN_EXT] {
|
||||
let _ = fs::remove_file(file(&self.dir, &self.id, ext));
|
||||
}
|
||||
self.lock = None;
|
||||
let _ = fs::remove_file(file(&self.dir, &self.id, "lock"));
|
||||
}
|
||||
|
||||
/// Closing the app: with nothing unsaved the entry goes; otherwise it stays for the next
|
||||
/// launch to offer. The lock is released either way.
|
||||
pub fn close(mut self, s: &Session) {
|
||||
|
||||
@@ -1785,23 +1785,6 @@ fn checkpoint_restores_with_a_full_undo_history() {
|
||||
assert!(s.execute("edit.restore_checkpoint", &json!({"checkpoint": id})).is_err());
|
||||
}
|
||||
|
||||
/// Regression (#3242): `file.new` replaced the session and its checkpoints with it, so a
|
||||
/// batch that failed after `file.new` could not go back to the design it began in.
|
||||
#[test]
|
||||
fn a_checkpoint_survives_file_new() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"length": 1.0, "width": 1.0, "height": 1.0}));
|
||||
let id = run(&mut s, "edit.checkpoint", json!({}))["checkpoint"].as_u64().unwrap();
|
||||
run(&mut s, "file.new", json!({"name": "Replacement"}));
|
||||
run(&mut s, "solid.box", json!({"length": 2.0, "width": 2.0, "height": 2.0}));
|
||||
assert_eq!(run(&mut s, "inspect.measure", json!({}))["total"]["volume_mm3"].as_f64(), Some(8.0));
|
||||
run(&mut s, "edit.restore_checkpoint", json!({"checkpoint": id}));
|
||||
assert_eq!(run(&mut s, "inspect.measure", json!({}))["total"]["volume_mm3"].as_f64(), Some(1.0));
|
||||
// The undo history is the restored design's own: one step back is the empty design.
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert_eq!(run(&mut s, "inspect.measure", json!({}))["total"]["volume_mm3"].as_f64(), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn components_occurrences_and_world_placement() {
|
||||
let mut s = Session::default();
|
||||
|
||||
@@ -541,17 +541,6 @@ thread_local! {
|
||||
pub(crate) static REPHASED: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
|
||||
/// The volume one closed shell encloses, signed by the way its faces face (positive when they
|
||||
/// face out of it), from a mesh as coarse as the orientation mesh of a solid of its size.
|
||||
/// `None` when it cannot be meshed.
|
||||
pub(crate) fn shell_volume(shell: &mt::Shell) -> Option<f64> {
|
||||
let bounds: Aabb3 = shell.vertex_iter().map(|v| from_p3(v.point())).collect();
|
||||
let span = if bounds.is_empty() { 1.0 } else { bounds.diagonal() };
|
||||
let deflection = Deflection { display: true, ..Deflection::chord(orientation_tolerance(span)) };
|
||||
let v = guard("shell volume", || Ok(shell.robust_triangulation_within(deflection).to_polygon().volume())).ok()?;
|
||||
v.is_finite().then_some(v)
|
||||
}
|
||||
|
||||
/// Deflection the orientation mesh uses: coarse enough for a large solid, fine enough that a
|
||||
/// small one still tessellates. `unmeshed_faces` counts at the same value, so the count the
|
||||
/// orientation mesh produced is reused instead of meshing the body again.
|
||||
|
||||
+26
-212
@@ -347,11 +347,6 @@ struct Nurbs {
|
||||
|
||||
impl Nurbs {
|
||||
fn eval(&self, t: f64) -> Option<V3> {
|
||||
let h = self.eval_h(t)?;
|
||||
(h[3].abs() > 1e-300).then(|| [h[0] / h[3], h[1] / h[3], h[2] / h[3]])
|
||||
}
|
||||
/// The homogeneous point (x·w, y·w, z·w, w) at `t`.
|
||||
fn eval_h(&self, t: f64) -> Option<[f64; 4]> {
|
||||
let p = self.deg;
|
||||
let n = self.pts.len();
|
||||
if n == 0 || self.knots.len() != n + p + 1 {
|
||||
@@ -373,49 +368,14 @@ impl Nurbs {
|
||||
d[j] = [0, 1, 2, 3].map(|c| (1.0 - alpha) * x[c] + alpha * y[c]);
|
||||
}
|
||||
}
|
||||
d.get(p).copied()
|
||||
let h = d[p];
|
||||
(h[3].abs() > 1e-300).then(|| [h[0] / h[3], h[1] / h[3], h[2] / h[3]])
|
||||
}
|
||||
fn range(&self) -> (f64, f64) {
|
||||
(self.knots.get(self.deg).copied().unwrap_or(0.0), self.knots.get(self.pts.len()).copied().unwrap_or(0.0))
|
||||
}
|
||||
}
|
||||
|
||||
/// A circular arc in its definition plane (at height `c[2]`), counter-clockwise from `a0` to
|
||||
/// `a1` (radians, `a1 > a0`).
|
||||
struct Arc {
|
||||
c: V3,
|
||||
rad: f64,
|
||||
a0: f64,
|
||||
a1: f64,
|
||||
}
|
||||
|
||||
impl Arc {
|
||||
/// The arc placed by `x`, as an exact rational quadratic B-spline (pieces of at most a
|
||||
/// quarter turn).
|
||||
fn nurbs(&self, x: &Xf) -> Nurbs {
|
||||
let sweep = self.a1 - self.a0;
|
||||
let pieces = (sweep / std::f64::consts::FRAC_PI_2).ceil().clamp(1.0, 8.0) as usize;
|
||||
let step = sweep / pieces as f64;
|
||||
let w = (step / 2.0).cos();
|
||||
let on = |t: f64, k: f64| x.point([self.c[0] + k * t.cos(), self.c[1] + k * t.sin(), self.c[2]]);
|
||||
let mut pts = Vec::with_capacity(2 * pieces + 1);
|
||||
let mut knots = vec![0.0; 3];
|
||||
for i in 0..pieces {
|
||||
let t = self.a0 + step * i as f64;
|
||||
let (p, m) = (on(t, self.rad), on(t + step / 2.0, self.rad / w));
|
||||
pts.push([p[0], p[1], p[2], 1.0]);
|
||||
pts.push([m[0] * w, m[1] * w, m[2] * w, w]);
|
||||
if i > 0 {
|
||||
knots.extend([i as f64 / pieces as f64; 2]);
|
||||
}
|
||||
}
|
||||
let p = on(self.a1, self.rad);
|
||||
pts.push([p[0], p[1], p[2], 1.0]);
|
||||
knots.extend([1.0; 3]);
|
||||
Nurbs { deg: 2, knots, pts }
|
||||
}
|
||||
}
|
||||
|
||||
struct Tr<'a> {
|
||||
f: &'a Iges,
|
||||
o: Out,
|
||||
@@ -463,11 +423,6 @@ impl<'a> Tr<'a> {
|
||||
}
|
||||
/// The entity's own transformation (its chain), in millimetres.
|
||||
fn own_xf(&self, seq: i64, depth: usize) -> R<Xf> {
|
||||
self.own_xf_in(seq, depth, self.f.unit)
|
||||
}
|
||||
/// The entity's own transformation with translations in units of `unit` millimetres (1 for
|
||||
/// a curve in a surface's parameter space, which has no length unit).
|
||||
fn own_xf_in(&self, seq: i64, depth: usize, unit: f64) -> R<Xf> {
|
||||
let de = self.de(seq)?;
|
||||
if de.xform <= 0 || depth > MAX_DEPTH {
|
||||
return Ok(Xf::ID);
|
||||
@@ -482,15 +437,13 @@ impl<'a> Tr<'a> {
|
||||
for (col, x) in r.iter_mut().enumerate() {
|
||||
*x = self.n(t, 1 + row * 4 + col)?;
|
||||
}
|
||||
r[3] *= unit;
|
||||
r[3] *= self.f.unit;
|
||||
}
|
||||
let parent = self.own_xf_in(t, depth + 1, unit)?;
|
||||
let parent = self.own_xf(t, depth + 1)?;
|
||||
Ok(parent.then(&Xf(m)))
|
||||
}
|
||||
fn pt(&self, seq: i64, i: usize) -> R<V3> {
|
||||
self.pt_in(seq, i, self.f.unit)
|
||||
}
|
||||
fn pt_in(&self, seq: i64, i: usize, u: f64) -> R<V3> {
|
||||
let u = self.f.unit;
|
||||
Ok([self.n(seq, i)? * u, self.n(seq, i + 1)? * u, self.n(seq, i + 2)? * u])
|
||||
}
|
||||
/// A point entity (116) or direction (123), placed.
|
||||
@@ -536,8 +489,20 @@ impl<'a> Tr<'a> {
|
||||
Ok(vec![Seg::along(c, |t| add(a, mul(sub(b, a), t)))])
|
||||
}
|
||||
100 => {
|
||||
let Arc { c, rad, a0, a1 } = self.arc(seq)?;
|
||||
let zt = c[2];
|
||||
let u = self.f.unit;
|
||||
let zt = self.n(seq, 1)? * u;
|
||||
let c = [self.n(seq, 2)? * u, self.n(seq, 3)? * u, zt];
|
||||
let s = [self.n(seq, 4)? * u, self.n(seq, 5)? * u, zt];
|
||||
let e = [self.n(seq, 6)? * u, self.n(seq, 7)? * u, zt];
|
||||
let rad = norm(sub(s, c));
|
||||
if !(rad > 0.0) {
|
||||
return Err(format!("entity {seq}: arc of zero radius"));
|
||||
}
|
||||
let a0 = (s[1] - c[1]).atan2(s[0] - c[0]);
|
||||
let mut a1 = (e[1] - c[1]).atan2(e[0] - c[0]);
|
||||
while a1 <= a0 + 1e-12 {
|
||||
a1 += std::f64::consts::TAU;
|
||||
}
|
||||
let at = |t: f64| x.point([c[0] + rad * t.cos(), c[1] + rad * t.sin(), zt]);
|
||||
let z = unit(x.dir([0.0, 0.0, 1.0])).ok_or("degenerate arc frame")?;
|
||||
let xa = unit(x.dir([1.0, 0.0, 0.0])).ok_or("degenerate arc frame")?;
|
||||
@@ -606,62 +571,7 @@ impl<'a> Tr<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A circular arc (100) in its definition plane, in millimetres.
|
||||
fn arc(&self, seq: i64) -> R<Arc> {
|
||||
let u = self.f.unit;
|
||||
let zt = self.n(seq, 1)? * u;
|
||||
let c = [self.n(seq, 2)? * u, self.n(seq, 3)? * u, zt];
|
||||
let s = [self.n(seq, 4)? * u, self.n(seq, 5)? * u, zt];
|
||||
let e = [self.n(seq, 6)? * u, self.n(seq, 7)? * u, zt];
|
||||
let rad = norm(sub(s, c));
|
||||
if !(rad > 0.0) {
|
||||
return Err(format!("entity {seq}: arc of zero radius"));
|
||||
}
|
||||
let a0 = (s[1] - c[1]).atan2(s[0] - c[0]);
|
||||
let mut a1 = (e[1] - c[1]).atan2(e[0] - c[0]);
|
||||
while a1 <= a0 + 1e-12 {
|
||||
a1 += std::f64::consts::TAU;
|
||||
}
|
||||
Ok(Arc { c, rad, a0, a1 })
|
||||
}
|
||||
|
||||
/// A surface of revolution (120) whose generatrix is the circular arc `seq`, about the axis
|
||||
/// through `o` along `d`. A STEP surface of revolution turns the whole of its curve, and the
|
||||
/// whole circle turns into a whole sphere or torus (a sphere twice over when the arc is
|
||||
/// centred on the axis): the face's trim cannot pick its piece out of that, and the face
|
||||
/// meshed as the full ball. An arc in a plane through the axis is a sphere or a torus; any
|
||||
/// other arc turns as an exact rational B-spline of just the arc.
|
||||
fn revolved_arc(&mut self, seq: i64, xf: &Xf, o: V3, d: V3) -> R<u64> {
|
||||
let x = xf.then(&self.own_xf(seq, 0)?);
|
||||
let arc = self.arc(seq)?;
|
||||
let n = unit(x.dir([0.0, 0.0, 1.0])).ok_or("degenerate arc frame")?;
|
||||
let (centre, rad) = (x.point(arc.c), arc.rad * x.scale());
|
||||
let off = sub(centre, o);
|
||||
let h = dot(off, d);
|
||||
let rho = norm(sub(off, mul(d, h)));
|
||||
let tol = self.tol.max(rad * 1e-9);
|
||||
if dot(n, d).abs() < 1e-7 && dot(off, n).abs() <= tol {
|
||||
let ax = self.o.placement(add(o, mul(d, h)), d, perp(d));
|
||||
if rho <= tol {
|
||||
return Ok(self.o.add(format!("SPHERICAL_SURFACE('',#{ax},{})", r(rad))));
|
||||
}
|
||||
if rad < rho {
|
||||
return Ok(self.o.add(format!("TOROIDAL_SURFACE('',#{ax},{},{})", r(rho), r(rad))));
|
||||
}
|
||||
}
|
||||
let nb = arc.nurbs(&x);
|
||||
let curve = self.write_nurbs_curve(&nb);
|
||||
let (pa, dd) = (self.o.point(o), self.o.dir(d));
|
||||
let a1 = self.o.add(format!("AXIS1_PLACEMENT('',#{pa},#{dd})"));
|
||||
Ok(self.o.add(format!("SURFACE_OF_REVOLUTION('',#{curve},#{a1})")))
|
||||
}
|
||||
|
||||
fn nurbs_curve(&self, seq: i64, x: &Xf) -> R<Nurbs> {
|
||||
self.nurbs_curve_in(seq, x, self.f.unit)
|
||||
}
|
||||
fn nurbs_curve_in(&self, seq: i64, x: &Xf, unit: f64) -> R<Nurbs> {
|
||||
/// Knot spans shorter than this are moved onto (0, 1).
|
||||
const MIN_KNOT_SPAN: f64 = 1e-4;
|
||||
let k = self.i(seq, 1)?;
|
||||
let m = self.i(seq, 2)?;
|
||||
if !(0..=MAX_LIST as i64).contains(&k) || !(1..=32).contains(&m) || m > k.max(1) + 1 {
|
||||
@@ -681,27 +591,17 @@ impl<'a> Tr<'a> {
|
||||
if !(w > 0.0) {
|
||||
return Err(format!("entity {seq}: non-positive weight"));
|
||||
}
|
||||
let p = x.point(self.pt_in(seq, base_p + 3 * j, unit)?);
|
||||
let p = x.point(self.pt(seq, base_p + 3 * j)?);
|
||||
pts.push([p[0] * w, p[1] * w, p[2] * w, w]);
|
||||
}
|
||||
if knots.windows(2).any(|w| w[1] < w[0]) {
|
||||
return Err(format!("entity {seq}: knots out of order"));
|
||||
}
|
||||
let mut nb = Nurbs { deg: m, knots, pts };
|
||||
let nb = Nurbs { deg: m, knots, pts };
|
||||
let (a, b) = nb.range();
|
||||
if !(b > a) {
|
||||
return Err(format!("entity {seq}: empty parameter range"));
|
||||
}
|
||||
// Some exporters write a curve over a knot span of 1e-7 or less. The STEP side reads a
|
||||
// span that small as a single value and refused the curve, losing the faces it bounds.
|
||||
// Moving and scaling the knots onto (0, 1) leaves the curve's shape as it is; nothing
|
||||
// else refers to its parameters.
|
||||
if b - a < MIN_KNOT_SPAN {
|
||||
let scale = 1.0 / (b - a);
|
||||
for k in &mut nb.knots {
|
||||
*k = (*k - a) * scale;
|
||||
}
|
||||
}
|
||||
Ok(nb)
|
||||
}
|
||||
|
||||
@@ -884,9 +784,6 @@ impl<'a> Tr<'a> {
|
||||
let ax_x = x.then(&self.own_xf(axis, 0)?);
|
||||
let (a, b) = (ax_x.point(self.pt(axis, 1)?), ax_x.point(self.pt(axis, 4)?));
|
||||
let d = unit(sub(b, a)).ok_or("zero-length revolution axis")?;
|
||||
if self.de(generatrix)?.typ == 100 {
|
||||
return self.revolved_arc(generatrix, &x, a, d);
|
||||
}
|
||||
let segs = self.curve(generatrix, &x, 1)?;
|
||||
let [seg] = segs.as_slice() else { return Err(format!("entity {seq}: composite generatrix is not read")) };
|
||||
let (pa, dd) = (self.o.point(a), self.o.dir(d));
|
||||
@@ -909,61 +806,6 @@ impl<'a> Tr<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The start point of a curve in a surface's parameter space (the B curve of a curve on a
|
||||
/// surface, 142): its (u, v) are the first two coordinates. Lines, arcs, B-splines and
|
||||
/// composites of them, as the specification lists for parameter-space curves.
|
||||
fn param_start(&self, seq: i64, depth: usize) -> R<V3> {
|
||||
if depth > MAX_DEPTH {
|
||||
return Err("parameter-space curves nested too deeply".into());
|
||||
}
|
||||
let de = self.de(seq)?;
|
||||
let x = self.own_xf_in(seq, 0, 1.0)?;
|
||||
let p = match de.typ {
|
||||
110 => self.pt_in(seq, 1, 1.0)?,
|
||||
100 => [self.n(seq, 4)?, self.n(seq, 5)?, self.n(seq, 1)?],
|
||||
126 => {
|
||||
let nb = self.nurbs_curve_in(seq, &Xf::ID, 1.0)?;
|
||||
nb.eval(nb.range().0).ok_or_else(|| format!("entity {seq}: B-spline cannot be evaluated"))?
|
||||
}
|
||||
102 if self.i(seq, 1)? > 0 => self.param_start(self.i(seq, 2)?, depth + 1)?,
|
||||
t => return Err(format!("parameter-space curve type {t} is not read")),
|
||||
};
|
||||
Ok(x.point(p))
|
||||
}
|
||||
|
||||
/// The point of a B-spline surface (128) at parameters (u, v), placed as `surface` places it.
|
||||
fn surface_point(&self, seq: i64, xf: &Xf, u: f64, v: f64) -> R<V3> {
|
||||
if self.de(seq)?.typ != 128 {
|
||||
return Err(format!("entity {seq}: only B-spline surfaces are evaluated"));
|
||||
}
|
||||
let x = xf.then(&self.own_xf(seq, 0)?);
|
||||
let (k1, k2, m1, m2) = (self.i(seq, 1)?, self.i(seq, 2)?, self.i(seq, 3)?, self.i(seq, 4)?);
|
||||
if !(0..=4096).contains(&k1) || !(0..=4096).contains(&k2) || !(1..=32).contains(&m1) || !(1..=32).contains(&m2) {
|
||||
return Err(format!("entity {seq}: bad B-spline surface size"));
|
||||
}
|
||||
let (k1, k2, m1, m2) = (k1 as usize, k2 as usize, m1 as usize, m2 as usize);
|
||||
let (n1, n2) = (k1 + m1 + 2, k2 + m2 + 2);
|
||||
let s_knots: Vec<f64> = (0..n1).map(|j| self.n(seq, 10 + j)).collect::<R<_>>()?;
|
||||
let t_knots: Vec<f64> = (0..n2).map(|j| self.n(seq, 10 + n1 + j)).collect::<R<_>>()?;
|
||||
let base_w = 10 + n1 + n2;
|
||||
let base_p = base_w + (k1 + 1) * (k2 + 1);
|
||||
// Each row of the net at u, then the column of those points at v (homogeneous, so the
|
||||
// weights combine as the rational tensor product does).
|
||||
let mut column = Vec::with_capacity(k2 + 1);
|
||||
for j in 0..=k2 {
|
||||
let mut row = Vec::with_capacity(k1 + 1);
|
||||
for i in 0..=k1 {
|
||||
let idx = j * (k1 + 1) + i;
|
||||
let w = self.n(seq, base_w + idx)?;
|
||||
let p = x.point(self.pt(seq, base_p + 3 * idx)?);
|
||||
row.push([p[0] * w, p[1] * w, p[2] * w, w]);
|
||||
}
|
||||
let h = Nurbs { deg: m1, knots: s_knots.clone(), pts: row }.eval_h(u).ok_or("surface cannot be evaluated")?;
|
||||
column.push(h);
|
||||
}
|
||||
Nurbs { deg: m2, knots: t_knots, pts: column }.eval(v).ok_or_else(|| "surface cannot be evaluated".into())
|
||||
}
|
||||
|
||||
/// The natural boundary of a B-spline surface (its four edge curves), for trimmed surfaces
|
||||
/// with no outer curve.
|
||||
fn natural_boundary(&mut self, seq: i64, xf: &Xf) -> R<Vec<Seg>> {
|
||||
@@ -1355,10 +1197,10 @@ fn trimmed(t: &mut Tr, sw: &mut Sewer, seq: i64) -> R<FaceB> {
|
||||
let n1 = t.i(seq, 2)?;
|
||||
let n2 = t.i(seq, 3)?.clamp(0, MAX_LIST as i64) as usize;
|
||||
let mut loops = Vec::new();
|
||||
let outer = if n1 == 0 { t.natural_boundary(surf_seq, &xf)? } else { cos_curve(t, t.i(seq, 4)?, &xf, surf_seq)? };
|
||||
let outer = if n1 == 0 { t.natural_boundary(surf_seq, &xf)? } else { cos_curve(t, t.i(seq, 4)?, &xf)? };
|
||||
loops.push(loop_of(t, sw, outer));
|
||||
for k in 0..n2 {
|
||||
let segs = cos_curve(t, t.i(seq, 5 + k)?, &xf, surf_seq)?;
|
||||
let segs = cos_curve(t, t.i(seq, 5 + k)?, &xf)?;
|
||||
loops.push(loop_of(t, sw, segs));
|
||||
}
|
||||
loops.retain(|l| !l.is_empty());
|
||||
@@ -1392,16 +1234,8 @@ fn trimmed(t: &mut Tr, sw: &mut Sewer, seq: i64) -> R<FaceB> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The model-space curve of a curve on a surface (142) bounding a trimmed `surface`, its pieces
|
||||
/// head to tail.
|
||||
///
|
||||
/// A curve on a surface is one curve given twice: B in the surface's parameter space and C in
|
||||
/// model space, both running the same way (C = S∘B). A composite C must run each piece on from
|
||||
/// the end of the one before, but some exporters write its pieces in order with some of them
|
||||
/// reversed, so the loop does not close and the face is lost. The pieces are turned to run on,
|
||||
/// starting where S(B) starts (the end of the first piece there), so the loop also keeps the
|
||||
/// sense the file gives it (outer loop and holes the opposite ways round).
|
||||
fn cos_curve(t: &mut Tr, seq: i64, xf: &Xf, surface: i64) -> R<Vec<Seg>> {
|
||||
/// The model-space curve of a curve on a surface (142).
|
||||
fn cos_curve(t: &mut Tr, seq: i64, xf: &Xf) -> R<Vec<Seg>> {
|
||||
let de = t.de(seq)?;
|
||||
if de.typ != 142 {
|
||||
return t.curve(seq, xf, 0);
|
||||
@@ -1411,27 +1245,7 @@ fn cos_curve(t: &mut Tr, seq: i64, xf: &Xf, surface: i64) -> R<Vec<Seg>> {
|
||||
if c <= 0 {
|
||||
return Err("a curve on a surface without its model-space curve".into());
|
||||
}
|
||||
let segs = t.curve(c, &x, 0)?;
|
||||
let b = t.i(seq, 3).unwrap_or(0);
|
||||
let start = if b > 0 { t.param_start(b, 0).and_then(|p| t.surface_point(surface, xf, p[0], p[1])).ok() } else { None };
|
||||
Ok(head_to_tail(segs, start, t.tol))
|
||||
}
|
||||
|
||||
/// Pieces of a composite curve turned so that each starts where the one before ends, the first
|
||||
/// at the end nearer `start` (when known). A closed piece (its ends within `tol`, the sewing
|
||||
/// tolerance that decides they are one vertex) keeps its direction.
|
||||
fn head_to_tail(segs: Vec<Seg>, start: Option<V3>, tol: f64) -> Vec<Seg> {
|
||||
let mut prev = start;
|
||||
segs.into_iter()
|
||||
.map(|s| {
|
||||
let s = match prev {
|
||||
Some(p) if norm(sub(s.a, s.b)) > tol && norm(sub(s.b, p)) < norm(sub(s.a, p)) => s.reversed(),
|
||||
_ => s,
|
||||
};
|
||||
prev = Some(s.b);
|
||||
s
|
||||
})
|
||||
.collect()
|
||||
t.curve(c, &x, 0)
|
||||
}
|
||||
|
||||
/// The model-space curves of a boundary (141), each in its sense.
|
||||
|
||||
@@ -85,16 +85,10 @@ fn hostile_iges_never_panics() {
|
||||
|
||||
/// An IGES file from (type, form, parameters) entities, in order (pointers are 2k + 1).
|
||||
fn iges_file(ents: &[(i64, i64, String)]) -> String {
|
||||
let placed: Vec<(i64, i64, String, i64)> = ents.iter().map(|(t, f, p)| (*t, *f, p.clone(), 0)).collect();
|
||||
iges_file_placed(&placed)
|
||||
}
|
||||
|
||||
/// Like [`iges_file`], with each entity's transformation matrix pointer (0 for none).
|
||||
fn iges_file_placed(ents: &[(i64, i64, String, i64)]) -> String {
|
||||
let mut out = vec![format!("{:<72}S{:>7}", "test", 1), format!("{:<72}G{:>7}", "1H,,1H;,,,,,,,,,,,1.,2,2HMM,,,,1.E-6;", 1)];
|
||||
let mut p_lines: Vec<(usize, String)> = Vec::new();
|
||||
let mut starts = Vec::new();
|
||||
for (k, (t, _, p, _)) in ents.iter().enumerate() {
|
||||
for (k, (t, _, p)) in ents.iter().enumerate() {
|
||||
starts.push(p_lines.len() + 1);
|
||||
let text = format!("{t},{p};");
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
@@ -102,9 +96,9 @@ fn iges_file_placed(ents: &[(i64, i64, String, i64)]) -> String {
|
||||
p_lines.push((2 * k + 1, c.iter().collect()));
|
||||
}
|
||||
}
|
||||
for (k, (t, form, _, xf)) in ents.iter().enumerate() {
|
||||
for (k, (t, form, _)) in ents.iter().enumerate() {
|
||||
let count = p_lines.iter().filter(|(d, _)| *d == 2 * k + 1).count();
|
||||
out.push(format!("{:>8}{:>8}{:>8}{:>8}{:>8}{:>8}{:>8}{:>8}{:>8}D{:>7}", t, starts[k], 0, 0, 0, 0, xf, 0, "00000000", 2 * k + 1));
|
||||
out.push(format!("{:>8}{:>8}{:>8}{:>8}{:>8}{:>8}{:>8}{:>8}{:>8}D{:>7}", t, starts[k], 0, 0, 0, 0, 0, 0, "00000000", 2 * k + 1));
|
||||
out.push(format!("{:>8}{:>8}{:>8}{:>8}{:>8}{:>8}{:>8}{:>8}{:>8}D{:>7}", t, 0, 0, count, form, 0, 0, "", 0, 2 * k + 2));
|
||||
}
|
||||
for (i, (d, l)) in p_lines.iter().enumerate() {
|
||||
@@ -114,48 +108,6 @@ fn iges_file_placed(ents: &[(i64, i64, String, i64)]) -> String {
|
||||
out.join("\n")
|
||||
}
|
||||
|
||||
/// A spherical cap written the way analytic IGES exports write one: a surface of revolution
|
||||
/// (120) whose generatrix is a short circular arc (100) centred on the axis, turned into the
|
||||
/// meridian plane by its transformation matrix (124), trimmed by a circle round the axis. The arc
|
||||
/// is only the part of the circle the cap needs; read as the whole circle, the revolution was a
|
||||
/// whole sphere twice over and the cap meshed as the full ball.
|
||||
#[test]
|
||||
fn an_arc_revolved_about_its_centre_is_a_cap_not_a_ball() {
|
||||
let (r, polar) = (50.0f64, 30f64.to_radians());
|
||||
let (rim_r, rim_z) = (r * polar.sin(), r * polar.cos());
|
||||
let ents: Vec<(i64, i64, String, i64)> = vec![
|
||||
// 1: the meridian plane: local x stays x, local y becomes z.
|
||||
(124, 0, "1.,0.,0.,0.,0.,0.,-1.,0.,0.,1.,0.,0.".into(), 0),
|
||||
// 3: the axis, 5: the arc from the rim (60° in its plane) to the pole (90°).
|
||||
(110, 0, "0.,0.,0.,0.,0.,1.".into(), 0),
|
||||
(100, 0, format!("0.,0.,0.,{:?},{:?},0.,{r:?}", rim_r, rim_z), 1),
|
||||
// 7: the surface, 9: the rim (a full circle at the cap's height), 11: on the surface.
|
||||
(120, 0, format!("3,5,0.,{:?}", std::f64::consts::TAU), 0),
|
||||
(100, 0, format!("{rim_z:?},0.,0.,{rim_r:?},0.,{rim_r:?},0."), 0),
|
||||
(142, 0, "0,7,0,9,2".into(), 0),
|
||||
// 13: the cap.
|
||||
(144, 0, "7,1,0,11".into(), 0),
|
||||
];
|
||||
let imp = iges_import(&iges_file_placed(&ents)).unwrap();
|
||||
assert_eq!(imp.bodies.len(), 1, "{:?}", imp.warnings);
|
||||
let mesh = imp.bodies[0].body.tessellate(0.01).unwrap();
|
||||
let (mut lo, mut hi) = ([f64::MAX; 3], [f64::MIN; 3]);
|
||||
for p in &mesh.positions {
|
||||
for (k, x) in [p.x, p.y, p.z].into_iter().enumerate() {
|
||||
lo[k] = lo[k].min(x);
|
||||
hi[k] = hi[k].max(x);
|
||||
}
|
||||
}
|
||||
for k in 0..2 {
|
||||
assert!((lo[k] + rim_r).abs() < 0.1 && (hi[k] - rim_r).abs() < 0.1, "cap is {lo:?} .. {hi:?}");
|
||||
}
|
||||
assert!((lo[2] - rim_z).abs() < 0.1 && (hi[2] - r).abs() < 0.1, "cap is {lo:?} .. {hi:?}");
|
||||
// Area of a cap: 2π r h.
|
||||
let area = measure(&imp.bodies[0].body).unwrap().area;
|
||||
let cap = std::f64::consts::TAU * r * (r - rim_z);
|
||||
assert!(rel(area, cap) < 1e-3, "area {area} vs {cap}");
|
||||
}
|
||||
|
||||
/// A unit-cube-like box (10 × 20 × 30) made of six trimmed bilinear B-spline patches whose
|
||||
/// boundaries are lines: sewn into a closed solid.
|
||||
#[test]
|
||||
@@ -257,127 +209,3 @@ fn directory_entries_cannot_share_their_way_past_the_file_size() {
|
||||
assert!(error.contains("parameter data is too large"), "{error}");
|
||||
assert!(t.elapsed() < std::time::Duration::from_secs(10), "took {:?}", t.elapsed());
|
||||
}
|
||||
|
||||
/// A curve on a surface (142) for a trimmed patch: model-space lines through `corners` with the
|
||||
/// pieces `reversed` written end to start (as some exporters write composite trim curves), and
|
||||
/// the same loop in the patch's parameter space through `uv`. Returns the 142's pointer.
|
||||
fn trim_loop(ents: &mut Vec<(i64, i64, String)>, surface: usize, corners: &[[f64; 3]], uv: &[[f64; 2]], reversed: &[usize]) -> usize {
|
||||
let fmt = |p: [f64; 3]| format!("{:?},{:?},{:?}", p[0], p[1], p[2]);
|
||||
let mut model = Vec::new();
|
||||
let mut param = Vec::new();
|
||||
for k in 0..corners.len() {
|
||||
let (p0, p1) = (corners[k], corners[(k + 1) % corners.len()]);
|
||||
let (p0, p1) = if reversed.contains(&k) { (p1, p0) } else { (p0, p1) };
|
||||
ents.push((110, 0, format!("{},{}", fmt(p0), fmt(p1))));
|
||||
model.push(2 * ents.len() - 1);
|
||||
let (q0, q1) = (uv[k], uv[(k + 1) % uv.len()]);
|
||||
ents.push((110, 0, format!("{},{}", fmt([q0[0], q0[1], 0.0]), fmt([q1[0], q1[1], 0.0]))));
|
||||
param.push(2 * ents.len() - 1);
|
||||
}
|
||||
let list = |v: &[usize]| v.iter().map(|x| x.to_string()).collect::<Vec<_>>().join(",");
|
||||
ents.push((102, 0, format!("{},{}", model.len(), list(&model))));
|
||||
let c = 2 * ents.len() - 1;
|
||||
ents.push((102, 0, format!("{},{}", param.len(), list(¶m))));
|
||||
let b = 2 * ents.len() - 1;
|
||||
// Created by projection, preferring the parameter-space curve (PREF 1).
|
||||
ents.push((142, 0, format!("0,{surface},{b},{c},1")));
|
||||
2 * ents.len() - 1
|
||||
}
|
||||
|
||||
/// A bilinear B-spline patch through corners `a` (u, v = 0), `b` (u = 1), `c` (u, v = 1) and
|
||||
/// `d` (v = 1).
|
||||
fn bilinear_patch(a: [f64; 3], b: [f64; 3], c: [f64; 3], d: [f64; 3]) -> (i64, i64, String) {
|
||||
let fmt = |p: [f64; 3]| format!("{:?},{:?},{:?}", p[0], p[1], p[2]);
|
||||
(128, 0, format!("1,1,1,1,0,0,1,0,0,0.,0.,1.,1.,0.,0.,1.,1.,1.,1.,1.,1.,{},{},{},{},0.,1.,0.,1.", fmt(a), fmt(b), fmt(d), fmt(c)))
|
||||
}
|
||||
|
||||
/// The box of `trimmed_surfaces_sew_into_a_solid` with some pieces of every trim curve written
|
||||
/// end to start (the first piece on half the faces): each face's loop still closes and the box
|
||||
/// is a solid.
|
||||
#[test]
|
||||
fn trim_curves_with_reversed_pieces_still_close_their_faces() {
|
||||
let c =
|
||||
|i: usize| -> [f64; 3] { [if i & 1 != 0 { 10.0 } else { 0.0 }, if i & 2 != 0 { 20.0 } else { 0.0 }, if i & 4 != 0 { 30.0 } else { 0.0 }] };
|
||||
let quads = [[0, 2, 3, 1], [4, 5, 7, 6], [0, 1, 5, 4], [2, 6, 7, 3], [0, 4, 6, 2], [1, 3, 7, 5]];
|
||||
let mut ents: Vec<(i64, i64, String)> = Vec::new();
|
||||
for (fi, q) in quads.iter().enumerate() {
|
||||
let corners = q.map(c);
|
||||
ents.push(bilinear_patch(corners[0], corners[1], corners[2], corners[3]));
|
||||
let surface = 2 * ents.len() - 1;
|
||||
let reversed: &[usize] = if fi % 2 == 0 { &[0, 2] } else { &[1, 3] };
|
||||
let cos = trim_loop(&mut ents, surface, &corners, &[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]], reversed);
|
||||
ents.push((144, 0, format!("{surface},1,0,{cos}")));
|
||||
}
|
||||
let imp = iges_import(&iges_file(&ents)).unwrap();
|
||||
assert!(imp.warnings.is_empty(), "{:?}", imp.warnings);
|
||||
assert!(imp.bodies[0].closed);
|
||||
let m = measure(&imp.bodies[0].body).unwrap();
|
||||
assert!(rel(m.volume, 6000.0) < 1e-6 && rel(m.area, 2200.0) < 1e-6, "{} {}", m.volume, m.area);
|
||||
}
|
||||
|
||||
/// A plate with a square hole whose trim curve starts with a reversed piece: where the
|
||||
/// parameter-space curve starts decides which way the hole runs, so it stays a hole.
|
||||
#[test]
|
||||
fn a_hole_whose_first_trim_piece_is_reversed_stays_a_hole() {
|
||||
let mut ents: Vec<(i64, i64, String)> = vec![bilinear_patch([0.0; 3], [10.0, 0.0, 0.0], [10.0, 10.0, 0.0], [0.0, 10.0, 0.0])];
|
||||
let outer = trim_loop(
|
||||
&mut ents,
|
||||
1,
|
||||
&[[0.0, 0.0, 0.0], [10.0, 0.0, 0.0], [10.0, 10.0, 0.0], [0.0, 10.0, 0.0]],
|
||||
&[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]],
|
||||
&[],
|
||||
);
|
||||
// Clockwise about the patch's normal, as a hole runs.
|
||||
let hole = trim_loop(
|
||||
&mut ents,
|
||||
1,
|
||||
&[[3.0, 3.0, 0.0], [3.0, 7.0, 0.0], [7.0, 7.0, 0.0], [7.0, 3.0, 0.0]],
|
||||
&[[0.3, 0.3], [0.3, 0.7], [0.7, 0.7], [0.7, 0.3]],
|
||||
&[0],
|
||||
);
|
||||
ents.push((144, 0, format!("1,1,1,{outer},{hole}")));
|
||||
let imp = iges_import(&iges_file(&ents)).unwrap();
|
||||
assert_eq!(imp.bodies.len(), 1);
|
||||
assert!(!imp.warnings.iter().any(|w| w.contains("not read") || w.contains("face #")), "{:?}", imp.warnings);
|
||||
let m = measure(&imp.bodies[0].body).unwrap();
|
||||
assert!(rel(m.area, 84.0) < 1e-6, "area {}", m.area);
|
||||
}
|
||||
|
||||
/// The box of `trimmed_surfaces_sew_into_a_solid` with one edge written as a B-spline (126)
|
||||
/// over a knot span of 1e-7 or less, as some exporters write short curves: the STEP side read
|
||||
/// that span as a single value and refused the curve, and both faces it bounds were lost.
|
||||
#[test]
|
||||
fn a_curve_over_a_tiny_knot_span_still_bounds_its_faces() {
|
||||
let c =
|
||||
|i: usize| -> [f64; 3] { [if i & 1 != 0 { 10.0 } else { 0.0 }, if i & 2 != 0 { 20.0 } else { 0.0 }, if i & 4 != 0 { 30.0 } else { 0.0 }] };
|
||||
let quads = [[0, 2, 3, 1], [4, 5, 7, 6], [0, 1, 5, 4], [2, 6, 7, 3], [0, 4, 6, 2], [1, 3, 7, 5]];
|
||||
let fmt = |p: [f64; 3]| format!("{:?},{:?},{:?}", p[0], p[1], p[2]);
|
||||
for span in [1e-7f64, 1e-9, 1e-13] {
|
||||
let mut ents: Vec<(i64, i64, String)> = Vec::new();
|
||||
for (fi, q) in quads.iter().enumerate() {
|
||||
let (a, b, cc, d) = (c(q[0]), c(q[1]), c(q[2]), c(q[3]));
|
||||
let surf = format!("1,1,1,1,0,0,1,0,0,0.,0.,1.,1.,0.,0.,1.,1.,1.,1.,1.,1.,{},{},{},{},0.,1.,0.,1.", fmt(a), fmt(b), fmt(d), fmt(cc));
|
||||
ents.push((128, 0, surf));
|
||||
let s_de = 2 * ents.len() - 1;
|
||||
let mut lines = Vec::new();
|
||||
for (k, (p0, p1)) in [(a, b), (b, cc), (cc, d), (d, a)].into_iter().enumerate() {
|
||||
if fi == 0 && k == 0 {
|
||||
ents.push((126, 0, format!("1,1,0,0,1,0,0.,0.,{span:E},{span:E},1.,1.,{},{},0.,{span:E},0.,0.,1.", fmt(p0), fmt(p1))));
|
||||
} else {
|
||||
ents.push((110, 0, format!("{},{}", fmt(p0), fmt(p1))));
|
||||
}
|
||||
lines.push(2 * ents.len() - 1);
|
||||
}
|
||||
ents.push((102, 0, format!("4,{}", lines.iter().map(|x| x.to_string()).collect::<Vec<_>>().join(","))));
|
||||
let comp = 2 * ents.len() - 1;
|
||||
ents.push((142, 0, format!("0,{s_de},0,{comp},2")));
|
||||
let cos = 2 * ents.len() - 1;
|
||||
ents.push((144, 0, format!("{s_de},1,0,{cos}")));
|
||||
}
|
||||
let imp = iges_import(&iges_file(&ents)).unwrap();
|
||||
assert!(imp.warnings.is_empty(), "span {span}: {:?}", imp.warnings);
|
||||
assert!(imp.bodies[0].closed, "span {span}");
|
||||
let m = measure(&imp.bodies[0].body).unwrap();
|
||||
assert!(rel(m.volume, 6000.0) < 1e-6 && rel(m.area, 2200.0) < 1e-6, "span {span}: {} {}", m.volume, m.area);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,63 +53,6 @@ fn rid(p: Option<&Param>, what: &str) -> R<u64> {
|
||||
/// A contractible outer loop whose winding opposes the surface's parameter normal.
|
||||
/// Samples follow the wire through periodic seams; a loop that winds around a surface is
|
||||
/// not a disk in parameter space and must retain the file's sense.
|
||||
/// The point of a cone (a surface revolved from a line that meets its axis) when `ring`, a
|
||||
/// face's only loop, goes once round the axis without touching it: the face then reaches the
|
||||
/// point. `None` for any other surface or loop.
|
||||
fn enclosed_apex(surface: &mt::Surface, ring: &mt::Wire, tol: f64) -> Option<mt::Point3> {
|
||||
use mt::Transform as _;
|
||||
let mt::Surface::RevolutedCurve(proc) = surface else { return None };
|
||||
let rc = proc.entity();
|
||||
let mt::Curve::Line(line) = rc.entity_curve() else { return None };
|
||||
let (o, axis) = (rc.origin(), rc.axis().normalize());
|
||||
// The rotation applied to the revolved surface, applied to its axis and profile too.
|
||||
let m = proc.transform();
|
||||
let (o, axis) = (m.transform_point(o), m.transform_vector(axis).normalize());
|
||||
let (p0, p1) = (m.transform_point(line.0), m.transform_point(line.1));
|
||||
let radius = |p: mt::Point3| (p - o).cross(axis).magnitude();
|
||||
let (r0, r1) = (radius(p0), radius(p1));
|
||||
if !((r0 - r1).abs() > 1e-9 * (r0 + r1)) {
|
||||
return None;
|
||||
}
|
||||
let apex = p0 + (p1 - p0) * (r0 / (r0 - r1));
|
||||
if !(radius(apex) <= tol.max(1e-9) * 10.0) {
|
||||
return None;
|
||||
}
|
||||
// Once round the axis, staying clear of it.
|
||||
let x = (p0 - o) - axis * (p0 - o).dot(axis);
|
||||
let x = if x.magnitude() > tol { x.normalize() } else { return None };
|
||||
let y = axis.cross(x);
|
||||
let mut turned = 0.0;
|
||||
let mut last: Option<f64> = None;
|
||||
let mut first: Option<f64> = None;
|
||||
for edge in ring.edge_iter() {
|
||||
let c = edge.oriented_curve();
|
||||
let (t0, t1) = c.range_tuple();
|
||||
for k in 0..16 {
|
||||
let q = c.subs(t0 + (t1 - t0) * f64::from(k) / 16.0);
|
||||
if radius(q) <= tol {
|
||||
return None;
|
||||
}
|
||||
let d = q - o;
|
||||
let ang = d.dot(y).atan2(d.dot(x));
|
||||
if let Some(l) = last {
|
||||
let mut step = ang - l;
|
||||
step -= (step / std::f64::consts::TAU).round() * std::f64::consts::TAU;
|
||||
turned += step;
|
||||
} else {
|
||||
first = Some(ang);
|
||||
}
|
||||
last = Some(ang);
|
||||
}
|
||||
}
|
||||
if let (Some(l), Some(f)) = (last, first) {
|
||||
let mut step = f - l;
|
||||
step -= (step / std::f64::consts::TAU).round() * std::f64::consts::TAU;
|
||||
turned += step;
|
||||
}
|
||||
((turned.abs() - std::f64::consts::TAU).abs() < 0.1).then_some(apex)
|
||||
}
|
||||
|
||||
fn reversed_outer(surface: &mt::Surface, wire: &mt::Wire, tol: f64) -> bool {
|
||||
if let mt::Surface::RevolutedCurve(proc) = surface {
|
||||
let rc = proc.entity();
|
||||
@@ -378,35 +321,6 @@ impl Builder {
|
||||
surface = guard("step surface orientation", || geom::surface(cx, sid, &extent, &seam, !sense, 0).map_err(crate::KernelError::Failed))
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
// A cone face bounded only by a ring round its axis (no vertex at the point, or a
|
||||
// VERTEX_LOOP there, which has no edges) covers the cone up to its point. Its surface
|
||||
// was spanned over the ring alone, a sliver at the base; it is spanned to the apex,
|
||||
// and the ring is joined to the apex by a seam line out and back, as a cone with a
|
||||
// seam is written.
|
||||
let (mut wires, mut surface) = (wires, surface);
|
||||
if e.name() != "FACE"
|
||||
&& seam.is_empty()
|
||||
&& let [ring] = wires.as_slice()
|
||||
&& let Some(apex) = enclosed_apex(&surface, ring, cx.tol)
|
||||
&& let Some(first) = ring.edge_iter().next()
|
||||
{
|
||||
let v = first.front().clone();
|
||||
let a = mt::Vertex::new(apex);
|
||||
let line = mt::Edge::new_unchecked(&v, &a, mt::Curve::Line(mt::Line(v.point(), apex)));
|
||||
let mut to_apex = extent.clone();
|
||||
to_apex.push(apex);
|
||||
let seam_line = [v.point(), apex];
|
||||
let sid = rid(p.get(2), "face geometry")?;
|
||||
let sense = p.get(3).and_then(Param::as_bool).unwrap_or(true);
|
||||
let spanned =
|
||||
guard("step cone to its point", || geom::surface(cx, sid, &to_apex, &seam_line, sense, 0).map_err(crate::KernelError::Failed))
|
||||
.map_err(|e| e.to_string())?;
|
||||
let mut edges: Vec<mt::Edge> = ring.edge_iter().cloned().collect();
|
||||
edges.push(line.clone());
|
||||
edges.push(line.inverse());
|
||||
wires = vec![edges.into()];
|
||||
surface = spanned;
|
||||
}
|
||||
// Loops on a surface of revolution start away from the axis: a pole (cone apex, sphere
|
||||
// pole) has no definite rotation parameter, and the mesher takes the first point's
|
||||
// parameter as the anchor for the rest of the loop.
|
||||
@@ -641,22 +555,5 @@ pub(crate) fn read_item(cx: &Ctx, id: u64) -> R<BrepOut> {
|
||||
if checked.is_err() {
|
||||
closed = false;
|
||||
}
|
||||
// A void's faces face into the cavity, out of the material: it encloses volume of the
|
||||
// opposite sign to the outer shell. The ORIENTED_CLOSED_SHELL flag alone doesn't say so:
|
||||
// some exporters write .F. over a shell already facing into the cavity (#3730), others .T.
|
||||
// A void enclosing volume of the outer shell's sign is turned inside out.
|
||||
if e.name() == "BREP_WITH_VOIDS"
|
||||
&& checked.is_ok()
|
||||
&& let Some((outer, voids)) = shells.split_first_mut()
|
||||
&& let Some(outside) = crate::body::shell_volume(outer)
|
||||
{
|
||||
for void in voids {
|
||||
if crate::body::shell_volume(void).is_some_and(|v| v * outside > 0.0) {
|
||||
for face in void.face_iter_mut() {
|
||||
face.invert();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(BrepOut { solid: mt::Solid::new_unchecked(shells), failed, closed, file_faces, face_origin })
|
||||
}
|
||||
|
||||
@@ -236,40 +236,3 @@ fn hostile_conics_never_panic() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A cone to a point written with no vertex at its point (#258): the cone face's only loop is
|
||||
/// its base circle, or that circle and a VERTEX_LOOP at the point, which has no edges. It was
|
||||
/// read as a sliver at the base and the solid came in as a flat disc; the face reaches the
|
||||
/// point now.
|
||||
#[test]
|
||||
fn a_cone_bounded_only_by_its_base_circle_reaches_its_point() {
|
||||
let (r, h) = (6.0f64, 15.0f64);
|
||||
for apex_loop in [false, true] {
|
||||
let mut t = String::from(HEAD);
|
||||
t += "#1=CARTESIAN_POINT('',(0.,0.,0.));\n#2=DIRECTION('',(0.,0.,-1.));\n#3=DIRECTION('',(1.,0.,0.));\n#4=AXIS2_PLACEMENT_3D('',#1,#2,#3);\n";
|
||||
t += &format!("#5=CONICAL_SURFACE('',#4,{r:.1},{:.15});\n", (r / h).atan());
|
||||
t += "#6=DIRECTION('',(0.,0.,1.));\n#7=AXIS2_PLACEMENT_3D('',#1,#6,#3);\n#8=PLANE('',#7);\n";
|
||||
t += &format!("#9=CIRCLE('',#7,{r:.1});\n#10=CARTESIAN_POINT('',({r:.1},0.,0.));\n#11=VERTEX_POINT('',#10);\n");
|
||||
t += "#12=EDGE_CURVE('',#11,#11,#9,.T.);\n";
|
||||
t += "#20=ORIENTED_EDGE('',*,*,#12,.F.);\n#21=EDGE_LOOP('',(#20));\n#22=FACE_OUTER_BOUND('',#21,.T.);\n#23=ADVANCED_FACE('',(#22),#8,.F.);\n";
|
||||
t += "#30=ORIENTED_EDGE('',*,*,#12,.T.);\n#31=EDGE_LOOP('',(#30));\n#32=FACE_OUTER_BOUND('',#31,.T.);\n";
|
||||
if apex_loop {
|
||||
t += &format!(
|
||||
"#33=CARTESIAN_POINT('',(0.,0.,{h:.1}));\n#34=VERTEX_POINT('',#33);\n#35=VERTEX_LOOP('',#34);\n#36=FACE_BOUND('',#35,.T.);\n"
|
||||
);
|
||||
t += "#37=ADVANCED_FACE('',(#32,#36),#5,.T.);\n";
|
||||
} else {
|
||||
t += "#37=ADVANCED_FACE('',(#32),#5,.T.);\n";
|
||||
}
|
||||
t += "#80=CLOSED_SHELL('',(#23,#37));\n#81=MANIFOLD_SOLID_BREP('Cone',#80);\n";
|
||||
t += CONTEXT;
|
||||
t += TAIL;
|
||||
let imp = step_import(&t).unwrap();
|
||||
let body = &imp.bodies[0].body;
|
||||
let m = measure(body).unwrap();
|
||||
let exact = PI * r * r * h / 3.0;
|
||||
assert!(rel(m.volume, exact) < 2e-3, "vertex loop {apex_loop}: volume {} vs {exact}", m.volume);
|
||||
let top = body.tessellate(0.01).unwrap().positions.iter().map(|p| p.z).fold(f64::MIN, f64::max);
|
||||
assert!((top - h).abs() < 1e-6, "vertex loop {apex_loop}: reaches z = {top}, not {h}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,30 +111,6 @@ fn seam_only_sphere_void_is_turned_inside_out() {
|
||||
assert!(rel(m.area, area) < 1e-3, "area {} vs {area}", m.area);
|
||||
}
|
||||
|
||||
/// A block with a closed cavity, written as BREP_WITH_VOIDS. SolidCraft writes the void's
|
||||
/// shell facing into the cavity with ORIENTED_CLOSED_SHELL `.T.`; some exporters write the
|
||||
/// same faces with `.F.` (#3730), which turned the void inside out and added its volume. Either
|
||||
/// way the cavity is taken out.
|
||||
#[test]
|
||||
fn a_void_reads_as_a_cavity_whichever_way_its_shell_flag_points() {
|
||||
let block = box_solid(Vec3::ZERO, Vec3::new(40.0, 30.0, 12.5)).unwrap();
|
||||
let cavity = box_solid(Vec3::new(10.0, 12.0, 4.0), Vec3::new(30.0, 18.0, 9.0)).unwrap();
|
||||
let Ok(Some(hollow)) = boolean(&block, &cavity, BoolOp::Cut) else { panic!("cut") };
|
||||
let want = 40.0 * 30.0 * 12.5 - 20.0 * 6.0 * 5.0;
|
||||
assert!(rel(measure(&hollow).unwrap().volume, want) < 1e-9);
|
||||
let text = step_export(&[&hollow], "void").unwrap();
|
||||
let flag = |t: &str| t.lines().filter(|l| l.contains("ORIENTED_CLOSED_SHELL")).map(str::to_owned).collect::<Vec<_>>();
|
||||
let written = flag(&text);
|
||||
assert!(written.len() == 1 && written[0].ends_with(".T.);"), "{written:?}");
|
||||
let flipped = text.replace(&written[0], &written[0].replace(".T.);", ".F.);"));
|
||||
for t in [text, flipped] {
|
||||
let imp = step_import(&t).unwrap();
|
||||
assert!(imp.warnings.is_empty(), "{:?}", imp.warnings);
|
||||
let m = measure(&imp.bodies[0].body).unwrap();
|
||||
assert!(rel(m.volume, want) < 1e-6, "{:?}: volume {} vs {want}", flag(&t), m.volume);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_antipodal_seam_sphere_does_not_panic() {
|
||||
let text = seam_sphere_step(Vec3::ZERO, 2.0, Vec3::Z, Vec3::X, 0.0, false, 2.5);
|
||||
@@ -1770,53 +1746,3 @@ fn extruded_ellipse_patch_crossing_profile_seam_stays_trimmed() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parts of known shape from the step-conformance corpus (`tests/step-conformance`, generated
|
||||
/// from formulas by https://github.com/dan335/step-conformance at 96f30bb, CC0 1.0):
|
||||
/// each reads back with its exact volume (from the generator's own profile), its box and its
|
||||
/// face count. Left out: `torus_ring` (meshes below its equator, a meshing issue for the
|
||||
/// tessellation work) and the 1.4 MB `plate_900_holes`.
|
||||
#[test]
|
||||
fn step_conformance_parts_read_back_their_exact_shape() {
|
||||
// (file, exact volume (mm³), box (min, max), kernel faces, bodies)
|
||||
#[rustfmt::skip]
|
||||
let parts: [(&str, &str, f64, [f64; 6], usize, usize); 17] = [
|
||||
("cone_point", include_str!("../../../../tests/step-conformance/cone_point.step"), 1000.0 * std::f64::consts::PI, [-10.0, -10.0, 0.0, 10.0, 10.0, 30.0], 2, 1),
|
||||
("cone_point_vertex", include_str!("../../../../tests/step-conformance/cone_point_vertex.step"), 1000.0 * std::f64::consts::PI, [-10.0, -10.0, 0.0, 10.0, 10.0, 30.0], 2, 1),
|
||||
("cylinder", include_str!("../../../../tests/step-conformance/cylinder.step"), 9424.778, [-10.0, -10.0, 0.0, 10.0, 10.0, 30.0], 3, 1),
|
||||
("cylinder_inch", include_str!("../../../../tests/step-conformance/cylinder_inch.step"), 9424.778, [-10.0, -10.0, 0.0, 10.0, 10.0, 30.0], 3, 1),
|
||||
("cylinder_nurbs", include_str!("../../../../tests/step-conformance/cylinder_nurbs.step"), 9424.778, [-10.0, -10.0, 0.0, 10.0, 10.0, 30.0], 3, 1),
|
||||
("cylinder_placed", include_str!("../../../../tests/step-conformance/cylinder_placed.step"), 9424.778, [31.835, -33.165, 1.835, 65.485, 0.485, 35.485], 3, 1),
|
||||
("cylinder_seam", include_str!("../../../../tests/step-conformance/cylinder_seam.step"), 9424.778, [-10.0, -10.0, 0.0, 10.0, 10.0, 30.0], 3, 1),
|
||||
("cylinder_twice", include_str!("../../../../tests/step-conformance/cylinder_twice.step"), 18849.556, [-10.0, -10.0, 0.0, 90.0, 10.0, 30.0], 6, 2),
|
||||
("dome", include_str!("../../../../tests/step-conformance/dome.step"), 16755.161, [-20.0, -20.0, 0.0, 20.0, 20.0, 20.0], 2, 1),
|
||||
("dome_nurbs", include_str!("../../../../tests/step-conformance/dome_nurbs.step"), 16755.161, [-20.0, -20.0, 0.0, 20.0, 20.0, 20.0], 2, 1),
|
||||
("far_cylinder", include_str!("../../../../tests/step-conformance/far_cylinder.step"), 9424.778, [99990.0, -10.0, 0.0, 100010.0, 10.0, 30.0], 3, 1),
|
||||
("grooved_shaft", include_str!("../../../../tests/step-conformance/grooved_shaft.step"), 153532.251, [-10.0, -10.0, 0.0, 10.0, 10.0, 501.0], 753, 1),
|
||||
("plate_100_holes", include_str!("../../../../tests/step-conformance/plate_100_holes.step"), 71588.321, [0.0, 0.0, 0.0, 100.0, 100.0, 10.0], 110, 1),
|
||||
("sheet_disc", include_str!("../../../../tests/step-conformance/sheet_disc.step"), 193396443.755, [-1800.0, -1800.0, 0.0, 1800.0, 1800.0, 19.0], 3, 1),
|
||||
("tiny_pin", include_str!("../../../../tests/step-conformance/tiny_pin.step"), std::f64::consts::FRAC_PI_8, [-0.25, -0.25, 0.0, 0.25, 0.25, 2.0], 3, 1),
|
||||
("torus_ring_nurbs", include_str!("../../../../tests/step-conformance/torus_ring_nurbs.step"), 4934.802, [-25.0, -25.0, 0.0, 25.0, 25.0, 5.0], 2, 1),
|
||||
("washer", include_str!("../../../../tests/step-conformance/washer.step"), 3534.292, [-20.0, -20.0, 0.0, 20.0, 20.0, 3.0], 4, 1),
|
||||
];
|
||||
for (name, text, volume, bbox, faces, bodies) in parts {
|
||||
let imp = step_import(text).unwrap_or_else(|e| panic!("{name}: {e}"));
|
||||
assert!(imp.warnings.is_empty(), "{name}: {:?}", imp.warnings);
|
||||
assert_eq!(imp.bodies.len(), bodies, "{name}");
|
||||
let (mut v, mut n) = (0.0, 0);
|
||||
let mut b = solidcraft_geom::Aabb3::EMPTY;
|
||||
for body in &imp.bodies {
|
||||
let m = measure(&body.body).unwrap_or_else(|e| panic!("{name}: {e}"));
|
||||
v += m.volume;
|
||||
n += m.faces;
|
||||
b = b.union(&m.bbox);
|
||||
}
|
||||
assert!(rel(v, volume) < 1e-3, "{name}: volume {v} vs {volume}");
|
||||
assert_eq!(n, faces, "{name}: faces");
|
||||
let [x0, y0, z0, x1, y1, z1] = bbox;
|
||||
let size = Vec3::new(x1 - x0, y1 - y0, z1 - z0).len();
|
||||
for (got, want) in [(b.min, Vec3::new(x0, y0, z0)), (b.max, Vec3::new(x1, y1, z1))] {
|
||||
assert!(got.dist(want) <= size * 1e-3, "{name}: box corner {got:?} vs {want:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3012,45 +3012,3 @@ fn placing_a_body_does_not_turn_its_revolved_faces_again() {
|
||||
assert_eq!(a.faces, b.faces);
|
||||
assert!(a.bbox.min.dist(b.bbox.min) < 1e-9 && a.bbox.max.dist(b.bbox.max) < 1e-9);
|
||||
}
|
||||
|
||||
/// A plate 3 m long and 2 mm thick with ten holes of radius `r` along a diagonal, turned by
|
||||
/// `tilt` about the X axis and read back from STEP (each rim is then two half circles).
|
||||
fn long_plate_with_small_holes(r: f64, tilt: f64) -> (Body, Vec<Vec3>) {
|
||||
let mut p = box_solid(Vec3::new(-1500.0, -150.0, 0.0), Vec3::new(1500.0, 150.0, 2.0)).unwrap();
|
||||
let centres: Vec<Vec3> = (0..10).map(|i| Vec3::new(-1259.3 + 271.3 * i as f64, -120.0 + 27.7 * i as f64, 0.0)).collect();
|
||||
for &c in ¢res {
|
||||
let tool = cylinder(c - Vec3::Z, Vec3::Z, r, 4.0).unwrap();
|
||||
p = boolean(&p, &tool, BoolOp::Cut).unwrap().unwrap();
|
||||
}
|
||||
let shift = Vec3::new(0.0, 300.0, 300.0);
|
||||
let p = transform(&p, shift, Vec3::ZERO, Vec3::X, tilt).unwrap();
|
||||
let text = step_export(&[&p], "plate").unwrap();
|
||||
let p = step_import(&text).unwrap().bodies.remove(0).body;
|
||||
let (s, c) = tilt.sin_cos();
|
||||
let placed = centres.iter().map(|q| Vec3::new(q.x, q.y * c - q.z * s, q.y * s + q.z * c) + shift).collect();
|
||||
(p, placed)
|
||||
}
|
||||
|
||||
/// A small hole in a face of a large body, divided by a boolean. The boolean's tolerance follows
|
||||
/// the larger body (a few millimetres on a part a few metres long), so each half circle of the
|
||||
/// hole's rim was a single chord; the rim's polygon in the face's parameters had no area, and its
|
||||
/// sign, which tells a hole from a face piece, came out at random. The hole then became a face of
|
||||
/// its own, inside out, and the face it was cut in was left whole: a box cut round such a hole
|
||||
/// gave a piece without it, and the shared part of that piece and a pin in the hole came out
|
||||
/// negative or not at all.
|
||||
#[test]
|
||||
fn a_box_cut_round_a_small_hole_of_a_long_plate_keeps_the_hole() {
|
||||
for (r, tilt) in [(1.8, 0.27), (1.8, 0.7)] {
|
||||
let (p, centres) = long_plate_with_small_holes(r, tilt);
|
||||
for q in centres {
|
||||
let g = Vec3::new(8.0, 8.0, 8.0);
|
||||
let cut = box_solid(q - g, q + g).unwrap();
|
||||
let piece = boolean(&p, &cut, BoolOp::Intersect).unwrap().unwrap();
|
||||
let walls = (0..piece.face_count()).filter(|&i| matches!(face_surface(&piece, i), Some(FaceSurface::Cylinder { .. }))).count();
|
||||
// Both faces of the plate in the box keep the hole: an outer loop and the rim.
|
||||
let holed = piece.solid.face_iter().filter(|f| f.boundaries().len() == 2).count();
|
||||
assert!(walls >= 1 && holed == 2, "r {r}, tilt {tilt}, hole at {q:?}: {walls} walls, {holed} faces with a hole");
|
||||
assert!(measure(&piece).unwrap().volume > 0.0, "r {r}, tilt {tilt}, hole at {q:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -421,35 +421,6 @@ fn batch_rolls_back_with_a_full_undo_history() {
|
||||
assert_eq!(d["undo"], undo);
|
||||
}
|
||||
|
||||
/// Regression (#3242): a failed batch containing `file.new` reported `rolled_back: true` and
|
||||
/// kept the replacement design. The checkpoint survives the new document, so the design the
|
||||
/// batch started in — the 1 mm cube — is back when the batch fails.
|
||||
#[test]
|
||||
fn a_failed_batch_with_file_new_restores_the_original_design() {
|
||||
let mut s = server();
|
||||
tool(&mut s, "batch", json!({"commands": [{"command": "solid.box", "params": {"length": 1, "width": 1, "height": 1, "body_name": "Original"}}]}));
|
||||
let name = tool(&mut s, "inspect_design", json!({"measure": false, "sketches": false}))["name"].clone();
|
||||
let r = raw_tool(
|
||||
&mut s,
|
||||
"batch",
|
||||
json!({"rollback": true, "commands": [
|
||||
{"command": "file.new", "params": {"name": "Replacement"}},
|
||||
{"command": "solid.box", "params": {"length": 2, "width": 2, "height": 2}},
|
||||
{"command": "not.a.command"},
|
||||
]}),
|
||||
);
|
||||
assert_eq!(r["result"]["isError"], true, "{r}");
|
||||
let body: Value = serde_json::from_str(r["result"]["content"][0]["text"].as_str().unwrap()).unwrap();
|
||||
assert_eq!(body["failed"]["index"], 2, "{body}");
|
||||
assert_eq!(body["rolled_back"], true, "{body}");
|
||||
assert_eq!(body["undone"], 2, "{body}");
|
||||
// The original 1 mm cube is back; the replacement design is gone.
|
||||
assert_eq!(tool(&mut s, "measure", json!({}))["body_count"], 1);
|
||||
assert!(rel(volume(&mut s), 1.0) < 1e-9, "{}", volume(&mut s));
|
||||
let d = tool(&mut s, "inspect_design", json!({"measure": false, "sketches": false}));
|
||||
assert_eq!(d["name"], name, "{d}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn helper_sketches_can_be_hidden() {
|
||||
let mut s = server();
|
||||
@@ -1375,27 +1346,6 @@ fn batch_reports_a_failed_engine_batch_like_before() {
|
||||
);
|
||||
}
|
||||
|
||||
/// `rolled_back` tells whether the design went back, not whether a rollback was asked for
|
||||
/// (#3242): the app's `rolled_back`, or, for an app that predates that field, none of the
|
||||
/// completed steps undone means the design did not go back.
|
||||
#[test]
|
||||
fn a_batch_the_app_could_not_roll_back_reports_it() {
|
||||
let failed = |reply: Value| {
|
||||
let (_, body, is_error) = recorded_batch(Ok(reply), json!({"commands": [{"command": "a.one"}, {"command": "a.two"}]}));
|
||||
assert!(is_error);
|
||||
body
|
||||
};
|
||||
let body = failed(
|
||||
json!({"completed": 1, "results": [{"x": 1}], "failed": {"index": 1, "command": "a.two", "error": "boom"}, "rolled_back": false, "undone": 0}),
|
||||
);
|
||||
assert_eq!(body["rolled_back"], false, "{body}");
|
||||
let body = failed(json!({"completed": 1, "results": [{"x": 1}], "failed": {"index": 1, "command": "a.two", "error": "boom"}, "undone": 0}));
|
||||
assert_eq!(body["rolled_back"], false, "{body}");
|
||||
// A first step that failed left nothing to take back.
|
||||
let body = failed(json!({"completed": 0, "results": [], "failed": {"index": 0, "command": "a.one", "error": "boom"}, "undone": 0}));
|
||||
assert_eq!(body["rolled_back"], true, "{body}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_falls_back_when_the_app_lacks_engine_batch() {
|
||||
let (b, body, is_error) =
|
||||
|
||||
+19
-28
@@ -500,13 +500,12 @@ impl Undo {
|
||||
}
|
||||
}
|
||||
|
||||
/// Go back; (whether the design is back as it was, the number of steps undone). A failed
|
||||
/// rollback says so rather than reporting the flag the caller asked for (#3242).
|
||||
fn rollback(self, b: &mut dyn Backend, steps: usize) -> (bool, u64) {
|
||||
/// Go back; the number of steps undone.
|
||||
fn rollback(self, b: &mut dyn Backend, steps: usize) -> u64 {
|
||||
match self {
|
||||
Undo::Checkpoint(id) => match exec(b, "edit.restore_checkpoint", json!({"checkpoint": id})) {
|
||||
Ok(_) => (true, steps as u64),
|
||||
Err(_) => (false, 0),
|
||||
Ok(_) => steps as u64,
|
||||
Err(_) => 0,
|
||||
},
|
||||
Undo::Depth(before) => {
|
||||
let after = undo_depth(b).unwrap_or(before);
|
||||
@@ -517,10 +516,9 @@ impl Undo {
|
||||
}
|
||||
undone += 1;
|
||||
}
|
||||
(undone == steps as u64, undone)
|
||||
undone
|
||||
}
|
||||
// Nothing remembered the design; only a first step that failed left nothing behind.
|
||||
Undo::None => (steps == 0, 0),
|
||||
Undo::None => 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -585,12 +583,12 @@ fn batch(b: &mut dyn Backend, a: &Map<String, Value>) -> Value {
|
||||
match r {
|
||||
Ok(v) => results.push(if verbose { v } else { compact(v) }),
|
||||
Err(e) => {
|
||||
let (rolled_back, undone) = start.rollback(b, i);
|
||||
let undone = start.rollback(b, i);
|
||||
let reply = json!({
|
||||
"ok": false,
|
||||
"completed": i,
|
||||
"failed": {"index": i, "command": command, "error": e},
|
||||
"rolled_back": rolled_back,
|
||||
"rolled_back": rollback,
|
||||
"undone": undone,
|
||||
"results": results,
|
||||
});
|
||||
@@ -618,24 +616,17 @@ fn batch_reply(r: &Value, rollback: bool, verbose: bool) -> Value {
|
||||
r["results"].as_array().cloned().unwrap_or_default().into_iter().map(|v| if verbose { v } else { compact(v) }).collect();
|
||||
let (mut reply, error) = match r.get("failed").filter(|f| !f.is_null()) {
|
||||
None => (json!({"ok": true, "completed": results.len(), "results": results}), false),
|
||||
Some(failed) => {
|
||||
// `rolled_back` tells whether the design went back, not whether a rollback was
|
||||
// asked for (#3242): the app answers with its own `rolled_back`; an app that
|
||||
// predates that field and undid none of the steps it had completed did not go back.
|
||||
let undone = r["undone"].as_u64().unwrap_or(0);
|
||||
let went_back = r.get("rolled_back").and_then(Value::as_bool).unwrap_or(undone > 0 || r["completed"].as_u64() == Some(0));
|
||||
(
|
||||
json!({
|
||||
"ok": false,
|
||||
"completed": r["completed"],
|
||||
"failed": failed,
|
||||
"rolled_back": rollback && went_back,
|
||||
"undone": r["undone"],
|
||||
"results": results,
|
||||
}),
|
||||
true,
|
||||
)
|
||||
}
|
||||
Some(failed) => (
|
||||
json!({
|
||||
"ok": false,
|
||||
"completed": r["completed"],
|
||||
"failed": failed,
|
||||
"rolled_back": rollback,
|
||||
"undone": r["undone"],
|
||||
"results": results,
|
||||
}),
|
||||
true,
|
||||
),
|
||||
};
|
||||
// The app timed the steps where they ran.
|
||||
if let Some(o) = reply.as_object_mut() {
|
||||
|
||||
@@ -63,7 +63,7 @@ pub use profiles::{
|
||||
CurvePick, Profile, distinct_curves, find_drawn_profiles, find_profiles, merge_regions, profile_at_point, profile_by_curves,
|
||||
without_text_counters,
|
||||
};
|
||||
pub use shape::{Shape, intersections, intersections_within};
|
||||
pub use shape::{Shape, intersections};
|
||||
pub use solver::solve_after_edit_with_analysis;
|
||||
pub use solver::{
|
||||
Analysis, CONSISTENT, DrivingValues, Hold, SolveReport, SolveStatus, constraint_residual, determined_by, measure_dimension, move_line_towards,
|
||||
|
||||
@@ -428,19 +428,9 @@ pub(crate) fn draw_row(ui: &mut egui::Ui, id: egui::Id, row: &Row) -> RowResp {
|
||||
if row.selected { crate::theme::TabAccent::current(ui.ctx()).ink } else { row.color.unwrap_or(if row.dim { t.text_dim } else { t.text }) };
|
||||
let label_display = if row.name { crate::i18n::tr_name(row.label) } else { crate::i18n::tr_dyn(row.label) };
|
||||
let font = if row.selected { crate::fonts::semibold(t.font_label) } else { FontId::proportional(t.font_label) };
|
||||
// The label stops before the badges and the card's edge; a longer name ends in an ellipsis
|
||||
// and its full text is in the tooltip.
|
||||
let badges_width = row.badges.len() as f32 * 15.0;
|
||||
let room = (r.right() - 4.0 - badges_width - x).max(0.0);
|
||||
let mut job = egui::text::LayoutJob::simple(label_display.to_string(), font, col, room);
|
||||
job.wrap.max_rows = 1;
|
||||
job.wrap.break_anywhere = true;
|
||||
job.wrap.overflow_character = Some('\u{2026}');
|
||||
let galley = ui.painter().layout_job(job);
|
||||
let galley = ui.painter().layout_no_wrap(label_display, font, col);
|
||||
let lw = galley.size().x;
|
||||
let elided = galley.elided;
|
||||
crate::scenario::publish_rect(&format!("row_label:{}", row.label), Rect::from_min_size(pos2(x, cy - galley.size().y / 2.0), galley.size()));
|
||||
ui.painter().with_clip_rect(ui.painter().clip_rect().intersect(r)).galley(pos2(x, cy - galley.size().y / 2.0), galley, col);
|
||||
ui.painter().galley(pos2(x, cy - galley.size().y / 2.0), galley, col);
|
||||
let mut bx = (x + lw + 6.0).min(r.right() - 16.0);
|
||||
for b in row.badges {
|
||||
let br = Rect::from_center_size(pos2(bx + 6.0, cy), vec2(12.0, 12.0));
|
||||
@@ -456,7 +446,6 @@ pub(crate) fn draw_row(ui: &mut egui::Ui, id: egui::Id, row: &Row) -> RowResp {
|
||||
};
|
||||
let resp = match tip {
|
||||
Some(t) => resp.on_hover_text(t),
|
||||
None if elided => resp.on_hover_text(label_display.to_string()),
|
||||
None => resp,
|
||||
};
|
||||
out.on_control = press_on != 0;
|
||||
@@ -1248,7 +1237,7 @@ fn origin_rows(app: &mut SolveApp, ui: &mut egui::Ui, depth: usize, acts: &Actio
|
||||
let sel = match icon {
|
||||
"plane" => Some(Sel::Plane { name: k.into() }),
|
||||
"axis" => Some(Sel::Axis { name: k.into() }),
|
||||
_ => Some(Sel::Vertex { body: String::new(), point: solidcraft_engine::geom::Vec3::ZERO }),
|
||||
_ => None,
|
||||
};
|
||||
let (selected, mark) = lit_row(lit(&thing));
|
||||
let r = draw_row(
|
||||
@@ -1568,8 +1557,6 @@ fn pick_from_browser(app: &mut SolveApp, x: Sel) {
|
||||
let hit = match &x {
|
||||
Sel::Plane { name } => crate::viewport::Hit::Plane { name: name.clone(), point: solidcraft_engine::geom::Vec3::ZERO },
|
||||
Sel::Axis { name } => crate::viewport::Hit::Axis { name: name.clone() },
|
||||
// The origin point goes only into a dialog input that takes it.
|
||||
Sel::Vertex { body, point } if body.is_empty() && app.dialog.is_some() => crate::viewport::Hit::Vertex { body: String::new(), point: *point },
|
||||
_ => return,
|
||||
};
|
||||
if let Some(mut d) = app.dialog.take() {
|
||||
|
||||
@@ -239,16 +239,3 @@ fn browser_width_rejects_hostile_preferences_and_tiny_windows_still_fit() {
|
||||
assert!(available.contains_rect(bounds));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_long_body_name_stays_inside_the_card_and_ends_in_an_ellipsis() {
|
||||
let mut h = Harness::new();
|
||||
h.call("ui.resize", json!({"width":1400,"height":900}));
|
||||
let name = "W".repeat(200);
|
||||
h.app.run("solid.box", json!({"body_name":name, "operation":"new", "length":10, "width":10, "height":10})).unwrap();
|
||||
h.settle();
|
||||
let card = rect_of("browser:card").unwrap();
|
||||
let label = rect_of(&format!("row_label:{name}")).expect("the body row is drawn");
|
||||
assert!(label.right() <= card.right(), "{label:?} spills out of {card:?}");
|
||||
assert!(label.width() > 100.0, "the name keeps the room it has: {label:?}");
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ use solidcraft_engine::geom::Vec3;
|
||||
use crate::SolveApp;
|
||||
use crate::dialogs::{Dialog, Kind};
|
||||
use crate::gizmo::{self, Change, Drag};
|
||||
use crate::selection::{AXES, BODIES, EDGES, FACES, ORIGIN, SelInput, VERTICES};
|
||||
use crate::selection::{AXES, BODIES, EDGES, FACES, SelInput, VERTICES};
|
||||
|
||||
pub const TYPES: [&str; 5] = ["Free Move", "Translate", "Rotate", "Point to Point", "Point to Position"];
|
||||
pub const FREE: usize = 0;
|
||||
@@ -20,7 +20,7 @@ pub const TRANSLATE: usize = 1;
|
||||
pub const ROTATE: usize = 2;
|
||||
pub const POINT_TO_POINT: usize = 3;
|
||||
pub const POINT_TO_POSITION: usize = 4;
|
||||
const POINTS: u16 = VERTICES | FACES | EDGES | ORIGIN;
|
||||
const POINTS: u16 = VERTICES | FACES | EDGES;
|
||||
|
||||
/// A fresh Move dialog of a type (all values zero).
|
||||
pub fn kind(mode: usize) -> Kind {
|
||||
|
||||
@@ -69,33 +69,3 @@ fn every_move_type_has_somewhere_to_put_its_value_box() {
|
||||
assert!(at.is_some(), "{} has nowhere to put its value box", TYPES[mode]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Point to Point takes the origin point as From or To (#3696): a click on the origin dot picks
|
||||
/// it, and the body moves from its corner onto the origin.
|
||||
#[test]
|
||||
fn point_to_point_takes_the_origin_point() {
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
h.app.ui.show_origin = true;
|
||||
h.app.run("solid.box", json!({"length": 20, "width": 20, "height": 20})).unwrap();
|
||||
h.app.run("solid.move", json!({"bodies": ["Body1"], "translate": ["30 mm", "20 mm", "0 mm"]})).unwrap();
|
||||
h.app.cam.distance = 200.0;
|
||||
h.settle();
|
||||
let hi = h.app.session.model.state().body("Body1").unwrap().mesh().bounds().max;
|
||||
h.app.start("solid.move");
|
||||
{
|
||||
let d = h.app.dialog.as_mut().unwrap();
|
||||
d.kind = kind(POINT_TO_POINT);
|
||||
d.inputs = inputs(POINT_TO_POINT);
|
||||
d.inputs[0].items = vec![Sel::Body { name: "Body1".into() }];
|
||||
d.active = 1;
|
||||
}
|
||||
let corner = h.call("ui.at", json!({ "world": [hi.x, hi.y, hi.z] }))["result"].clone();
|
||||
h.step(&json!({ "click": corner })).unwrap();
|
||||
h.app.dialog.as_mut().unwrap().active = 2;
|
||||
let origin = h.call("ui.at", json!({ "world": [0, 0, 0] }))["result"].clone();
|
||||
h.step(&json!({ "click": origin })).unwrap();
|
||||
let d = h.app.dialog.clone().unwrap();
|
||||
assert_eq!(d.inputs[2].items, vec![Sel::Vertex { body: String::new(), point: Vec3::ZERO }], "the origin is the To point");
|
||||
let p = params(&h.app, &d).unwrap();
|
||||
assert_eq!(p["translate"], json!([mm(-hi.x), mm(-hi.y), mm(-hi.z)]), "the corner goes onto the origin");
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! The Save as mesh dialog (`file.save_mesh`): which bodies, the mesh format, and optionally a
|
||||
//! slicer to open the saved file in.
|
||||
//!
|
||||
//! Every place that saves a mesh opens this dialog: File › Export STL / Export 3MF, the Mesh tab, and the
|
||||
//! Every place that saves a mesh opens this dialog: File › Save as mesh, the Mesh tab, and the
|
||||
//! Save As Mesh item of the body menus. The slicer list holds the programs the user added (kept
|
||||
//! between runs, newest first), then the slicers found on this machine, then "Custom…", which asks
|
||||
//! for a program and adds it at the top; "Custom…" itself is never the selection. Pressing Delete
|
||||
@@ -39,14 +39,6 @@ pub struct SaveMesh {
|
||||
stale: bool,
|
||||
}
|
||||
|
||||
impl SaveMesh {
|
||||
/// The chosen format, as `file.save_mesh` names it (`stl`, `3mf`, `3mf-tagged`).
|
||||
#[cfg(test)]
|
||||
pub(crate) fn format_name(&self) -> &'static str {
|
||||
FORMATS.get(self.format).map_or("stl", |f| f.1)
|
||||
}
|
||||
}
|
||||
|
||||
/// One line of the slicer list.
|
||||
struct Entry {
|
||||
key: String,
|
||||
@@ -95,17 +87,6 @@ pub fn start(app: &SolveApp, id: &str) -> Option<(Kind, Vec<SelInput>)> {
|
||||
Some((kind, vec![SelInput::new("Bodies", BODIES, true)]))
|
||||
}
|
||||
|
||||
/// Open the dialog with `format` (`stl`, `3mf` or `3mf-tagged`) chosen, whatever was used last:
|
||||
/// File › Export STL… and Export 3MF…. The user can still change it in the dialog.
|
||||
pub fn start_with_format(app: &mut SolveApp, format: &str) {
|
||||
app.start("file.save_mesh");
|
||||
if let Some(index) = FORMATS.iter().position(|f| f.1 == format)
|
||||
&& let Some(Kind::SaveMesh(k)) = app.dialog.as_mut().map(|d| &mut d.kind)
|
||||
{
|
||||
k.format = index;
|
||||
}
|
||||
}
|
||||
|
||||
/// A popup with the slicer list is open (Delete belongs to it, not to the design's selection).
|
||||
pub fn popup_open(ctx: &egui::Context) -> bool {
|
||||
let frame = ctx.cumulative_frame_nr();
|
||||
|
||||
@@ -18,9 +18,7 @@ pub const MAX_OPEN: usize = 32;
|
||||
|
||||
/// A design that is not the active one: everything that belongs to it alone.
|
||||
pub struct DocSlot {
|
||||
pub(crate) session: Session,
|
||||
pub(crate) autosave: Option<solidcraft_engine::recovery::Autosaver>,
|
||||
pub(crate) big_operation: bool,
|
||||
session: Session,
|
||||
cam: Camera,
|
||||
pre_sketch_cam: Option<Camera>,
|
||||
pre_sketch_marker: Option<Option<usize>>,
|
||||
@@ -40,16 +38,7 @@ struct DocView {
|
||||
|
||||
impl DocSlot {
|
||||
fn new(session: Session) -> DocSlot {
|
||||
DocSlot {
|
||||
session,
|
||||
autosave: None,
|
||||
big_operation: false,
|
||||
cam: Camera::default(),
|
||||
pre_sketch_cam: None,
|
||||
pre_sketch_marker: None,
|
||||
view: DocView::default(),
|
||||
occurrence_move: None,
|
||||
}
|
||||
DocSlot { session, cam: Camera::default(), pre_sketch_cam: None, pre_sketch_marker: None, view: DocView::default(), occurrence_move: None }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,10 +58,6 @@ pub struct Documents {
|
||||
}
|
||||
|
||||
impl Documents {
|
||||
pub(crate) fn background_mut(&mut self) -> impl Iterator<Item = &mut DocSlot> {
|
||||
self.slots.iter_mut().filter_map(Option::as_mut)
|
||||
}
|
||||
|
||||
/// Number of open designs (at least the active one).
|
||||
pub fn count(&self) -> usize {
|
||||
self.slots.len().max(1)
|
||||
@@ -122,19 +107,14 @@ fn swap_in(app: &mut SolveApp, slot: DocSlot) -> DocSlot {
|
||||
app.menu.props = None;
|
||||
crate::sketch_attachment_ui::clear(app);
|
||||
let out_rev = app.session.revision;
|
||||
let mut out = DocSlot {
|
||||
let out = DocSlot {
|
||||
session: std::mem::replace(&mut app.session, slot.session),
|
||||
autosave: app.autosave.as_mut().and_then(|a| std::mem::replace(&mut a.active, slot.autosave)),
|
||||
big_operation: std::mem::replace(&mut app.big_operation, slot.big_operation),
|
||||
cam: std::mem::replace(&mut app.cam, slot.cam),
|
||||
pre_sketch_cam: std::mem::replace(&mut app.pre_sketch_cam, slot.pre_sketch_cam),
|
||||
pre_sketch_marker: std::mem::replace(&mut app.pre_sketch_marker, slot.pre_sketch_marker),
|
||||
view: take_view(app),
|
||||
occurrence_move: std::mem::replace(&mut app.tree.occurrence_move, slot.occurrence_move),
|
||||
};
|
||||
// Only the displayed design owns the process-wide crash replay journal.
|
||||
app.session.crash_journal = out.session.crash_journal;
|
||||
out.session.crash_journal = false;
|
||||
put_view(app, slot.view);
|
||||
// Preferences the session holds go with the user, not the tab (#680).
|
||||
app.session.auto_project = out.session.auto_project;
|
||||
@@ -274,10 +254,6 @@ pub fn close(app: &mut SolveApp, i: usize, force: bool) {
|
||||
app.docs.closing = None;
|
||||
if n <= 1 {
|
||||
// The last design: a blank one takes its place.
|
||||
if let Some(a) = app.autosave.as_mut().and_then(|a| a.active.take()) {
|
||||
a.discard();
|
||||
}
|
||||
app.big_operation = false;
|
||||
crate::sketch_text_move::cancel_drag(app);
|
||||
let rev = app.session.revision;
|
||||
crate::dialogs::cancel(app);
|
||||
@@ -297,11 +273,7 @@ pub fn close(app: &mut SolveApp, i: usize, force: bool) {
|
||||
let next = if i + 1 < n { i + 1 } else { i - 1 };
|
||||
switch(app, next);
|
||||
}
|
||||
if let Some(slot) = app.docs.slots.remove(i)
|
||||
&& let Some(a) = slot.autosave
|
||||
{
|
||||
a.discard();
|
||||
}
|
||||
app.docs.slots.remove(i);
|
||||
if app.docs.active > i {
|
||||
app.docs.active -= 1;
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ pub fn with_symbol_fallback(mut fonts: egui::FontDefinitions) -> egui::FontDefin
|
||||
|
||||
/// UI languages whose text needs the CJK font. The web app's bundled font covers Japanese only;
|
||||
/// add Chinese or Korean here together with a font that covers them.
|
||||
pub const CJK_LANGUAGES: &[&str] = &["ja", "zh", "zh-TW", "ko"];
|
||||
pub const CJK_LANGUAGES: &[&str] = &["ja", "zh", "ko"];
|
||||
|
||||
/// Whether the UI language needs a CJK font (the web app fetches one only then).
|
||||
pub fn needs_cjk_font(language: &str) -> bool {
|
||||
@@ -648,7 +648,7 @@ fn find_system_cjk_font() -> Option<Vec<u8>> {
|
||||
/// Thai comes last whatever the language: no other face draws it.
|
||||
pub fn fallback_order(language: &str) -> [&'static str; 4] {
|
||||
match language {
|
||||
"zh" | "zh-TW" => [CHINESE_FALLBACK, CJK_FALLBACK, HANGUL_FALLBACK, THAI_FALLBACK],
|
||||
"zh" => [CHINESE_FALLBACK, CJK_FALLBACK, HANGUL_FALLBACK, THAI_FALLBACK],
|
||||
"ko" => [HANGUL_FALLBACK, CJK_FALLBACK, CHINESE_FALLBACK, THAI_FALLBACK],
|
||||
_ => [CJK_FALLBACK, CHINESE_FALLBACK, HANGUL_FALLBACK, THAI_FALLBACK],
|
||||
}
|
||||
@@ -991,7 +991,6 @@ mod tests {
|
||||
#[test]
|
||||
fn a_chinese_ui_puts_the_chinese_face_before_the_japanese_one() {
|
||||
assert_eq!(fallback_order("zh"), [CHINESE_FALLBACK, CJK_FALLBACK, HANGUL_FALLBACK, THAI_FALLBACK]);
|
||||
assert_eq!(fallback_order("zh-TW"), fallback_order("zh"));
|
||||
assert_eq!(fallback_order("ko"), [HANGUL_FALLBACK, CJK_FALLBACK, CHINESE_FALLBACK, THAI_FALLBACK]);
|
||||
for lang in ["en", "ja", "de"] {
|
||||
assert_eq!(
|
||||
|
||||
@@ -425,17 +425,8 @@ pub fn open_sample(app: &mut SolveApp, i: usize) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Is this recent design there to open: in memory, or something the host can bring back?
|
||||
pub fn available(app: &SolveApp, path: &str) -> bool {
|
||||
solidcraft_engine::io::vfs::exists(path) || app.services.can_restore.as_ref().is_some_and(|can| can(path))
|
||||
}
|
||||
|
||||
pub fn open_recent(app: &mut SolveApp, path: &str) {
|
||||
if !solidcraft_engine::io::vfs::exists(path) {
|
||||
if let Some(restore) = app.services.restore_file.as_ref().filter(|_| available(app, path)) {
|
||||
restore(path);
|
||||
return;
|
||||
}
|
||||
app.set_status(format!("{path} is no longer there"), true);
|
||||
return;
|
||||
}
|
||||
@@ -538,12 +529,9 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
let Some(p) = recent.get(i) else { return };
|
||||
let path = std::path::Path::new(p);
|
||||
let name = path.file_stem().map(|x| x.to_string_lossy().to_string()).unwrap_or_else(|| p.clone());
|
||||
let missing = !available(app, p);
|
||||
let missing = !solidcraft_engine::io::vfs::exists(p);
|
||||
let mut sub = if missing {
|
||||
app.tr("home.missing").to_string()
|
||||
} else if p.starts_with("/disk/") {
|
||||
// The web build remembers files it saved on the computer.
|
||||
crate::i18n::tr_label("On your computer").to_string()
|
||||
} else if p.starts_with("/opfs/") {
|
||||
// The web build keeps designs in the browser's own storage.
|
||||
app.tr("home.in_this_browser").to_string()
|
||||
@@ -707,24 +695,6 @@ mod tests {
|
||||
assert!(sample_picture(samples.len()).is_none());
|
||||
}
|
||||
|
||||
/// A recent design the host can bring back (the browser build's remembered files) is not
|
||||
/// "missing": choosing it asks the host to restore it, and one the host doesn't know still is.
|
||||
#[test]
|
||||
fn a_recent_design_the_host_can_restore_is_asked_for_not_reported_missing() {
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
let asked = std::rc::Rc::new(std::cell::RefCell::new(Vec::<String>::new()));
|
||||
let sink = asked.clone();
|
||||
h.app.services.can_restore = Some(Box::new(|p| p.starts_with("/disk/known/")));
|
||||
h.app.services.restore_file = Some(Box::new(move |p| sink.borrow_mut().push(p.to_string())));
|
||||
assert!(available(&h.app, "/disk/known/Bracket.solidcraft"));
|
||||
assert!(!available(&h.app, "/disk/other/Bracket.solidcraft"));
|
||||
open_recent(&mut h.app, "/disk/known/Bracket.solidcraft");
|
||||
assert_eq!(*asked.borrow(), ["/disk/known/Bracket.solidcraft"]);
|
||||
open_recent(&mut h.app, "/disk/other/Bracket.solidcraft");
|
||||
assert_eq!(asked.borrow().len(), 1, "a file the host doesn't remember is not asked for");
|
||||
assert!(h.app.status.as_ref().is_some_and(|(text, _, error)| *error && text.contains("no longer there")));
|
||||
}
|
||||
|
||||
/// Rebuild the shipped pictures from the authoritative sample command scripts.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
|
||||
@@ -38,26 +38,18 @@ static GERMAN: Catalog = Catalog::new("de", "Deutsch", include_str!(concat!(env!
|
||||
static SPANISH: Catalog = Catalog::new("es", "Español (España)", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/locales/es.json")));
|
||||
static FRENCH: Catalog = Catalog::new("fr", "Français", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/locales/fr.json")));
|
||||
static GALICIAN: Catalog = Catalog::new("gl", "Galego", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/locales/gl.json")));
|
||||
static HUNGARIAN: Catalog = Catalog::new("hu", "Magyar", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/locales/hu.json")));
|
||||
static INDONESIAN: Catalog =
|
||||
Catalog::new("id", "Bahasa Indonesia", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/locales/id.json")));
|
||||
static ITALIAN: Catalog = Catalog::new("it", "Italiano", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/locales/it.json")));
|
||||
static JAPANESE: Catalog = Catalog::new("ja", "日本語", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/locales/ja.json")));
|
||||
static KOREAN: Catalog = Catalog::new("ko", "한국어", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/locales/ko.json")));
|
||||
static MALAY: Catalog = Catalog::new("ms", "Bahasa Melayu", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/locales/ms.json")));
|
||||
static DUTCH: Catalog = Catalog::new("nl", "Nederlands", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/locales/nl.json")));
|
||||
static POLISH: Catalog = Catalog::new("pl", "Polski", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/locales/pl.json")));
|
||||
static PORTUGUESE_BR: Catalog =
|
||||
Catalog::new("pt-BR", "Português (Brasil)", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/locales/pt-BR.json")));
|
||||
static ROMANIAN: Catalog = Catalog::new("ro", "Română", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/locales/ro.json")));
|
||||
static RUSSIAN: Catalog = Catalog::new("ru", "Русский", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/locales/ru.json")));
|
||||
static SWEDISH: Catalog = Catalog::new("sv", "Svenska", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/locales/sv.json")));
|
||||
static TURKISH: Catalog = Catalog::new("tr", "Türkçe", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/locales/tr.json")));
|
||||
static UKRAINIAN: Catalog = Catalog::new("uk", "Українська", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/locales/uk.json")));
|
||||
static VIETNAMESE: Catalog = Catalog::new("vi", "Tiếng Việt", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/locales/vi.json")));
|
||||
static CHINESE: Catalog = Catalog::new("zh", "中文", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/locales/zh.json")));
|
||||
static CHINESE_TRADITIONAL: Catalog =
|
||||
Catalog::new("zh-TW", "繁體中文", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/locales/zh-TW.json")));
|
||||
/// English first (the fallback), then by language id, one per line so that adding a language
|
||||
/// doesn't edit a line another addition edits.
|
||||
#[rustfmt::skip]
|
||||
@@ -69,23 +61,17 @@ pub const LOCALES: &[&Catalog] = &[
|
||||
&SPANISH,
|
||||
&FRENCH,
|
||||
&GALICIAN,
|
||||
&HUNGARIAN,
|
||||
&INDONESIAN,
|
||||
&ITALIAN,
|
||||
&JAPANESE,
|
||||
&KOREAN,
|
||||
&MALAY,
|
||||
&DUTCH,
|
||||
&POLISH,
|
||||
&PORTUGUESE_BR,
|
||||
&ROMANIAN,
|
||||
&RUSSIAN,
|
||||
&SWEDISH,
|
||||
&TURKISH,
|
||||
&UKRAINIAN,
|
||||
&VIETNAMESE,
|
||||
&CHINESE,
|
||||
&CHINESE_TRADITIONAL,
|
||||
];
|
||||
|
||||
pub fn is_supported(id: &str) -> bool {
|
||||
@@ -729,28 +715,6 @@ mod tests {
|
||||
assert_eq!(vi.dynamic("Sketch of bracket"), "Sketch of bracket");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_languages_translate_keys_labels_and_default_names() {
|
||||
for (id, name, preferences, extrude, sketch1, save) in [
|
||||
("hu", "Magyar", "Beállítások", "Kihúzás", "Vázlat1", "Menti a(z) My {design} módosításait?"),
|
||||
("id", "Bahasa Indonesia", "Preferensi", "Ekstrusi", "Sketsa1", "Simpan perubahan ke My {design}?"),
|
||||
("nl", "Nederlands", "Voorkeuren", "Extruderen", "Schets1", "Wijzigingen in My {design} opslaan?"),
|
||||
("ro", "Română", "Preferințe", "Extrudare", "Schița1", "Salvați modificările în My {design}?"),
|
||||
("sv", "Svenska", "Inställningar", "Extrudera", "Skiss1", "Spara ändringar i My {design}?"),
|
||||
("zh-TW", "繁體中文", "偏好設定", "擠出", "草圖1", "儲存對 My {design} 的變更?"),
|
||||
] {
|
||||
let locale = Locale::for_language(id);
|
||||
assert_eq!(locale.name(), name);
|
||||
assert!(is_supported(id));
|
||||
assert_eq!(locale.text("ui.preferences"), preferences);
|
||||
assert_eq!(locale.label("Extrude"), extrude);
|
||||
assert_eq!(locale.label("NonExistentKey"), "NonExistentKey");
|
||||
assert_eq!(locale.format("document.save_changes", &[("name", "My {design}")]), save);
|
||||
assert_eq!(locale.dynamic("Sketch1"), sketch1);
|
||||
assert_eq!(locale.dynamic("Sketch of bracket"), "Sketch of bracket");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_scopes_restore_the_previous_language() {
|
||||
let _english = scope("en");
|
||||
|
||||
@@ -246,7 +246,7 @@ pub use control::{ControlRequest, ControlResponse};
|
||||
pub struct UiState {
|
||||
/// Automation-only shell mode; never persisted in preferences.
|
||||
pub layout: shell::LayoutMode,
|
||||
/// Ribbon tab; SHEET METAL and the contextual SKETCH join the seven design tabs.
|
||||
/// Ribbon tab; contextual SKETCH and SHEET METAL join the seven design tabs.
|
||||
pub tab: String,
|
||||
/// Last contextual body/sketch, only used to select on entry.
|
||||
#[serde(skip)]
|
||||
@@ -332,12 +332,6 @@ pub struct Services {
|
||||
/// Hand a design's bytes to the user as a downloaded file (name, bytes); the browser build's
|
||||
/// File > Download design. None: the host saves real files, so there is nothing to offer.
|
||||
pub download_design: Option<Box<dyn Fn(&str, &[u8]) -> Result<(), String>>>,
|
||||
/// Whether a recent design that is not in memory can be brought back by the host (the browser
|
||||
/// build remembers files it saved to the disk); None: it can't.
|
||||
pub can_restore: Option<Box<dyn Fn(&str) -> bool>>,
|
||||
/// Bring such a design back and open it when it arrives (asks the user for access, so call it
|
||||
/// in the click that chose it).
|
||||
pub restore_file: Option<Box<dyn Fn(&str)>>,
|
||||
/// Show the designs the browser keeps for itself; None: the host has no such store.
|
||||
pub browser_files: Option<Box<dyn Fn()>>,
|
||||
/// HTTP for the agent chat's API agents (OpenAI-compatible endpoints); None: no API agents.
|
||||
@@ -430,7 +424,7 @@ pub struct SolveApp {
|
||||
pub status: Option<(String, f64, bool)>,
|
||||
pub quit_requested: bool,
|
||||
/// Autosave into the recovery folder (the desktop app turns it on).
|
||||
pub autosave: Option<recovery_ui::Autosaves>,
|
||||
pub autosave: Option<solidcraft_engine::recovery::Autosaver>,
|
||||
/// Designs left by crashed apps, offered for recovery.
|
||||
pub recoverable: Vec<solidcraft_engine::recovery::Entry>,
|
||||
/// The bodies of a just-opened Y-up file, offered to be stood up (`stand_up`).
|
||||
|
||||
@@ -2173,3 +2173,67 @@ fn hovering_a_large_body_does_not_wait_for_its_pick_index() {
|
||||
assert!(matches!(h.app.viewport.hover, Some(crate::viewport::Hit::Face { .. })), "{:?}", h.app.viewport.hover);
|
||||
assert!(hover_ms < index_ms / 3.0, "the first hover took {hover_ms:.1} ms; making the index takes {index_ms:.1} ms");
|
||||
}
|
||||
|
||||
/// A modelled thread is thousands of small faces but one surface to point at: the pointer over
|
||||
/// it lands on a face (not on the vertices and edges between the thread's own faces), the face
|
||||
/// lights up with the whole thread, and a click selects the thread as one item, which stands for
|
||||
/// every one of its faces when it is shown and when a command takes faces. No hover waits for the
|
||||
/// thread's faces to be worked out.
|
||||
#[test]
|
||||
fn a_modelled_thread_hovers_and_selects_as_one_surface() {
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
h.app.run("solid.cylinder", json!({"diameter": 8, "height": 16, "base": [0, 0, 0], "axis": [0, 0, 1]})).unwrap();
|
||||
h.app.run("solid.thread", json!({"face": [4, 0, 8], "designation": "M8", "length": 10, "modeled": true})).unwrap();
|
||||
let st = h.app.session.world_state();
|
||||
let b = &st.bodies[0];
|
||||
let names = solidcraft_engine::doc::naming::face_names(b);
|
||||
let thread = h.app.session.doc.features.iter().find(|f| matches!(f.kind, solidcraft_engine::doc::FeatureKind::Thread { .. })).unwrap().id;
|
||||
let made = names.iter().filter(|n| n.starts_with(&format!("F{thread}:"))).count();
|
||||
assert!(made > 500, "{made} thread faces");
|
||||
// Close in on the threaded part, side on.
|
||||
h.app.cam.target = Vec3::new(0.0, 0.0, 5.0);
|
||||
h.app.cam.distance = 30.0;
|
||||
h.frames(3);
|
||||
let mut slowest = 0.0f64;
|
||||
let mut grouped = None;
|
||||
for k in 0..40 {
|
||||
let at = h.at(&json!({"world": [4.0 * (0.3 * k as f64).cos(), 4.0 * (0.3 * k as f64).sin(), 2.0 + (k % 6) as f64]})).unwrap();
|
||||
let t = std::time::Instant::now();
|
||||
h.step(&json!({"move": at})).unwrap();
|
||||
slowest = slowest.max(t.elapsed().as_secs_f64() * 1000.0);
|
||||
if let Some(crate::viewport::Hit::Face { body, index, point }) = h.app.viewport.hover.clone() {
|
||||
let sel = Sel::Face { body, index, point };
|
||||
let pieces = crate::viewport::grouped_selection(&h.app.session, &sel);
|
||||
if pieces.len() >= made {
|
||||
grouped = Some(sel);
|
||||
break;
|
||||
}
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(20));
|
||||
}
|
||||
assert!(slowest < 250.0, "a hover took {slowest:.0} ms");
|
||||
let sel = grouped.expect("hovering the thread lands on its faces, and they are worked out as one surface");
|
||||
// Over the thread, the pointer finds the surface, not the vertices and edges inside it.
|
||||
let faces = (0..12)
|
||||
.filter(|k| {
|
||||
let a = 0.5 * *k as f64;
|
||||
let at = h.at(&json!({"world": [4.0 * a.cos(), 4.0 * a.sin(), 3.0 + (*k % 4) as f64]})).unwrap();
|
||||
h.step(&json!({"move": at})).unwrap();
|
||||
matches!(h.app.viewport.hover, Some(crate::viewport::Hit::Face { .. }))
|
||||
})
|
||||
.count();
|
||||
assert!(faces >= 9, "{faces} of 12 hovers over the thread landed on its surface");
|
||||
// A click selects it as one item, which stands for every face of the thread.
|
||||
let point = match &sel {
|
||||
Sel::Face { point, .. } => *point,
|
||||
_ => Vec3::ZERO,
|
||||
};
|
||||
let at = h.at(&json!({"world": [point.x, point.y, point.z]})).unwrap();
|
||||
let t = std::time::Instant::now();
|
||||
h.step(&json!({"click": at})).unwrap();
|
||||
let click_ms = t.elapsed().as_secs_f64() * 1000.0;
|
||||
assert_eq!(h.app.session.selection.len(), 1, "one item: {:?}", h.app.session.selection.len());
|
||||
let pieces = crate::viewport::grouped_selection(&h.app.session, &h.app.session.selection[0]);
|
||||
assert!(pieces.len() >= made, "the thread stands for {} faces of {made}", pieces.len());
|
||||
assert!(click_ms < 250.0, "the click took {click_ms:.0} ms");
|
||||
}
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
|
||||
use serde_json::json;
|
||||
use solidcraft_engine::recovery::{self, Autosaver, Entry};
|
||||
use solidcraft_engine::{Result, Session};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::SolveApp;
|
||||
use crate::ui_kit;
|
||||
@@ -12,71 +9,11 @@ use crate::ui_kit;
|
||||
/// A command that ran at least this long counts as a big operation: autosave right after it.
|
||||
pub const BIG_OPERATION_MS: f64 = 1500.0;
|
||||
|
||||
/// A tab's autosaver has no clock of its own: [`tick`] tells it when its turn has come.
|
||||
const SHARED_CADENCE: Duration = Duration::MAX;
|
||||
|
||||
/// Shared settings and cadence; each tab owns its own entry and change tracking.
|
||||
pub struct Autosaves {
|
||||
pub dir: PathBuf,
|
||||
pub interval: Duration,
|
||||
pub(crate) active: Option<Autosaver>,
|
||||
last_at: Instant,
|
||||
}
|
||||
|
||||
impl Autosaves {
|
||||
/// Save the active design immediately, including when taking over a recovered entry.
|
||||
pub fn save(&mut self, session: &Session) -> Result<bool> {
|
||||
save_slot(&self.dir, &mut self.active, session, true, true)
|
||||
}
|
||||
}
|
||||
|
||||
/// Bring one tab's autosave up to date. `due`: its turn has come. `wait`: write on this thread
|
||||
/// and wait for it (Save, close, shutdown); otherwise the write runs on a worker thread, so a
|
||||
/// scheduled autosave of several large designs never holds up the window (#3044).
|
||||
fn save_slot(dir: &Path, slot: &mut Option<Autosaver>, session: &Session, due: bool, wait: bool) -> Result<bool> {
|
||||
// Clean tabs need no serialization or entry, even between scheduled saves.
|
||||
if !session.is_dirty() {
|
||||
if let Some(mut a) = slot.take() {
|
||||
// Rebase the active crash journal before deleting the autosave it may refer to.
|
||||
a.save(session)?;
|
||||
a.discard();
|
||||
}
|
||||
return Ok(false);
|
||||
}
|
||||
if !due {
|
||||
return Ok(false);
|
||||
}
|
||||
if slot.is_none() {
|
||||
*slot = Some(Autosaver::new(dir, SHARED_CADENCE)?);
|
||||
}
|
||||
match slot.as_mut() {
|
||||
Some(a) if wait => a.save(session),
|
||||
// Skips a tab whose previous write is still running.
|
||||
Some(a) => a.tick(session, true),
|
||||
None => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
/// Autosave every open design on the shared cadence, or a tab after its big operation.
|
||||
/// Autosave (when due, or after a big operation).
|
||||
pub fn tick(app: &mut SolveApp) {
|
||||
let big = std::mem::take(&mut app.big_operation);
|
||||
let Some(a) = app.autosave.as_mut() else { return };
|
||||
let due = a.last_at.elapsed() >= a.interval;
|
||||
if due {
|
||||
a.last_at = Instant::now();
|
||||
}
|
||||
let mut error = None;
|
||||
for slot in app.docs.background_mut() {
|
||||
let big = std::mem::take(&mut slot.big_operation);
|
||||
if let Err(e) = save_slot(&a.dir, &mut slot.autosave, &slot.session, due || big, false) {
|
||||
error.get_or_insert(e);
|
||||
}
|
||||
}
|
||||
// Background writes must not replace the active design's crash-journal checkpoint.
|
||||
if let Err(e) = save_slot(&a.dir, &mut a.active, &app.session, due || big, false) {
|
||||
error.get_or_insert(e);
|
||||
}
|
||||
if let Some(e) = error {
|
||||
if let Err(e) = a.tick(&app.session, big) {
|
||||
let msg = format!("autosave failed: {e}");
|
||||
if app.status.as_ref().is_none_or(|(s, _, _)| *s != msg) {
|
||||
app.set_status(msg, true);
|
||||
@@ -86,10 +23,10 @@ pub fn tick(app: &mut SolveApp) {
|
||||
|
||||
/// Start autosaving into `dir` and queue the designs that crashed apps left there.
|
||||
pub fn start(app: &mut SolveApp, dir: &std::path::Path, interval: std::time::Duration) {
|
||||
match Autosaver::new(dir, SHARED_CADENCE) {
|
||||
match Autosaver::new(dir, interval) {
|
||||
Ok(a) => {
|
||||
app.recoverable = recovery::orphans(dir);
|
||||
app.autosave = Some(Autosaves { dir: dir.to_path_buf(), interval, active: Some(a), last_at: Instant::now() });
|
||||
app.autosave = Some(a);
|
||||
}
|
||||
Err(e) => app.set_status(format!("autosave is off: {e}"), true),
|
||||
}
|
||||
@@ -97,17 +34,8 @@ pub fn start(app: &mut SolveApp, dir: &std::path::Path, interval: std::time::Dur
|
||||
|
||||
/// The app is closing: keep unsaved work for the next launch, drop everything else.
|
||||
pub fn close(app: &mut SolveApp) {
|
||||
if let Some(mut a) = app.autosave.take() {
|
||||
for slot in app.docs.background_mut() {
|
||||
let _ = save_slot(&a.dir, &mut slot.autosave, &slot.session, true, true);
|
||||
if let Some(saver) = slot.autosave.take() {
|
||||
saver.close(&slot.session);
|
||||
}
|
||||
}
|
||||
let _ = a.save(&app.session);
|
||||
if let Some(saver) = a.active.take() {
|
||||
saver.close(&app.session);
|
||||
}
|
||||
if let Some(a) = app.autosave.take() {
|
||||
a.close(&app.session);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,277 +105,8 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{Services, documents};
|
||||
use std::fs;
|
||||
|
||||
fn app(tag: &str, interval: Duration) -> (SolveApp, PathBuf) {
|
||||
let dir = std::env::temp_dir().join(format!("solidcraft-recovery-tabs-{tag}-{}", std::process::id()));
|
||||
let _ = fs::remove_dir_all(&dir);
|
||||
let mut app = SolveApp::new(Session::default(), Services::default());
|
||||
start(&mut app, &dir, interval);
|
||||
(app, dir)
|
||||
}
|
||||
|
||||
fn edit(app: &mut SolveApp) {
|
||||
app.run("solid.box", json!({"length": 10, "width": 10, "height": 10})).unwrap();
|
||||
// Tests control the cadence without relying on how long the command took.
|
||||
app.big_operation = false;
|
||||
}
|
||||
|
||||
fn entries(dir: &Path) -> Vec<Entry> {
|
||||
let mut entries: Vec<Entry> = fs::read_dir(dir)
|
||||
.unwrap()
|
||||
.map(|p| p.unwrap().path())
|
||||
.filter(|p| p.extension().is_some_and(|e| e == "json"))
|
||||
.map(|p| serde_json::from_slice(&fs::read(p).unwrap()).unwrap())
|
||||
.collect();
|
||||
entries.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
entries
|
||||
}
|
||||
|
||||
/// A tick, then wait for the writes it started on worker threads. Waiting writes nothing
|
||||
/// new: every design it would write was just written.
|
||||
fn tick_and_wait(app: &mut SolveApp) {
|
||||
tick(app);
|
||||
let Some(a) = app.autosave.as_mut() else { return };
|
||||
for slot in app.docs.background_mut() {
|
||||
if let Some(saver) = slot.autosave.as_mut() {
|
||||
let _ = saver.save(&slot.session);
|
||||
}
|
||||
}
|
||||
if let Some(saver) = a.active.as_mut() {
|
||||
let _ = saver.save(&app.session);
|
||||
}
|
||||
}
|
||||
|
||||
fn name(app: &mut SolveApp, name: &str) {
|
||||
app.run("file.new", json!({"name": name})).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dirty_background_tab_is_saved_and_offered_after_a_crash() {
|
||||
let (mut app, dir) = app("background", Duration::ZERO);
|
||||
name(&mut app, "Background A");
|
||||
edit(&mut app);
|
||||
let expected = app.session.doc.clone();
|
||||
documents::new_tab(&mut app);
|
||||
assert!(!app.session.is_dirty());
|
||||
tick_and_wait(&mut app);
|
||||
assert_eq!(entries(&dir).len(), 1);
|
||||
assert!(recovery::orphans(&dir).is_empty(), "all live entries stay locked");
|
||||
drop(app); // Simulate a crash: release locks without a final save.
|
||||
let restarted = restart(&dir);
|
||||
assert_eq!(restarted.recoverable.len(), 1);
|
||||
let entry = &restarted.recoverable[0];
|
||||
assert_eq!(entry.name, "Background A");
|
||||
assert_eq!(recovery::load(&dir, &entry.id).unwrap().0, *expected);
|
||||
drop(restarted);
|
||||
fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
fn restart(dir: &Path) -> SolveApp {
|
||||
let mut app = SolveApp::new(Session::default(), Services::default());
|
||||
start(&mut app, dir, Duration::ZERO);
|
||||
app
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn switching_to_a_clean_tab_keeps_the_background_entry_and_its_id() {
|
||||
let (mut app, dir) = app("clean-tab", Duration::ZERO);
|
||||
edit(&mut app);
|
||||
tick_and_wait(&mut app);
|
||||
let before = entries(&dir);
|
||||
documents::new_tab(&mut app);
|
||||
tick_and_wait(&mut app);
|
||||
assert_eq!(entries(&dir), before, "the clean tab cannot delete another tab's entry");
|
||||
documents::switch(&mut app, 0);
|
||||
tick_and_wait(&mut app);
|
||||
assert_eq!(entries(&dir), before, "switching back preserves identity and skips unchanged content");
|
||||
drop(app);
|
||||
assert_eq!(recovery::orphans(&dir), before);
|
||||
fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn several_dirty_tabs_keep_distinct_entries_and_recover_their_own_designs() {
|
||||
let (mut app, dir) = app("several", Duration::ZERO);
|
||||
let mut expected = Vec::new();
|
||||
for i in 0..3 {
|
||||
if i > 0 {
|
||||
documents::new_tab(&mut app);
|
||||
}
|
||||
name(&mut app, &format!("Design {i}"));
|
||||
for _ in 0..=i {
|
||||
edit(&mut app);
|
||||
}
|
||||
expected.push(app.session.doc.clone());
|
||||
}
|
||||
tick_and_wait(&mut app);
|
||||
let saved = entries(&dir);
|
||||
assert_eq!(saved.len(), 3);
|
||||
let ids: std::collections::HashSet<_> = saved.iter().map(|e| &e.id).collect();
|
||||
assert_eq!(ids.len(), 3);
|
||||
for i in [0, 2, 1, 0] {
|
||||
documents::switch(&mut app, i);
|
||||
tick_and_wait(&mut app);
|
||||
assert_eq!(entries(&dir), saved);
|
||||
}
|
||||
drop(app);
|
||||
let mut restarted = restart(&dir);
|
||||
assert_eq!(restarted.recoverable.len(), 3);
|
||||
for (entry, doc) in saved.iter().zip(expected) {
|
||||
assert_eq!(recovery::load(&dir, &entry.id).unwrap().0, *doc);
|
||||
restarted.run("doc.recover", json!({"dir": dir, "id": entry.id})).unwrap();
|
||||
assert_eq!(*restarted.session.doc, *doc);
|
||||
assert!(restarted.session.is_dirty());
|
||||
}
|
||||
drop(restarted);
|
||||
fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn saving_and_closing_remove_only_the_affected_tabs_entry() {
|
||||
let (mut app, dir) = app("cleanup", Duration::ZERO);
|
||||
for i in 0..3 {
|
||||
if i > 0 {
|
||||
documents::new_tab(&mut app);
|
||||
}
|
||||
name(&mut app, &format!("Design {i}"));
|
||||
edit(&mut app);
|
||||
}
|
||||
tick_and_wait(&mut app);
|
||||
let saved = entries(&dir);
|
||||
// Cleanup is immediate, even when the next autosave is far away.
|
||||
app.autosave.as_mut().unwrap().interval = Duration::from_secs(3600);
|
||||
app.run("file.save_as", json!({"path": dir.join("saved.solidcraft")})).unwrap();
|
||||
tick_and_wait(&mut app);
|
||||
assert_eq!(entries(&dir), saved[..2]);
|
||||
documents::close(&mut app, 0, false);
|
||||
documents::answer_prompt(&mut app, documents::Answer::Cancel);
|
||||
assert_eq!(entries(&dir), saved[..2], "cancelling close keeps recovery");
|
||||
documents::close(&mut app, 0, true); // Discard a background tab.
|
||||
assert_eq!(entries(&dir), saved[1..2]);
|
||||
documents::switch(&mut app, 0);
|
||||
documents::close(&mut app, 0, true); // Discard the active tab.
|
||||
assert!(entries(&dir).is_empty());
|
||||
edit(&mut app);
|
||||
app.autosave.as_mut().unwrap().save(&app.session).unwrap();
|
||||
assert_eq!(entries(&dir).len(), 1);
|
||||
documents::close(&mut app, 0, true); // Last tab becomes a fresh blank design.
|
||||
assert!(entries(&dir).is_empty());
|
||||
drop(app);
|
||||
assert!(recovery::orphans(&dir).is_empty());
|
||||
fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shutdown_keeps_all_unsaved_tabs_but_confirmed_quit_discards_them() {
|
||||
for discard in [false, true] {
|
||||
let (mut app, dir) = app(if discard { "quit" } else { "shutdown" }, Duration::from_secs(3600));
|
||||
edit(&mut app);
|
||||
documents::new_tab(&mut app);
|
||||
edit(&mut app);
|
||||
if discard {
|
||||
// Make entries first, then exercise Don't Save for every design.
|
||||
app.autosave.as_mut().unwrap().interval = Duration::ZERO;
|
||||
tick_and_wait(&mut app);
|
||||
assert_eq!(entries(&dir).len(), 2);
|
||||
documents::request_quit(&mut app);
|
||||
documents::answer_prompt(&mut app, documents::Answer::DontSave);
|
||||
documents::answer_prompt(&mut app, documents::Answer::DontSave);
|
||||
assert!(app.quit_requested);
|
||||
}
|
||||
close(&mut app);
|
||||
assert_eq!(recovery::orphans(&dir).len(), if discard { 0 } else { 2 });
|
||||
assert!(app.docs.background_mut().all(|s| s.autosave.is_none()), "shutdown releases every lock");
|
||||
fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cadence_and_big_operations_follow_the_document_and_preferences() {
|
||||
let (mut app, dir) = app("cadence", Duration::from_secs(3600));
|
||||
app.session.crash_journal = true;
|
||||
edit(&mut app);
|
||||
app.big_operation = true;
|
||||
documents::new_tab(&mut app);
|
||||
assert!(app.session.crash_journal);
|
||||
assert!(app.docs.background_mut().all(|s| !s.session.crash_journal));
|
||||
assert!(!app.big_operation);
|
||||
edit(&mut app);
|
||||
tick_and_wait(&mut app);
|
||||
assert_eq!(entries(&dir).len(), 1, "only the background big operation is due");
|
||||
assert!(app.docs.background_mut().all(|s| !s.big_operation));
|
||||
tick_and_wait(&mut app);
|
||||
assert_eq!(entries(&dir).len(), 1, "the ordinary active edit waits for the interval");
|
||||
app.autosave_minutes = 0.00001;
|
||||
crate::prefs::set_autosave(&mut app);
|
||||
app.autosave.as_mut().unwrap().last_at = Instant::now() - Duration::from_secs(1);
|
||||
tick_and_wait(&mut app);
|
||||
assert_eq!(entries(&dir).len(), 2, "changed preferences apply to every tab");
|
||||
drop(app);
|
||||
fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_failed_autosave_does_not_skip_the_other_tabs() {
|
||||
let (mut app, dir) = app("failure", Duration::ZERO);
|
||||
name(&mut app, "A");
|
||||
edit(&mut app);
|
||||
tick_and_wait(&mut app);
|
||||
let id = entries(&dir)[0].id.clone();
|
||||
let blocked = dir.join(format!("{id}.solidcraft"));
|
||||
fs::remove_file(&blocked).unwrap();
|
||||
fs::create_dir(&blocked).unwrap();
|
||||
edit(&mut app);
|
||||
documents::new_tab(&mut app);
|
||||
name(&mut app, "B");
|
||||
edit(&mut app);
|
||||
// The writes run on worker threads: a failure is reported by a later tick.
|
||||
let failed = |app: &SolveApp| app.status.as_ref().is_some_and(|(message, _, error)| *error && message.contains("autosave failed"));
|
||||
let deadline = Instant::now() + Duration::from_secs(60);
|
||||
tick(&mut app);
|
||||
while !failed(&app) && Instant::now() < deadline {
|
||||
std::thread::sleep(Duration::from_millis(5));
|
||||
tick(&mut app);
|
||||
}
|
||||
assert!(failed(&app));
|
||||
tick_and_wait(&mut app);
|
||||
let saved = entries(&dir);
|
||||
assert_eq!(saved.len(), 2);
|
||||
assert_eq!(recovery::load(&dir, &saved[1].id).unwrap().0, *app.session.doc);
|
||||
// Retrying at the next tick also recovers the previously failing tab.
|
||||
fs::remove_dir(&blocked).unwrap();
|
||||
tick_and_wait(&mut app);
|
||||
assert_eq!(recovery::load(&dir, &id).unwrap().0.features.len(), 2);
|
||||
drop(app);
|
||||
fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_tab_closed_while_its_autosave_is_written_leaves_nothing_to_recover() {
|
||||
let (mut app, dir) = app("discard-race", Duration::ZERO);
|
||||
name(&mut app, "Closed");
|
||||
// Enough of a design that its write is still running when the tab is closed.
|
||||
for i in 0..40 {
|
||||
app.run("solid.cylinder", json!({"radius": 3, "height": 5, "base": [i * 8, 0, 0], "operation": "new"})).unwrap();
|
||||
}
|
||||
app.big_operation = false;
|
||||
documents::new_tab(&mut app);
|
||||
tick(&mut app); // Starts writing the background tab on a worker thread.
|
||||
documents::close(&mut app, 0, true); // Don't Save, right away.
|
||||
let files = || fs::read_dir(&dir).unwrap().filter_map(|e| e.ok()).map(|e| e.file_name()).collect::<Vec<_>>();
|
||||
// A write that outlived the close would recreate the entry; give it time to.
|
||||
let deadline = Instant::now() + Duration::from_secs(2);
|
||||
while Instant::now() < deadline {
|
||||
assert!(entries(&dir).is_empty(), "the closed design came back: {:?}", files());
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
}
|
||||
drop(app);
|
||||
assert!(recovery::orphans(&dir).is_empty());
|
||||
fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
use serde_json::json;
|
||||
use solidcraft_engine::recovery::Entry;
|
||||
|
||||
fn entry(id: &str, name: &str) -> Entry {
|
||||
Entry { id: id.into(), path: None, name: name.into(), saved_at: 0, features: 3 }
|
||||
|
||||
@@ -38,8 +38,6 @@ pub const POINTS: Accept = 512;
|
||||
pub const FEATURES: Accept = 1024;
|
||||
/// Points of the sketch being modified; unlike POINTS these never enable cross-sketch picking.
|
||||
pub const SKETCH_POINTS: Accept = 2048;
|
||||
/// The origin point, picked as a vertex of no body (in the viewport or the browser's Origin).
|
||||
pub const ORIGIN: Accept = 4096;
|
||||
|
||||
/// One selection input of a command dialog ("Profiles", "Edges", "Plane", ...).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -81,8 +79,7 @@ impl SelInput {
|
||||
Hit::SketchCurve { id, .. } if a & CURVES != 0 => Some(Sel::SketchCurve { id: id.clone() }),
|
||||
Hit::SketchPoint { id, .. } if a & SKETCH_POINTS != 0 => Some(Sel::SketchPoint { id: id.clone() }),
|
||||
Hit::SketchPoint { sketch, id, .. } if a & POINTS != 0 => Some(Sel::SketchPoint { id: format!("{sketch}:{id}") }),
|
||||
Hit::Vertex { body, point } if body.is_empty() && a & ORIGIN != 0 => Some(Sel::Vertex { body: String::new(), point: *point }),
|
||||
Hit::Vertex { body, point } if !body.is_empty() && a & VERTICES != 0 => Some(Sel::Vertex { body: body.clone(), point: *point }),
|
||||
Hit::Vertex { body, point } if a & VERTICES != 0 => Some(Sel::Vertex { body: body.clone(), point: *point }),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,24 +105,6 @@ pub fn score(q: &str, s: &str) -> u32 {
|
||||
300u32.saturating_sub(gaps * 20).max(10)
|
||||
}
|
||||
|
||||
/// Words people look for a command by that its name doesn't hold (file formats, what it is for).
|
||||
fn keywords(id: &str) -> &'static str {
|
||||
match id {
|
||||
"file.save_mesh" => "export stl 3mf mesh 3d print printing slicer",
|
||||
"file.export" => "export stl 3mf obj step urdf mesh 3d print",
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
|
||||
/// How well `q` matches a command's `keywords`: every word of the query starts a keyword.
|
||||
fn keyword_score(q: &str, keywords: &str) -> u32 {
|
||||
if keywords.is_empty() || q.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
let words = || keywords.split_whitespace();
|
||||
if q.split_whitespace().all(|w| words().any(|k| k.starts_with(w))) { 700 } else { 0 }
|
||||
}
|
||||
|
||||
/// A row of the box: the command id, its label, its icon, and where it lives on the toolbar
|
||||
/// (tab, panel; `None` off the toolbar), in the current language.
|
||||
pub type Row = (&'static str, &'static str, &'static str, Option<(&'static str, &'static str)>);
|
||||
@@ -136,7 +118,6 @@ pub fn search(app: &SolveApp, q: &str) -> Vec<Row> {
|
||||
let q = q.trim().to_lowercase();
|
||||
let mut v: Vec<(u32, Row)> = command_specs()
|
||||
.into_iter()
|
||||
.filter(|c| !crate::workspace::AGENT_ONLY.contains(&c.id))
|
||||
.filter_map(|c| {
|
||||
let (home_tab, home_panel) = crate::workspace::destination(c).unwrap_or(("", ""));
|
||||
let (label, tab, panel) = (crate::i18n::tr_label(c.label), crate::i18n::tr_label(home_tab), crate::i18n::tr_label(home_panel));
|
||||
@@ -144,7 +125,6 @@ pub fn search(app: &SolveApp, q: &str) -> Vec<Row> {
|
||||
let s = score(&q, label)
|
||||
.max(score(&q, c.label))
|
||||
.max(score(&q, c.id) * 9 / 10)
|
||||
.max(keyword_score(&q, keywords(c.id)))
|
||||
.max(score(&q, tab).max(score(&q, home_tab)) / 3)
|
||||
.max(score(&q, panel).max(score(&q, home_panel)) / 3);
|
||||
let recent = app.sbox.recent.iter().position(|r| r == c.id).map_or(0, |i| 2000u32.saturating_sub(i.min(2000) as u32));
|
||||
@@ -659,15 +639,6 @@ mod tests {
|
||||
assert_eq!((other.sbox.pinned.clone(), other.sbox.recent.len()), (vec!["solid.extrude".to_string()], 2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn solve_sketch_is_not_offered_but_the_command_remains() {
|
||||
let app = SolveApp::new(solidcraft_engine::Session::default(), Default::default());
|
||||
for q in ["solve", "sketch.solve", ""] {
|
||||
assert!(!search(&app, q).iter().any(|x| x.0 == "sketch.solve"), "{q:?}");
|
||||
}
|
||||
assert!(solidcraft_engine::find_command("sketch.solve").is_some(), "agents and MCP keep it");
|
||||
}
|
||||
|
||||
/// The box shows each command's label and place in the current language, and finds a name
|
||||
/// typed in that language, in English, or an id (#2544).
|
||||
#[test]
|
||||
|
||||
@@ -649,33 +649,3 @@ fn trim_hover_shows_the_piece_a_click_takes_away() {
|
||||
h.step(&json!({"move":{"world":[15,0,0]}})).unwrap();
|
||||
assert!(trim_hover_piece(&h.app, sid, &line).is_none(), "only Trim shows a piece");
|
||||
}
|
||||
|
||||
/// #3141: the 2-tangent circle tool, clicked on two sides of a rectangle drawn away from the
|
||||
/// origin, puts its circle in the corner between the clicks without moving the rectangle, and
|
||||
/// fixed sides no longer make it fail.
|
||||
#[test]
|
||||
fn two_tangent_circle_clicked_on_two_sides_fits_the_corner_and_moves_nothing() {
|
||||
for fixed in [false, true] {
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
h.app.run("sketch.create", json!({"plane":"XY"})).unwrap();
|
||||
let r = h.app.run("sketch.rectangle.two_point", json!({"p0":[-800,0],"p1":[-300,500]})).unwrap();
|
||||
if fixed {
|
||||
h.app.run("sketch.constraint.fix", json!({"entities": r["curves"]})).unwrap();
|
||||
}
|
||||
h.step(&json!({"call":"ui.view","params":{"view":"fit"}})).unwrap();
|
||||
h.settle();
|
||||
let sid = h.app.session.active_sketch.unwrap();
|
||||
let points =
|
||||
|h: &crate::scenario::Harness| h.app.session.model.state().sketch(sid).unwrap().sketch.points.iter().map(|p| p.pos).collect::<Vec<_>>();
|
||||
let before = points(&h);
|
||||
h.step(&json!({"start":"sketch.circle.two_tangent"})).unwrap();
|
||||
h.step(&json!({"click":{"world":[-550,0,0]}})).unwrap();
|
||||
h.step(&json!({"click":{"world":[-800,250,0]}})).unwrap();
|
||||
let sk = h.app.session.model.state().sketch(sid).unwrap().sketch.clone();
|
||||
let circle = sk.curves.iter().position(|c| matches!(c.kind, CurveKind::Circle { .. }));
|
||||
let circle = circle.unwrap_or_else(|| panic!("fixed {fixed}: no circle: {:?}", h.app.status));
|
||||
assert!(sk.center(circle).unwrap().dist(Vec2::new(-750.0, 50.0)) < 1e-6, "fixed {fixed}: {:?}", sk.center(circle));
|
||||
let after = points(&h);
|
||||
assert!(before.iter().zip(&after).all(|(a, b)| a.dist(*b) < 1e-9), "fixed {fixed}: the rectangle moved: {after:?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! buttons. Options: the line type of the selected curves (normal, construction, centerline),
|
||||
//! Look At, and the display toggles kept with the sketch (grid, snap, slice, profiles, points,
|
||||
//! dimensions, constraints, projected geometry, construction geometry, 3D sketch); the sketch's
|
||||
//! degrees of freedom. Finish Sketch sits at the bottom, beside the ribbon's Exit group. Toggles run `sketch.options`, line types
|
||||
//! degrees of freedom. Finish Sketch lives in the ribbon Exit group. Toggles run `sketch.options`, line types
|
||||
//! `sketch.construction` / `sketch.centerline`.
|
||||
//!
|
||||
//! The line type is also a drawing mode: with nothing selected, Construction (or X) makes what
|
||||
@@ -343,7 +343,6 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
let mut linetype = None;
|
||||
let mut construction = false;
|
||||
let mut look = false;
|
||||
let mut finish = false;
|
||||
let mut tie = false;
|
||||
let mut start = None;
|
||||
let feature = active_feature(app);
|
||||
@@ -461,10 +460,6 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
section_alias("sketch_palette:options_fold", "ui.palette_more");
|
||||
}
|
||||
crate::ui_kit::Part::Footer => {
|
||||
let button = crate::ui_kit::wide_primary_button(ui, "Finish Sketch");
|
||||
crate::scenario::publish_rect("sketch_palette:finish", button.rect);
|
||||
crate::scenario::publish_handle("sketch_palette:finish", button.rect.center());
|
||||
finish = button.clicked();
|
||||
let (at, slice) = crate::ui_kit::button_pair(
|
||||
ui,
|
||||
"sketch_palette",
|
||||
@@ -485,6 +480,13 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
if let Some(rect) = app.viewport.palette_rect {
|
||||
crate::scenario::publish_rect("sketch_palette:outer", rect);
|
||||
}
|
||||
// Compatibility automation hook now points at the single Finish action in the ribbon.
|
||||
if let Some(at) = crate::scenario::handle_at("button:sketch.finish") {
|
||||
crate::scenario::publish_handle("sketch_palette:finish", at);
|
||||
if let Some(rect) = crate::scenario::rect_of("button:sketch.finish") {
|
||||
crate::scenario::publish_rect("sketch_palette:finish", rect);
|
||||
}
|
||||
}
|
||||
if let Some((key, on)) = set {
|
||||
let mut params = serde_json::Map::new();
|
||||
params.insert(key, Value::Bool(on));
|
||||
@@ -511,9 +513,6 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
if look {
|
||||
crate::dialogs::look_at_sketch_again(app);
|
||||
}
|
||||
if finish {
|
||||
app.finish_sketch();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -591,18 +590,6 @@ mod tests {
|
||||
assert!(h.app.session.active_sketch.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn palette_has_a_finish_sketch_button_that_finishes_the_sketch() {
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
h.app.run("sketch.create", json!({"plane":"XY"})).unwrap();
|
||||
h.settle();
|
||||
let at = crate::scenario::handle_at("sketch_palette:finish").expect("the palette ends in Finish Sketch");
|
||||
assert!(crate::scenario::rect_of("sketch_palette:look_at").is_some(), "Look At | Slice stay");
|
||||
assert!(crate::scenario::handle_at("button:sketch.finish").is_some(), "and so does the ribbon's Finish");
|
||||
h.call("ui.click", json!({"x":at.x,"y":at.y}));
|
||||
assert!(h.app.session.active_sketch.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn construction_shortcut_keeps_the_drawing_tool_and_commits_its_line_type() {
|
||||
for command in ["sketch.line", "sketch.rectangle.two_point", "sketch.circle.center"] {
|
||||
|
||||
@@ -595,7 +595,7 @@ pub fn hint(id: &str) -> Option<String> {
|
||||
"sketch.edit_text" => "Edit Text: click a letter interior or outline",
|
||||
"sketch.blend_curve" => "Blend curve: click the ends of two curves",
|
||||
"sketch.constraint.curvature" => "Curvature: click two curves that share an end",
|
||||
"sketch.circle.two_tangent" => "2-tangent circle: click two curves (the circle goes between the two clicks)",
|
||||
"sketch.circle.two_tangent" => "2-tangent circle: click two curves (the circle goes near the second click)",
|
||||
"sketch.circle.three_tangent" => "3-tangent circle: click three curves",
|
||||
"sketch.constrainer" => "Constrain: click one or two entities; right-click to apply",
|
||||
"sketch.constraint.polygon" => "Polygon: click the lines of a closed chain; right-click to apply",
|
||||
@@ -1007,9 +1007,7 @@ fn run_entities(app: &mut SolveApp, t: &Tool) {
|
||||
"sketch.constraint.curvature" => json!({"a": k[0], "b": k[1]}),
|
||||
"sketch.circle.two_tangent" => {
|
||||
let r = round_nice(curve_len(app, &k[0]).min(curve_len(app, &k[1])) * 0.1);
|
||||
// Between the two clicks: each lies on its curve, so the circle in the corner they
|
||||
// span is the one nearest their midpoint (#3141).
|
||||
let near = xy(t.pts.iter().fold(Vec2::ZERO, |a, p| a + p.0) / t.pts.len().max(1) as f64);
|
||||
let near = t.pts.last().map(|p| xy(p.0)).unwrap_or(json!([0, 0]));
|
||||
json!({"curves": k, "radius": r, "near": near})
|
||||
}
|
||||
"sketch.circle.three_tangent" => {
|
||||
|
||||
@@ -503,7 +503,7 @@ fn file_menu(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
}
|
||||
crate::ui_kit::Menu::button(ui, app.tr("ui.open_recent"), |ui| {
|
||||
for path in app.home.recent.clone() {
|
||||
let missing = !crate::home::available(app, &path);
|
||||
let missing = !solidcraft_engine::io::vfs::exists(&path);
|
||||
let label = format!("{path}{}", if missing { " (missing)" } else { "" });
|
||||
if crate::context_menu::button(ui, &label, "open").clicked() {
|
||||
crate::home::open_recent(app, &path);
|
||||
@@ -555,12 +555,8 @@ fn file_menu(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
close = true;
|
||||
}
|
||||
ui.separator();
|
||||
if item_if(ui, "Export STL…", "", "mesh_export", shown) {
|
||||
crate::dialogs_save_mesh::start_with_format(app, "stl");
|
||||
close = true;
|
||||
}
|
||||
if item_if(ui, "Export 3MF…", "", "mesh_export", shown) {
|
||||
crate::dialogs_save_mesh::start_with_format(app, "3mf");
|
||||
if item_if(ui, "Save as mesh…", "", "mesh_export", shown) {
|
||||
app.start("file.save_mesh");
|
||||
close = true;
|
||||
}
|
||||
if item_if(ui, "Export STEP…", "", "export", shown) {
|
||||
@@ -775,12 +771,6 @@ fn ribbon_text(ui: &egui::Ui, at: egui::Pos2, text: &str, size: f32, color: Colo
|
||||
ui.painter().galley(origin, galley, color);
|
||||
}
|
||||
|
||||
/// SKETCH while editing a sketch and SHEET METAL while a sheet body is active take the contextual
|
||||
/// tab style; SHEET METAL is an ordinary tab otherwise.
|
||||
fn contextual_tab(app: &SolveApp, tab: &str) -> bool {
|
||||
tab == "SKETCH" || (tab == "SHEET METAL" && crate::workspace::sheet_body(app).is_some())
|
||||
}
|
||||
|
||||
fn ribbon_tabs(app: &mut SolveApp, ui: &mut egui::Ui, row: Rect) {
|
||||
let t = Tokens::get();
|
||||
let tabs = crate::workspace::visible_tabs(app);
|
||||
@@ -793,7 +783,7 @@ fn ribbon_tabs(app: &mut SolveApp, ui: &mut egui::Ui, row: Rect) {
|
||||
.collect();
|
||||
let tabs_width: f32 = widths.iter().sum::<f32>()
|
||||
+ tabs.len().saturating_sub(1) as f32 * 2.0
|
||||
+ tabs.iter().filter(|tab| contextual_tab(app, tab)).count() as f32 * 6.0;
|
||||
+ tabs.iter().filter(|tab| matches!(**tab, "SKETCH" | "SHEET METAL")).count() as f32 * 6.0;
|
||||
let search_width = crate::ui_kit::CommandSearch::closed_width(ui.ctx());
|
||||
// The first tab and the search field sit the same distance from the card's top, left and right
|
||||
// edges, so their outer corners are concentric with the card's corners.
|
||||
@@ -823,7 +813,7 @@ fn ribbon_tabs(app: &mut SolveApp, ui: &mut egui::Ui, row: Rect) {
|
||||
let (all, _) = ui.allocate_exact_size(vec2(tabs_width, TAB_ROW_HEIGHT), Sense::hover());
|
||||
let mut x = all.left();
|
||||
for (tab, width) in tabs.into_iter().zip(widths) {
|
||||
let contextual = contextual_tab(app, tab);
|
||||
let contextual = matches!(tab, "SKETCH" | "SHEET METAL");
|
||||
if contextual {
|
||||
x += 6.0;
|
||||
}
|
||||
@@ -1903,27 +1893,6 @@ mod header_report_tests {
|
||||
assert!(!solidcraft_engine::io::is_design_container(&std::fs::read(saved).unwrap()), "subsequent Save keeps a .json choice");
|
||||
}
|
||||
|
||||
/// File › Export STL… and Export 3MF… both open the Save as mesh dialog with their format
|
||||
/// chosen (whatever was used last), and command search finds the export by format or purpose.
|
||||
#[test]
|
||||
fn file_menu_has_export_stl_and_3mf_that_open_the_mesh_dialog_with_that_format() {
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
h.app.run("solid.box", json!({"length":2,"width":3,"height":4})).unwrap();
|
||||
h.app.preferences.save_mesh_format = "3mf".into();
|
||||
for (item, format) in [("Export STL…", "stl"), ("Export 3MF…", "3mf")] {
|
||||
h.frames(3);
|
||||
h.step(&json!({"click":{"handle":"appbar:file"}})).unwrap();
|
||||
h.step(&json!({"widget": item})).unwrap();
|
||||
let Some(crate::dialogs::Kind::SaveMesh(k)) = h.app.dialog.as_ref().map(|d| &d.kind) else { panic!("{item}: no Save as mesh dialog") };
|
||||
assert_eq!(k.format_name(), format, "{item}");
|
||||
crate::dialogs::cancel(&mut h.app);
|
||||
}
|
||||
for q in ["3mf", "stl", "export 3mf", "print"] {
|
||||
let found = crate::shortcut_box::search(&h.app, q);
|
||||
assert!(found.iter().any(|r| r.0 == "file.save_mesh"), "{q}: {:?}", found.iter().map(|r| r.0).collect::<Vec<_>>());
|
||||
}
|
||||
}
|
||||
|
||||
/// The browser build's File > Download design hands the open design to the host as a
|
||||
/// `.solidcraft` container named after it; a host that saves real files shows no such item.
|
||||
#[test]
|
||||
|
||||
@@ -279,17 +279,6 @@ pub(crate) fn primary_button(ui: &mut Ui, label: &str) -> Response {
|
||||
response
|
||||
}
|
||||
|
||||
/// Accent action across the full width of its container, for the bottom of a panel. Tokens:
|
||||
/// `form_primary, on_accent, form_button_height, font_label`.
|
||||
pub(crate) fn wide_primary_button(ui: &mut Ui, label: &str) -> Response {
|
||||
let t = Tokens::get();
|
||||
let text = RichText::new(crate::i18n::tr_label(label)).size(t.font_label).color(t.on_accent);
|
||||
let size = vec2(ui.available_width(), t.form_button_height);
|
||||
let response = ui.add(egui::Button::new(crate::fonts::RichTextExt::bold_face(text)).fill(t.form_primary).min_size(size));
|
||||
pointer_on_hover(ui, &response);
|
||||
response
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "actions_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -609,9 +609,6 @@ const ELEMENTS: &[Element] = &[
|
||||
primary_button(ui, "Create");
|
||||
});
|
||||
}),
|
||||
element!("wide_primary_button", |ui, _| {
|
||||
wide_primary_button(ui, "Finish Sketch");
|
||||
}),
|
||||
element!("button", "dialog:row:Gallery", Some("dialog:button:Gallery:Add"), |ui, _| {
|
||||
row(ui, "Gallery", |ui| {
|
||||
button(ui, "Add", Button::Normal);
|
||||
|
||||
+187
-46
@@ -1172,7 +1172,6 @@ pub(crate) fn pick_cached(app: &mut SolveApp, proj: &Proj, pos: Pos2) -> Vec<Hit
|
||||
app.dialog.as_ref().and_then(|d| d.active_input()).map(|i| i.accept).hash(&mut h);
|
||||
app.session.visibility.hidden_bodies.hash(&mut h);
|
||||
app.session.visibility.hidden_occurrences.hash(&mut h);
|
||||
origin_point_shown(app).hash(&mut h);
|
||||
app.perspective_view().hash(&mut h);
|
||||
(proj.rect.width().to_bits(), proj.rect.height().to_bits()).hash(&mut h);
|
||||
crate::sketch_display::projection_key(proj, app.viewport.pixels_per_point()).hash(&mut h);
|
||||
@@ -1366,12 +1365,41 @@ fn pick_hits(app: &SolveApp, proj: &Proj, pos: Pos2, other_sketch_points: bool,
|
||||
// The edges seen near the cursor, when the GPU pick buffer has them for this view (Select
|
||||
// Other wants hidden ones too).
|
||||
let seen = if all { None } else { crate::pick_buffer::edges_near(app, proj, pos, &st) };
|
||||
// The face under the pointer wins over the vertices and edges inside it when they are not
|
||||
// the boundary the pointer means: the edges between faces picked as one with it (a modelled
|
||||
// thread's thousands of faces), and, when the face is small on screen (a finely faceted
|
||||
// surface), any vertex or edge further than a couple of points away.
|
||||
let inside = match bestf.as_ref().map(|(_, h)| h) {
|
||||
Some(Hit::Face { body, index, .. }) if !all => st.body(body).map(|b| {
|
||||
let m = b.mesh();
|
||||
let groups = solidcraft_engine::doc::face_groups::face_groups_soon(&app.session.doc, b);
|
||||
let group = groups.as_ref().and_then(|g| g.group_of(*index));
|
||||
// Small on screen: only worth the look for a face picked alone.
|
||||
let small = group.is_none() && {
|
||||
let mut lo = egui::pos2(f32::INFINITY, f32::INFINITY);
|
||||
let mut hi = egui::pos2(f32::NEG_INFINITY, f32::NEG_INFINITY);
|
||||
for (t, f) in m.triangles.iter().zip(&m.tri_face) {
|
||||
if *f as usize == *index {
|
||||
for p in m.tri(t).into_iter().flatten().filter_map(|p| proj.to_screen(p)) {
|
||||
lo = lo.min(p);
|
||||
hi = hi.max(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
lo.x <= hi.x && (hi - lo).length() < 21.0
|
||||
};
|
||||
(body.clone(), groups.zip(group), small)
|
||||
}),
|
||||
_ => None,
|
||||
};
|
||||
let mut bestv = Near::new(all);
|
||||
let mut beste = Near::new(all);
|
||||
for (i, b) in st.bodies.iter().enumerate() {
|
||||
if !app.session.body_visible(&b.name) {
|
||||
continue;
|
||||
}
|
||||
let inside = inside.as_ref().filter(|(body, _, _)| *body == b.name);
|
||||
let (vertex_px, edge_px) = if inside.is_some_and(|(_, _, small)| *small) { (2.0, 2.0) } else { (7.0, 6.0) };
|
||||
// Only bodies whose outline on screen is near the cursor can have an edge under it.
|
||||
if let Some((_, bb)) = bounds.get(i)
|
||||
&& screen_box(proj, bb, 8.0).is_some_and(|r| !r.contains(pos))
|
||||
@@ -1392,12 +1420,18 @@ fn pick_hits(app: &SolveApp, proj: &Proj, pos: Pos2, other_sketch_points: bool,
|
||||
if !m.edge_pickable(ei) {
|
||||
continue;
|
||||
}
|
||||
// Between two faces picked as one with the face under the pointer: not a boundary.
|
||||
if let Some((_, Some((groups, group)), _)) = inside
|
||||
&& m.edge_faces.get(ei).is_some_and(|fs| !fs.is_empty() && fs.iter().all(|f| groups.group_of(*f as usize) == Some(*group)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if last_edge != Some(ei) {
|
||||
last_edge = Some(ei);
|
||||
for v in [e.first(), e.last()].into_iter().flatten() {
|
||||
if let Some(sp) = proj.to_screen(*v) {
|
||||
let dd = sp.distance(pos);
|
||||
if dd < 7.0 && visible(*v) && bestv.wants(dd) {
|
||||
if dd < vertex_px && visible(*v) && bestv.wants(dd) {
|
||||
bestv.offer(dd, Hit::Vertex { body: b.name.clone(), point: *v });
|
||||
}
|
||||
}
|
||||
@@ -1408,21 +1442,11 @@ fn pick_hits(app: &SolveApp, proj: &Proj, pos: Pos2, other_sketch_points: bool,
|
||||
let dd = seg_dist(pos, a, c);
|
||||
// Visible where the cursor is: the edge's point under the cursor, not an end or
|
||||
// the middle, which can lie deeper than the face the cursor is on.
|
||||
if dd < 6.0 && beste.wants(dd) && visible(ray_segment_closest(o, d, *p0, *p1)) {
|
||||
if dd < edge_px && beste.wants(dd) && visible(ray_segment_closest(o, d, *p0, *p1)) {
|
||||
beste.offer(dd, Hit::Edge { body: b.name.clone(), index: ei, mid: polyline_mid(e) });
|
||||
}
|
||||
}
|
||||
}
|
||||
// The origin point, for an input that takes it: a vertex of no body, drawn over everything.
|
||||
if app.dialog.as_ref().and_then(|d| d.active_input()).is_some_and(|i| i.accept & crate::selection::ORIGIN != 0)
|
||||
&& origin_point_shown(app)
|
||||
&& let Some(sp) = proj.to_screen(Vec3::ZERO)
|
||||
{
|
||||
let dd = sp.distance(pos);
|
||||
if dd < 7.0 && bestv.wants(dd) {
|
||||
bestv.offer(dd, Hit::Vertex { body: String::new(), point: Vec3::ZERO });
|
||||
}
|
||||
}
|
||||
hits.extend(bestv.hits());
|
||||
hits.extend(beste.hits());
|
||||
// Origin axes.
|
||||
@@ -1843,6 +1867,7 @@ fn edge_of(m: &solidcraft_engine::geom::Mesh, index: usize, point: Vec3) -> Opti
|
||||
m.edges.iter().min_by(|a, b| near(a).total_cmp(&near(b))).filter(|e| near(e) < tol * 1e3)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn face_tris(sc: &mut GpuScene, m: &solidcraft_engine::geom::Mesh, face: usize, col: [u8; 4], lit: bool) {
|
||||
mesh_tris(sc, m, col, lit, |f| f as usize == face);
|
||||
}
|
||||
@@ -1877,9 +1902,25 @@ fn edge_lines(sc: &mut GpuScene, e: &[Vec3], core: Color32, halo: Color32, w: f3
|
||||
}
|
||||
|
||||
/// The outline of a highlighted face where the model hides it (dimmed).
|
||||
fn hidden_face_outline(sc: &mut GpuScene, m: &solidcraft_engine::geom::Mesh, face: usize, col: Color32) {
|
||||
/// Whether an edge (between `faces`) lies inside one thread's faces: no boundary to draw or pick.
|
||||
fn inside_group(groups: Option<&solidcraft_engine::doc::face_groups::FaceGroups>, faces: &[u32]) -> bool {
|
||||
let Some(groups) = groups else { return false };
|
||||
let mut of = faces.iter().map(|f| groups.group_of(*f as usize));
|
||||
match of.next() {
|
||||
Some(Some(g)) => faces.len() > 1 && of.all(|x| x == Some(g)),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn hidden_faces_outline(
|
||||
sc: &mut GpuScene,
|
||||
m: &solidcraft_engine::geom::Mesh,
|
||||
faces: &std::collections::HashSet<usize>,
|
||||
groups: Option<&solidcraft_engine::doc::face_groups::FaceGroups>,
|
||||
col: Color32,
|
||||
) {
|
||||
for (ei, e) in m.edges.iter().enumerate() {
|
||||
let on = m.edge_faces.get(ei).is_some_and(|f| f.iter().any(|x| *x as usize == face));
|
||||
let on = m.edge_faces.get(ei).is_some_and(|f| f.iter().any(|x| faces.contains(&(*x as usize))) && !inside_group(groups, f));
|
||||
if on && m.edge_pickable(ei) {
|
||||
for p in e.windows(2) {
|
||||
sc.hidden_line(p[0].to_f32(), p[1].to_f32(), c4(col.gamma_multiply(0.45)), 1.4);
|
||||
@@ -1947,8 +1988,32 @@ fn highlight_geometry_key(app: &SolveApp) -> u64 {
|
||||
let selected = app.highlighted();
|
||||
app.session.revision.hash(&mut h);
|
||||
(std::sync::Arc::as_ptr(&crate::open_meshing::shown_world(app)) as usize).hash(&mut h);
|
||||
serde_json::to_string(&selected).unwrap_or_default().hash(&mut h);
|
||||
format!("{:?}", app.viewport.hover).hash(&mut h);
|
||||
// A thread selected is thousands of faces: they are hashed by body and index, not written
|
||||
// out every frame.
|
||||
selected.len().hash(&mut h);
|
||||
for item in &selected {
|
||||
match item {
|
||||
Sel::Face { body, index, .. } => (0u8, body, index).hash(&mut h),
|
||||
other => serde_json::to_string(other).unwrap_or_default().hash(&mut h),
|
||||
}
|
||||
}
|
||||
// A face hovered stands for the faces lit with it: moving over them (or within one) lights
|
||||
// the same, and the highlight is not built again.
|
||||
let group = match &app.viewport.hover {
|
||||
Some(Hit::Face { body, index, .. }) => app
|
||||
.session
|
||||
.world_state()
|
||||
.body(body)
|
||||
.and_then(|b| solidcraft_engine::doc::face_groups::face_groups_soon(&app.session.doc, b))
|
||||
.and_then(|g| g.of(*index))
|
||||
.and_then(|g| g.first().copied())
|
||||
.map(|first| (body.clone(), first)),
|
||||
_ => None,
|
||||
};
|
||||
match group {
|
||||
Some(group) => group.hash(&mut h),
|
||||
None => format!("{:?}", app.viewport.hover).hash(&mut h),
|
||||
}
|
||||
app.viewport.hover_feature.hash(&mut h);
|
||||
app.viewport.hover_bodies.hash(&mut h);
|
||||
// Which mesh lights a face follows how the model draws its body (see `shown_mesh`).
|
||||
@@ -2004,9 +2069,15 @@ pub(crate) fn build_highlight(app: &SolveApp) -> GpuScene {
|
||||
// Faces lit over the model use the triangles the model shows them with.
|
||||
let shown = |b: &solidcraft_engine::doc::ModelBody| shown_mesh(app, b, &active_bodies);
|
||||
let mut sel = Vec::new();
|
||||
// Faces by body and index: a selected thread stands for thousands of them.
|
||||
let mut seen_faces: std::collections::HashSet<(String, usize)> = std::collections::HashSet::new();
|
||||
for item in app.highlighted() {
|
||||
for piece in displayed_selection(app, &item) {
|
||||
if !sel.iter().any(|x| crate::selection::same_item(x, &piece)) {
|
||||
let fresh = match &piece {
|
||||
Sel::Face { body, index, .. } => seen_faces.insert((body.clone(), *index)),
|
||||
_ => !sel.iter().any(|x| crate::selection::same_item(x, &piece)),
|
||||
};
|
||||
if fresh {
|
||||
sel.push(piece);
|
||||
}
|
||||
}
|
||||
@@ -2087,16 +2158,31 @@ pub(crate) fn build_highlight(app: &SolveApp) -> GpuScene {
|
||||
return sc;
|
||||
}
|
||||
let mut feedback = GpuScene::default();
|
||||
// Selected faces, a pass per body however many of its faces are selected (a modelled
|
||||
// thread is thousands).
|
||||
let mut faces_of: Vec<(&str, std::collections::HashSet<usize>)> = Vec::new();
|
||||
for x in &sel {
|
||||
if let Sel::Face { body, index, .. } = x {
|
||||
match faces_of.iter_mut().find(|(b, _)| *b == body.as_str()) {
|
||||
Some((_, faces)) => {
|
||||
faces.insert(*index);
|
||||
}
|
||||
None => faces_of.push((body.as_str(), std::collections::HashSet::from([*index]))),
|
||||
}
|
||||
}
|
||||
}
|
||||
for (body, faces) in &faces_of {
|
||||
// A live preview stands in for the body: its old faces would fight with it.
|
||||
if let Some(b) = st.body(body).filter(|_| app.session.body_visible(body) && !app.preview.effective_replaced().iter().any(|r| r == *body)) {
|
||||
mesh_tris(&mut feedback, &shown(b), c4(t.sel_face), false, |f| faces.contains(&(f as usize)));
|
||||
let groups = solidcraft_engine::doc::face_groups::face_groups_soon(&s.doc, b);
|
||||
hidden_faces_outline(&mut feedback, &drawn_mesh(app, b), faces, groups.as_deref(), t.sel_edge);
|
||||
}
|
||||
}
|
||||
// Selected things.
|
||||
for x in &sel {
|
||||
match x {
|
||||
Sel::Face { body, index, .. } => {
|
||||
// A live preview stands in for the body: its old faces would fight with it.
|
||||
if let Some(b) = st.body(body).filter(|_| app.session.body_visible(body) && !app.preview.effective_replaced().contains(body)) {
|
||||
face_tris(&mut feedback, &shown(b), *index, c4(t.sel_face), false);
|
||||
hidden_face_outline(&mut feedback, &drawn_mesh(app, b), *index, t.sel_edge);
|
||||
}
|
||||
}
|
||||
Sel::Face { .. } => {}
|
||||
Sel::Body { name } => {
|
||||
if let Some(b) = st.body(name)
|
||||
&& app.session.body_visible(name)
|
||||
@@ -2222,12 +2308,21 @@ pub(crate) fn build_highlight(app: &SolveApp) -> GpuScene {
|
||||
let k = t.hover_face_lift;
|
||||
let c = body_rgb(app, body);
|
||||
let m = drawn_mesh(app, b);
|
||||
let patch = m.surface_patch(*index);
|
||||
let lit = shown(b);
|
||||
// The face with the faces picked as one with it (a modelled thread), in one pass.
|
||||
let patch = face_patch(s, b, &m, *index);
|
||||
let mut in_patch = vec![false; m.tri_face.iter().map(|f| *f as usize + 1).max().unwrap_or(0).max(*index + 1)];
|
||||
for &f in &patch {
|
||||
face_tris(&mut feedback, &lit, f, [c.0.saturating_add(k), c.1.saturating_add(k), c.2.saturating_add(k), 255], true);
|
||||
if let Some(slot) = in_patch.get_mut(f) {
|
||||
*slot = true;
|
||||
}
|
||||
}
|
||||
for (e, _) in m.edge_faces.iter().enumerate().filter(|(_, fs)| fs.iter().any(|f| patch.contains(&(*f as usize)))) {
|
||||
let lit = shown(b);
|
||||
let col = [c.0.saturating_add(k), c.1.saturating_add(k), c.2.saturating_add(k), 255];
|
||||
mesh_tris(&mut feedback, &lit, col, true, |f| in_patch.get(f as usize).copied().unwrap_or(false));
|
||||
// Its edges, but not those between two faces of one thread: no boundary there.
|
||||
let groups = solidcraft_engine::doc::face_groups::face_groups_soon(&s.doc, b);
|
||||
let lit_face = |f: &u32| in_patch.get(*f as usize).copied().unwrap_or(false);
|
||||
for (e, _) in m.edge_faces.iter().enumerate().filter(|(_, fs)| fs.iter().any(lit_face) && !inside_group(groups.as_deref(), fs)) {
|
||||
if let Some(p) = m.edges.get(e)
|
||||
&& m.edge_pickable(e)
|
||||
{
|
||||
@@ -3148,12 +3243,49 @@ pub(crate) fn picked_selection(s: &solidcraft_engine::Session, sel: &Sel) -> Vec
|
||||
{
|
||||
return letters.into_iter().map(|index| Sel::Profile { sketch: *sketch, index }).collect();
|
||||
}
|
||||
grouped_selection(s, sel)
|
||||
selection_pieces(s, sel)
|
||||
}
|
||||
|
||||
/// Expand a visible rim or surface into the kernel pieces a command needs. The original
|
||||
/// face pick keeps its point (used by tools such as Hole and construction geometry).
|
||||
/// The faces picked as one with `face` of `b` (`m` its mesh): the pieces of its surface joined
|
||||
/// across hidden seams, and every face of a modelled thread (or of one imported face) it is part
|
||||
/// of. The thread's faces are worked out on a worker; until then the face goes with its surface.
|
||||
fn face_patch(s: &solidcraft_engine::Session, b: &solidcraft_engine::doc::ModelBody, m: &solidcraft_engine::geom::Mesh, face: usize) -> Vec<usize> {
|
||||
face_pieces(s, b, m, face, true)
|
||||
}
|
||||
|
||||
/// [`face_patch`], with the faces of the thread it is part of when `groups`.
|
||||
fn face_pieces(
|
||||
s: &solidcraft_engine::Session,
|
||||
b: &solidcraft_engine::doc::ModelBody,
|
||||
m: &solidcraft_engine::geom::Mesh,
|
||||
face: usize,
|
||||
groups: bool,
|
||||
) -> Vec<usize> {
|
||||
let mut patch = m.surface_patch(face);
|
||||
if !groups {
|
||||
return patch;
|
||||
}
|
||||
if let Some(group) = solidcraft_engine::doc::face_groups::face_groups_soon(&s.doc, b).and_then(|g| g.of(face)) {
|
||||
let mut seen: std::collections::HashSet<usize> = patch.iter().copied().collect();
|
||||
patch.extend(group.iter().copied().filter(|f| seen.insert(*f)));
|
||||
}
|
||||
patch
|
||||
}
|
||||
|
||||
pub(crate) fn grouped_selection(s: &solidcraft_engine::Session, sel: &Sel) -> Vec<Sel> {
|
||||
pieces(s, sel, true)
|
||||
}
|
||||
|
||||
/// What a click or a box puts in the selection for `sel`: as [`grouped_selection`], but a face
|
||||
/// of a modelled thread stays one item (the thread's thousands of faces are what it stands for
|
||||
/// when shown or given to a command).
|
||||
pub(crate) fn selection_pieces(s: &solidcraft_engine::Session, sel: &Sel) -> Vec<Sel> {
|
||||
pieces(s, sel, false)
|
||||
}
|
||||
|
||||
fn pieces(s: &solidcraft_engine::Session, sel: &Sel, groups: bool) -> Vec<Sel> {
|
||||
let st = s.world_state();
|
||||
match sel {
|
||||
Sel::Edge { body, index, .. } => {
|
||||
@@ -3169,21 +3301,28 @@ pub(crate) fn grouped_selection(s: &solidcraft_engine::Session, sel: &Sel) -> Ve
|
||||
Sel::Face { body, index, .. } => {
|
||||
if let Some(b) = st.body(body) {
|
||||
let m = b.mesh();
|
||||
let mut patch = m.surface_patch(*index);
|
||||
let mut patch = face_pieces(s, b, &m, *index, groups);
|
||||
patch.sort_by_key(|i| usize::from(i != index));
|
||||
// A point on each face, from one pass over the triangles.
|
||||
let wanted: std::collections::HashSet<usize> = patch.iter().copied().collect();
|
||||
let mut point: std::collections::HashMap<usize, Vec3> = std::collections::HashMap::new();
|
||||
for (t, f) in m.triangles.iter().zip(&m.tri_face) {
|
||||
let f = *f as usize;
|
||||
if f != *index
|
||||
&& !point.contains_key(&f)
|
||||
&& wanted.contains(&f)
|
||||
&& let Some([a, b, c]) = m.tri(t)
|
||||
{
|
||||
point.insert(f, (a + b + c) / 3.0);
|
||||
}
|
||||
}
|
||||
return patch
|
||||
.into_iter()
|
||||
.filter_map(|i| {
|
||||
if i == *index {
|
||||
return Some(sel.clone());
|
||||
}
|
||||
m.triangles.iter().zip(&m.tri_face).find_map(|(t, f)| {
|
||||
if *f as usize != i {
|
||||
return None;
|
||||
}
|
||||
let [a, b, c] = m.tri(t)?;
|
||||
Some(Sel::Face { body: body.clone(), index: i, point: (a + b + c) / 3.0 })
|
||||
})
|
||||
point.get(&i).map(|p| Sel::Face { body: body.clone(), index: i, point: *p })
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
@@ -3358,9 +3497,15 @@ fn box_select(app: &mut SolveApp, proj: &Proj, bx: BoxSel, add: bool) {
|
||||
}
|
||||
}
|
||||
let mut grouped = Vec::new();
|
||||
// Faces by body and index: a box over a thread takes thousands of them.
|
||||
let mut faces: std::collections::HashSet<(String, usize)> = std::collections::HashSet::new();
|
||||
for item in out {
|
||||
for piece in grouped_selection(&app.session, &item) {
|
||||
if !grouped.iter().any(|x| crate::selection::same_item(x, &piece)) {
|
||||
for piece in selection_pieces(&app.session, &item) {
|
||||
let fresh = match &piece {
|
||||
Sel::Face { body, index, .. } => faces.insert((body.clone(), *index)),
|
||||
_ => !grouped.iter().any(|x| crate::selection::same_item(x, &piece)),
|
||||
};
|
||||
if fresh {
|
||||
grouped.push(piece);
|
||||
}
|
||||
}
|
||||
@@ -3375,15 +3520,11 @@ fn box_select(app: &mut SolveApp, proj: &Proj, bx: BoxSel, add: bool) {
|
||||
let _ = app.run("select.set", json!({"items": items, "add": add}));
|
||||
}
|
||||
|
||||
/// Whether the origin point is drawn (the Origin shown and its point not hidden).
|
||||
fn origin_point_shown(app: &SolveApp) -> bool {
|
||||
app.origin_visible() && !app.session.visibility.hidden_origin.iter().any(|h| h == "O")
|
||||
}
|
||||
|
||||
/// Screen-space dots: the origin point, and hovered or selected body vertices.
|
||||
fn points_2d(app: &SolveApp, painter: &egui::Painter, proj: &Proj) {
|
||||
let t = Tokens::get();
|
||||
if origin_point_shown(app)
|
||||
if app.origin_visible()
|
||||
&& !app.session.visibility.hidden_origin.iter().any(|h| h == "O")
|
||||
&& let Some(o) = proj.to_screen(Vec3::ZERO)
|
||||
{
|
||||
painter.circle(o, 4.5, t.origin_point, Stroke::new(1.0, Color32::from_gray(110)));
|
||||
|
||||
@@ -60,15 +60,6 @@ pub struct Section {
|
||||
pub ids: &'static [&'static str],
|
||||
}
|
||||
|
||||
/// Parameters and Configurations stay on every design tab they were on before the ribbon
|
||||
/// redesign (MODEL and SHEET METAL), on SKETCH while editing a sketch, and in MANAGE, their
|
||||
/// canonical home.
|
||||
const CONFIGURE: &[&str] = &["parameters.change", "config.table", "config.configure"];
|
||||
|
||||
/// Commands that stay for agents, MCP and scripts but are not offered to people: the sketch solves
|
||||
/// itself after every edit, so a Solve button (and the search hit for it) only confuses.
|
||||
pub const AGENT_ONLY: &[&str] = &["sketch.solve"];
|
||||
|
||||
pub const SECTIONS: &[Section] = &[
|
||||
Section {
|
||||
tab: "MODEL",
|
||||
@@ -89,7 +80,6 @@ pub const SECTIONS: &[Section] = &[
|
||||
promoted: 3,
|
||||
ids: &["solid.move", "solid.mirror", "solid.pattern.rectangular", "solid.align", "solid.pattern.circular", "solid.pattern.path"],
|
||||
},
|
||||
Section { tab: "MODEL", panel: "CONFIGURE", promoted: 2, ids: CONFIGURE },
|
||||
Section { tab: "MODEL", panel: "CONSTRUCT", promoted: 1, ids: &["construct.plane.offset"] },
|
||||
Section { tab: "SURFACE", panel: "CREATE", promoted: 4, ids: &["surface.extrude", "surface.revolve", "surface.sweep", "surface.loft"] },
|
||||
Section { tab: "SURFACE", panel: "EDIT", promoted: 4, ids: &["surface.trim", "surface.untrim", "surface.extend", "surface.stitch"] },
|
||||
@@ -242,11 +232,7 @@ pub const SECTIONS: &[Section] = &[
|
||||
"canvas.insert",
|
||||
],
|
||||
},
|
||||
// Sketch dimensions are driven by parameters, so they are one click away while sketching.
|
||||
Section { tab: "SKETCH", panel: "CONFIGURE", promoted: 2, ids: CONFIGURE },
|
||||
Section { tab: "SKETCH", panel: "FINISH SKETCH", promoted: 1, ids: &["sketch.finish"] },
|
||||
// New Sketch leads the tab, as on MODEL: a sheet part starts from a sketch (#3250).
|
||||
Section { tab: "SHEET METAL", panel: "SKETCH", promoted: 1, ids: &["sketch.create"] },
|
||||
Section {
|
||||
tab: "SHEET METAL",
|
||||
panel: "CREATE",
|
||||
@@ -256,7 +242,6 @@ pub const SECTIONS: &[Section] = &[
|
||||
Section { tab: "SHEET METAL", panel: "FLAT", promoted: 3, ids: &["sheet.unfold", "sheet.refold", "sheet.flat_pattern"] },
|
||||
Section { tab: "SHEET METAL", panel: "OUTPUT", promoted: 2, ids: &["sheet.export_dxf", "sheet.export_bend_table"] },
|
||||
Section { tab: "SHEET METAL", panel: "RULES", promoted: 1, ids: &["sheet.manage_rules"] },
|
||||
Section { tab: "SHEET METAL", panel: "CONFIGURE", promoted: 2, ids: CONFIGURE },
|
||||
Section { tab: "RENDER", panel: "SCENE", promoted: 2, ids: &["render.scene"] },
|
||||
Section { tab: "RENDER", panel: "IMAGE", promoted: 1, ids: &["render.start"] },
|
||||
];
|
||||
@@ -360,8 +345,6 @@ pub fn destination(c: &CommandSpec) -> Option<(&'static str, &'static str)> {
|
||||
let primary = match c.id {
|
||||
"sheet.manage_rules" => Some(("MANAGE", "RULES")),
|
||||
"sheet.convert" => Some(("MODEL", "MODIFY")),
|
||||
"parameters.change" => Some(("MANAGE", "PARAMETERS")),
|
||||
"config.table" | "config.configure" => Some(("MANAGE", "CONFIGURATIONS")),
|
||||
_ => None,
|
||||
};
|
||||
primary
|
||||
@@ -449,9 +432,9 @@ pub fn visible_tabs(app: &crate::SolveApp) -> Vec<&'static str> {
|
||||
if app.session.active_sketch.is_some() {
|
||||
tabs.push("SKETCH");
|
||||
}
|
||||
// Always reachable, as before the ribbon redesign: a sheet-metal part starts from an
|
||||
// ordinary sketch or solid, so the tab cannot wait for a sheet body.
|
||||
tabs.push("SHEET METAL");
|
||||
if sheet_body(app).is_some() {
|
||||
tabs.push("SHEET METAL");
|
||||
}
|
||||
tabs
|
||||
}
|
||||
|
||||
@@ -633,7 +616,6 @@ mod tests {
|
||||
("INSERT", "FILES", "component.insert_design"),
|
||||
("ASSEMBLE", "COMPONENTS", "component.insert_design"),
|
||||
("SHEET METAL", "CREATE", "sheet.convert"),
|
||||
("SHEET METAL", "SKETCH", "sketch.create"),
|
||||
("SKETCH", "REFERENCE", "canvas.insert"),
|
||||
] {
|
||||
assert!(layout(tab, panel, &specs).unwrap().0.iter().any(|c| c.id == id));
|
||||
@@ -643,9 +625,7 @@ mod tests {
|
||||
#[test]
|
||||
fn contextual_tabs_follow_the_active_body_and_allow_other_tabs() {
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
let mut fresh = TABS.to_vec();
|
||||
fresh.push("SHEET METAL");
|
||||
assert_eq!(visible_tabs(&h.app), fresh, "SHEET METAL is reachable before any sheet body exists");
|
||||
assert_eq!(visible_tabs(&h.app), TABS);
|
||||
h.app.run("sketch.create", json!({"plane":"XY"})).unwrap();
|
||||
assert_eq!(h.app.ui.tab, "SKETCH");
|
||||
h.app.run("sketch.rectangle.two_point", json!({"p0":[0,0],"p1":[40,30]})).unwrap();
|
||||
@@ -665,69 +645,15 @@ mod tests {
|
||||
h.app.run("solid.box", json!({"body_name":"Solid", "operation":"new", "length":10, "width":10, "height":10})).unwrap();
|
||||
h.app.run("select.set", json!({"items":[{"type":"body","name":"Solid"}]})).unwrap();
|
||||
sync_context(&mut h.app);
|
||||
assert!(visible_tabs(&h.app).contains(&"SHEET METAL"), "the tab stays reachable off a sheet body");
|
||||
assert!(sheet_body(&h.app).is_none());
|
||||
assert!(!visible_tabs(&h.app).contains(&"SHEET METAL"));
|
||||
h.app.run("select.set", json!({"items":[{"type":"body","name":"Sheet"}]})).unwrap();
|
||||
sync_context(&mut h.app);
|
||||
assert_eq!(h.app.ui.tab, "SHEET METAL");
|
||||
}
|
||||
|
||||
/// Parameters and Configurations are on the design tabs they were on before the redesign, and a
|
||||
/// fresh document reaches Sheet Metal (to flange a sketch or convert a solid) by its tab.
|
||||
#[test]
|
||||
fn parameters_and_configurations_are_on_model_and_sheet_metal_and_the_sheet_tab_opens_fresh() {
|
||||
let specs = solidcraft_engine::command_specs();
|
||||
for tab in ["MODEL", "SHEET METAL", "SKETCH", "MANAGE"] {
|
||||
let ids = panel_ids(tab, &specs);
|
||||
assert!(ids.contains(&"parameters.change"), "{tab}: Parameters");
|
||||
assert!(ids.contains(&"config.table"), "{tab}: Configurations");
|
||||
}
|
||||
for tab in ["MODEL", "SHEET METAL", "SKETCH"] {
|
||||
let (commands, n) = layout(tab, "CONFIGURE", &specs).unwrap();
|
||||
assert_eq!(commands.iter().take(n).map(|c| c.id).collect::<Vec<_>>(), ["parameters.change", "config.table"]);
|
||||
}
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
assert!(sheet_body(&h.app).is_none());
|
||||
h.step(&json!({"click":{"handle":"tab:SHEET METAL"}})).unwrap();
|
||||
h.frames(2);
|
||||
assert_eq!(h.app.ui.tab, "SHEET METAL");
|
||||
let specs = solidcraft_engine::command_specs();
|
||||
let (create, _) = layout("SHEET METAL", "CREATE", &specs).unwrap();
|
||||
assert!(create.iter().any(|c| c.id == "sheet.convert"), "Convert to Sheet Metal is in the Sheet Metal tab's Create panel");
|
||||
for id in ["sheet.flange", "parameters.change", "config.table"] {
|
||||
assert!(crate::scenario::handle_at(&format!("button:{id}")).is_some(), "{id} is a button on the fresh Sheet Metal tab");
|
||||
}
|
||||
h.step(&json!({"click":{"handle":"button:parameters.change"}})).unwrap();
|
||||
assert!(h.app.params.is_some(), "Parameters opens from the Sheet Metal tab");
|
||||
assert_eq!(h.app.ui.tab, "SHEET METAL", "and the tab stays");
|
||||
h.step(&json!({"click":{"handle":"params:close"}})).unwrap();
|
||||
h.app.run("sketch.create", json!({"plane":"XY"})).unwrap();
|
||||
h.frames(2);
|
||||
assert_eq!(h.app.ui.tab, "SKETCH");
|
||||
for id in ["parameters.change", "config.table"] {
|
||||
assert!(crate::scenario::handle_at(&format!("button:{id}")).is_some(), "{id} is a button on the Sketch tab");
|
||||
}
|
||||
h.app.finish_sketch();
|
||||
h.step(&json!({"click":{"handle":"tab:MODEL"}})).unwrap();
|
||||
h.frames(2);
|
||||
h.step(&json!({"click":{"handle":"button:config.table"}})).unwrap();
|
||||
assert!(matches!(
|
||||
h.app.dialog.as_ref().map(|d| &d.kind),
|
||||
Some(crate::dialogs::Kind::Motion(crate::dialogs_motion::Mo::Configurations { .. }))
|
||||
));
|
||||
assert_eq!(h.app.ui.tab, "MODEL", "Configurations opens from the Model tab without leaving it");
|
||||
}
|
||||
|
||||
fn panel_ids(tab: &str, specs: &[&CommandSpec]) -> Vec<&'static str> {
|
||||
SECTIONS.iter().filter(|s| s.tab == tab).flat_map(|s| layout(tab, s.panel, specs).unwrap().0).map(|c| c.id).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn starts_palette_and_control_use_the_new_destinations() {
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
// Parameters is also on MODEL, so a start from there stays put; from SURFACE it goes home.
|
||||
h.app.ui.tab = "SURFACE".into();
|
||||
h.frames(2);
|
||||
for (id, tab) in [("parameters.change", "MANAGE"), ("solid.box", "MODEL"), ("surface.patch", "SURFACE")] {
|
||||
h.step(&json!({"start":id})).unwrap();
|
||||
assert_eq!(h.app.ui.tab, tab, "{id}");
|
||||
@@ -851,101 +777,4 @@ mod tests {
|
||||
}
|
||||
assert_eq!(h.app.session.world_state().bodies.len(), 2, "the STEP file's body is inserted");
|
||||
}
|
||||
|
||||
/// Every way into a sketch (New Sketch on the ribbon then a plane, a double-click in the
|
||||
/// timeline or the browser, Edit Sketch in the browser's menu) opens the SKETCH tab and looks
|
||||
/// at the sketch; Finish Sketch (palette or ribbon) lands on MODEL with the earlier view back
|
||||
/// and the sketch still listed, as before the ribbon redesign.
|
||||
#[test]
|
||||
fn entering_and_finishing_a_sketch_keeps_the_tab_view_and_history() {
|
||||
let iso = json!({"camera": {"yaw_deg": -45, "pitch_deg": 35}});
|
||||
let top = json!({"camera": {"yaw_deg": 0, "pitch_deg": 90}});
|
||||
let expect = |h: &mut crate::scenario::Harness, e: &serde_json::Value, what: &str| {
|
||||
h.frames(3);
|
||||
crate::scenario::check(h, e).unwrap_or_else(|err| panic!("{what}: {err}"));
|
||||
};
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
h.call("ui.view", json!({"view": "iso"}));
|
||||
h.frames(3);
|
||||
h.step(&json!({"click": {"handle": "button:sketch.create"}})).unwrap();
|
||||
h.step(&json!({"click": {"plane": "XY"}})).unwrap();
|
||||
expect(&mut h, &top, "New Sketch on XY");
|
||||
assert_eq!(h.app.ui.tab, "SKETCH");
|
||||
h.app.run("sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [40, 30]})).unwrap();
|
||||
h.step(&json!({"click": {"handle": "sketch_palette:finish"}})).unwrap();
|
||||
expect(&mut h, &json!({"sketching": false, "errors": 0}), "Finish");
|
||||
expect(&mut h, &iso, "Finish puts the view back");
|
||||
assert_eq!(h.app.ui.tab, "MODEL");
|
||||
assert!(crate::scenario::handle_at("timeline:Sketch1").is_some() && crate::scenario::handle_at("row:Sketch1").is_some());
|
||||
|
||||
let entries = [
|
||||
("timeline double-click", vec![json!({"click": {"handle": "timeline:Sketch1"}, "double": true})]),
|
||||
("browser double-click", vec![json!({"click": {"handle": "row:Sketch1"}, "double": true})]),
|
||||
(
|
||||
"browser Edit Sketch",
|
||||
vec![json!({"click": {"handle": "row:Sketch1"}, "button": "right"}), json!({"click": {"handle": "menu:ui.editSketch"}})],
|
||||
),
|
||||
];
|
||||
for (how, steps) in entries {
|
||||
for s in steps {
|
||||
h.frames(3);
|
||||
h.step(&s).unwrap_or_else(|e| panic!("{how}: {e}"));
|
||||
}
|
||||
expect(&mut h, &json!({"sketching": true}), how);
|
||||
expect(&mut h, &top, how);
|
||||
assert_eq!(h.app.ui.tab, "SKETCH", "{how}");
|
||||
h.step(&json!({"click": {"handle": "button:sketch.finish"}})).unwrap();
|
||||
expect(&mut h, &json!({"sketching": false}), how);
|
||||
expect(&mut h, &iso, how);
|
||||
assert_eq!(h.app.ui.tab, "MODEL", "{how}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Escape and Enter, pressed one at a time, never leave a sketch (#1804); Undo right after
|
||||
/// Finish reopens it, and a second Undo removes it and leaves the SKETCH tab.
|
||||
#[test]
|
||||
fn keys_keep_a_sketch_open_and_undo_after_finish_reopens_it() {
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
h.frames(3);
|
||||
h.step(&json!({"start": "sketch.create"})).unwrap();
|
||||
h.step(&json!({"click": {"plane": "XY"}})).unwrap();
|
||||
h.app.run("sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [40, 30]})).unwrap();
|
||||
for key in [json!({"key": "Escape"}), json!({"key": "Escape"}), json!({"key": "Enter"}), json!({"key": "Enter", "cmd": true})] {
|
||||
h.step(&key).unwrap();
|
||||
// Well apart, so two presses are never a quick double press.
|
||||
h.frames(60);
|
||||
assert!(h.app.session.active_sketch.is_some(), "{key}");
|
||||
assert_eq!(h.app.ui.tab, "SKETCH", "{key}");
|
||||
}
|
||||
h.step(&json!({"click": {"handle": "sketch_palette:finish"}})).unwrap();
|
||||
h.frames(2);
|
||||
h.step(&json!({"key": "Z", "cmd": true})).unwrap();
|
||||
h.frames(2);
|
||||
assert!(h.app.session.active_sketch.is_some(), "Undo after Finish reopens the sketch");
|
||||
assert_eq!(h.app.ui.tab, "SKETCH");
|
||||
h.step(&json!({"key": "Z", "cmd": true})).unwrap();
|
||||
h.frames(2);
|
||||
assert!(h.app.session.active_sketch.is_none());
|
||||
assert_eq!(h.app.ui.tab, "MODEL");
|
||||
}
|
||||
|
||||
/// New Sketch is on the SHEET METAL tab, as it was before the ribbon redesign, and a sketch
|
||||
/// started there stays on its tab until it opens.
|
||||
#[test]
|
||||
fn new_sketch_starts_from_the_sheet_metal_tab() {
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
h.app.run("sketch.create", json!({"plane": "XY"})).unwrap();
|
||||
h.app.run("sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [40, 30]})).unwrap();
|
||||
h.app.run("sketch.finish", json!({})).unwrap();
|
||||
h.app.run("sheet.flange", json!({"name": "Sheet", "body_name": "Sheet"})).unwrap();
|
||||
h.frames(3);
|
||||
assert_eq!(h.app.ui.tab, "SHEET METAL");
|
||||
h.step(&json!({"click": {"handle": "button:sketch.create"}})).unwrap();
|
||||
h.frames(2);
|
||||
assert_eq!(h.app.ui.tab, "SHEET METAL", "New Sketch is on this tab, so it stays");
|
||||
h.step(&json!({"click": {"plane": "XZ"}})).unwrap();
|
||||
h.frames(2);
|
||||
assert!(h.app.session.active_sketch.is_some());
|
||||
assert_eq!(h.app.ui.tab, "SKETCH");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
"click": {"handle": "appbar:file"}
|
||||
},
|
||||
{
|
||||
"widget": "Export STL\u2026"
|
||||
"widget": "Save as mesh\u2026"
|
||||
},
|
||||
{
|
||||
"expect": {
|
||||
|
||||
@@ -77,8 +77,8 @@ Pull requests skip a few heavy tests (each takes one to twelve minutes); they ru
|
||||
`main`, nightly, and in `cargo xtask test`:
|
||||
|
||||
- `solidcraft-engine`: `tests::hostile_params_never_panic`, everything in `tests::robustness` and
|
||||
the seeds of `tests::robustness_goldens` (`seed_00` … `seed_95`; pull requests that change the kernel, truck or
|
||||
the goldens run all but the slowest, `SLOW_SEEDS` in `ci.yml`),
|
||||
the seeds of `tests::robustness_goldens` (`seed_00` … `seed_95`; pull requests that can change
|
||||
them run all but the slowest, `SLOW_SEEDS` in `ci.yml`),
|
||||
`pattern_suppress_tests::enclosure_arc_extinction_keeps_a_real_sharp_inner_lip_after_resize_and_reopen`,
|
||||
`pattern_suppress_tests::enclosure_datums_follow_wall_height_and_board_depth_with_material_proofs`,
|
||||
`cmd::plastic::tests::enclosure_snap_fit_follows_resized_front_wall`, the `atomic_commands` contract tests
|
||||
|
||||
@@ -38,9 +38,8 @@ A 3MF project saved by a slicer comes in too, also when it keeps its objects in
|
||||
their own inside the file (the 3MF Production extension): each object on the build plate becomes
|
||||
a mesh body, with its parts where the slicer placed them.
|
||||
|
||||
**File → Export STL…** and **File → Export 3MF…** (also on the Mesh tab under Modify as Save as
|
||||
mesh, and in the right-click menu of a body) open the same dialog with that format chosen; it saves
|
||||
the selected bodies as STL, 3MF or 3MF with face tags (below), and you can change the format there. With **Send to slicer** ticked, it writes the file to
|
||||
**File → Save as mesh…** (also on the Mesh tab under Modify, and in the right-click menu of a
|
||||
body) saves the selected bodies as STL, 3MF or 3MF with face tags (below). With **Send to slicer** ticked, it writes the file to
|
||||
a temporary folder and opens it in the chosen slicer instead of asking for a file name. The slicer
|
||||
list shows the slicers found on the computer with their icons; **Custom…** adds any other program
|
||||
to the top of the list (kept between runs; hover an entry you added and press Delete to remove it).
|
||||
@@ -48,7 +47,7 @@ to the top of the list (kept between runs; hover an entry you added and press De
|
||||
Closed mechanical meshes with a constant cross-section can be rebuilt as solids with the
|
||||
`mesh.convert` command (command palette, scripts and the control channel).
|
||||
|
||||
**File → Export 3MF…** with the format changed to **3MF with face tags** (`file.export` or `file.save_mesh` with `format: "3mf-tagged"`) writes an
|
||||
**File → Save as mesh…** with the format **3MF with face tags** (`file.export` or `file.save_mesh` with `format: "3mf-tagged"`) writes an
|
||||
ordinary 3MF in which every triangle also names the B-rep face it lies on and every coordinate
|
||||
is the tessellation's own double-precision value. Meshing and analysis tools can use the tags to
|
||||
keep a model's faces (for loads, boundary conditions or feature edges); other 3MF readers ignore
|
||||
@@ -62,9 +61,8 @@ written as STL files next to the `.urdf`. See [`docs/urdf.md`](https://github.co
|
||||
Saves are atomic: a crash at any moment leaves either the old file or the new one, and the
|
||||
replaced version is kept as `<file>.bak`. A save keeps the file's permissions, and a design
|
||||
opened through a symbolic link (a synced folder linked into a project) is saved into the file the
|
||||
link points to, so the link stays. Unsaved changes in every open tab are autosaved to separate
|
||||
entries in a recovery folder, and the next launch offers to recover each design. Saving or
|
||||
closing one tab removes only its recovery entry. Older design files are upgraded on open, and files
|
||||
link points to, so the link stays. Unsaved changes are autosaved to a recovery folder,
|
||||
and the next launch offers to recover them. Older design files are upgraded on open, and files
|
||||
from a newer version are refused with a message.
|
||||
|
||||
**Save** writes a container by default; files named `.json` keep JSON on subsequent saves.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Sheet metal
|
||||
|
||||
MODEL → MODIFY offers Convert to Sheet Metal. The SHEET METAL tab is always available (it is highlighted
|
||||
and selected when the active body is a sheet), and builds parts from a sheet of constant thickness under a **sheet metal rule**
|
||||
MODEL → MODIFY offers Convert to Sheet Metal. The contextual SHEET METAL tab appears
|
||||
when the active body is a sheet, and builds parts from a sheet of constant thickness under a **sheet metal rule**
|
||||
(thickness, bend radius, K-factor), set in **Sheet Metal Rules** (`sheet.manage_rules`).
|
||||
|
||||
- **Flange** (`sheet.flange`): a base flange from a sketch profile, a contour flange from an open
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
unsaved changes). On the right: Report a bug, Help (?), Preferences (which hold the theme) and the
|
||||
window's buttons.
|
||||
- **Toolbar.** The workspace switcher (DESIGN) sits on the left. The tabs (MODEL, SURFACE,
|
||||
ASSEMBLE, INSERT, INSPECT, MANAGE, VIEW and SHEET METAL) hold sections of commands. SKETCH appears while
|
||||
editing a sketch; SHEET METAL is always there and is highlighted for the active sheet body. Both select automatically
|
||||
ASSEMBLE, INSERT, INSPECT, MANAGE and VIEW) hold sections of commands. SKETCH appears while
|
||||
editing a sketch; SHEET METAL appears for the active sheet body. Both select automatically
|
||||
when entered, and the other tabs remain available. Click a
|
||||
panel's name for all its commands. Drag a command from the dropdown onto a panel's label
|
||||
or empty space to append it; the insertion line shows its destination. Drop onto an existing
|
||||
|
||||
@@ -74,7 +74,7 @@ MCP server sends its folders this way on every request ([mcp.md](mcp.md)). Witho
|
||||
|---|---|---|
|
||||
| `engine.execute` | `command`: command id, `params`: JSON object | the command's result |
|
||||
| `engine.script` | `commands`: `[{command, params}, …]` | results, stops at the first error |
|
||||
| `engine.batch` | `commands`: `[{command, params}, …]` (at most 10 000), `rollback?`: bool (default true), `timings?`, `profile?`: bool (default false) | `{completed, results, undone}`, plus `failed: {index, command, error}` when a step fails, and with `timings` `timings: [{index, command, ms}]` (the failing step included) and `total_ms`; `profile` adds each step's `profile: {boolean_ms, tessellate_ms, other_ms}`. Runs the steps in one request, so within one frame, each as `engine.execute` runs it. The first failure stops the batch, and with `rollback` the design goes back to how it was before it (`undone`: the steps taken back, `rolled_back`: whether the design is back as it was, `false` only when the checkpoint could not restore it). Refused before anything runs, with an error starting `engine.batch unavailable`, while the agent cursor is shown or when a step loads a file (`doc.open`, `file.insert_step`, `file.insert_mesh`). Send such steps one by one. Answered when done, for up to 3600 s |
|
||||
| `engine.batch` | `commands`: `[{command, params}, …]` (at most 10 000), `rollback?`: bool (default true), `timings?`, `profile?`: bool (default false) | `{completed, results, undone}`, plus `failed: {index, command, error}` when a step fails, and with `timings` `timings: [{index, command, ms}]` (the failing step included) and `total_ms`; `profile` adds each step's `profile: {boolean_ms, tessellate_ms, other_ms}`. Runs the steps in one request, so within one frame, each as `engine.execute` runs it. The first failure stops the batch, and with `rollback` the design goes back to how it was before it (`undone`: the steps taken back). Refused before anything runs, with an error starting `engine.batch unavailable`, while the agent cursor is shown or when a step loads a file (`doc.open`, `file.insert_step`, `file.insert_mesh`). Send such steps one by one. Answered when done, for up to 3600 s |
|
||||
| `engine.commands` | — | every command: id, label, tab, panel, icon, shortcut, params doc, enabled |
|
||||
| `document.inspect` | `measure?`: bool | parameters, timeline (errors, timings), bodies, sketches, selection (assembly joints: `engine.execute` `joint.inspect {joint?}`, which reads only: each joint's two origins with the occurrence, the snap as saved, the feature, face, edge or sketch curve its names refer to, and whether it resolves or why not; kind, offset, angle, flip, values, limits, expressions and motion links) |
|
||||
|
||||
@@ -140,7 +140,7 @@ labels; `prefs:measure_axis_distances` identifies the checkbox.
|
||||
| `ui.sketchToScreen` | `points`: `[[x, y]…]` in active-sketch coordinates | their screen points (null when behind the camera) |
|
||||
| `ui.dialogInput` | `index` | what clicking an input's Select chip does: picks go to that input |
|
||||
| `ui.at` | `world: [x,y,z]` \| `sketch: [x,y]` \| `plane: "XY"` \| `axis: "Z"` \| `dimension: "d1"` (its value text) \| `handle: "arrow"` (a manipulator handle, see `scenario::publish_handle`) | the screen point (a scenario step's target; see `crates/ui-egui/src/scenario.rs`) |
|
||||
| `ui.handles` | `prefix?` | the handles drawn last frame, `[{name, x, y}]`: toolbar buttons and drop-down items `toolbar:<command id>` (the button alone: `button:<command id>`; a drop-down row: `dropdown:<command id>`, highlighted `dropdown_hot:<command id>`, its pin `dropdown_pin:<command id>`; Customize Toolbar's check boxes `customize:<command id>`), ribbon tabs `tab:<name>` (MODEL, SURFACE, ASSEMBLE, INSERT, INSPECT, MANAGE, VIEW, SHEET METAL; contextual SKETCH; workspace RENDER), panel labels `panel:<tab>:<panel>` (also `ribbon:section:<TAB>:<SECTION>`), document overflow list `tab:overflow`, workspace column `ribbon:workspace`, command-search launcher `ribbon:search`, browser rows `row:<label>` and fold arrows `fold:<label>`, manipulator handles; signed-axis tips `axis_view:z`, `axis_view:-y`; optional cube facets `navigation_cube:top`, `navigation_cube:top_front_right`, controls `navigation_cube:control:home` (and other actions), plus signed-axis aliases for visible cube faces; Preferences navigator picker `prefs:navigator`, options `prefs:navigator:Signed axes` and `prefs:navigator:Cube`; in the Motion Study dialog, where the chart puts its first and last step `motion:chart_start` and `motion:chart_end`, the Check collisions button `motion:check` and each collision found `motion:collision:<n>` |
|
||||
| `ui.handles` | `prefix?` | the handles drawn last frame, `[{name, x, y}]`: toolbar buttons and drop-down items `toolbar:<command id>` (the button alone: `button:<command id>`; a drop-down row: `dropdown:<command id>`, highlighted `dropdown_hot:<command id>`, its pin `dropdown_pin:<command id>`; Customize Toolbar's check boxes `customize:<command id>`), ribbon tabs `tab:<name>` (MODEL, SURFACE, ASSEMBLE, INSERT, INSPECT, MANAGE, VIEW; contextual SKETCH and SHEET METAL; workspace RENDER), panel labels `panel:<tab>:<panel>` (also `ribbon:section:<TAB>:<SECTION>`), document overflow list `tab:overflow`, workspace column `ribbon:workspace`, command-search launcher `ribbon:search`, browser rows `row:<label>` and fold arrows `fold:<label>`, manipulator handles; signed-axis tips `axis_view:z`, `axis_view:-y`; optional cube facets `navigation_cube:top`, `navigation_cube:top_front_right`, controls `navigation_cube:control:home` (and other actions), plus signed-axis aliases for visible cube faces; Preferences navigator picker `prefs:navigator`, options `prefs:navigator:Signed axes` and `prefs:navigator:Cube`; in the Motion Study dialog, where the chart puts its first and last step `motion:chart_start` and `motion:chart_end`, the Check collisions button `motion:check` and each collision found `motion:collision:<n>` |
|
||||
| `ui.editFeature` | `feature` (id or name) | what double-clicking a timeline item does: rolls back to it and opens its dialog filled in (sketches open in sketch mode) |
|
||||
| `ui.scroll` | `x`, `y`, `delta?` | wheel zoom at the cursor |
|
||||
| `ui.key` | `key` (egui key name, `Enter`, `Escape`…), `cmd?`, `shift?` | |
|
||||
|
||||
@@ -7,7 +7,7 @@ A tessellated body loses its faces: a plain mesh says where the surface is, not
|
||||
each triangle came from. Downstream tools that mesh or analyse a part need that back. They apply
|
||||
loads and boundary conditions to faces, keep face boundaries as feature edges, and group
|
||||
results by face. SolidCraft therefore exports a 3MF in which every triangle carries the index of
|
||||
the B-rep face it lies on (**File → Export 3MF…**, then the format **3MF with face tags**, or `file.export` with
|
||||
the B-rep face it lies on (**File → Save as mesh…** (format **3MF with face tags**), or `file.export` with
|
||||
`format: "3mf-tagged"`).
|
||||
|
||||
## Why 3MF
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
The UI layout in `crates/ui-egui/src/workspace.rs` maps the original engine registry placements into ribbon sections. `docs/features.md` continues to document registry placements; command registrations and ids are unchanged.
|
||||
|
||||
Always shown, in order: MODEL, SURFACE, ASSEMBLE, INSERT, INSPECT, MANAGE, VIEW, SHEET METAL. SKETCH appears during sketch editing. SHEET METAL is reachable in every document, so a sheet-metal part can start from a sketch or a solid; it takes the contextual style and selects itself when the selected body (or the last body of the active component) has evaluated sheet-metal data. Entering a context selects its tab; the other tabs remain available. RENDER belongs to the Render workspace.
|
||||
Always shown, in order: MODEL, SURFACE, ASSEMBLE, INSERT, INSPECT, MANAGE, VIEW. SKETCH appears during sketch editing; SHEET METAL appears for the selected body (or the last body of the active component) when it has evaluated sheet-metal data. Entering a new context selects its tab; the other tabs remain available. RENDER belongs to the Render workspace.
|
||||
|
||||
**Bold command ids are default promoted buttons.** Every listed command remains in the section menu. Saved user pins can add buttons, as before.
|
||||
|
||||
@@ -12,7 +12,6 @@ Always shown, in order: MODEL, SURFACE, ASSEMBLE, INSERT, INSPECT, MANAGE, VIEW,
|
||||
| MODEL | CREATE | **`solid.extrude`**, **`solid.revolve`**, **`solid.hole`**, `solid.sweep`, `solid.loft`, `solid.box`, `solid.cylinder`, `solid.sphere`, `solid.torus`, `solid.pipe`, `solid.bounding_solid`, `solid.thread`, `solid.boundary_fill`, `solid.coil`, `solid.rib`, `solid.web`, `solid.emboss`, `plastic.boss`, `plastic.lip`, `plastic.snap_fit`, `plastic.rest` |
|
||||
| MODEL | MODIFY | **`solid.fillet`**, **`solid.chamfer`**, **`solid.shell`**, **`solid.press_pull`**, `sheet.convert`, `solid.combine`, `solid.scale`, `solid.offset_face`, `solid.draft`, `solid.split_face`, `solid.split_body`, `solid.replace_face`, `solid.remove`, `timeline.delete`, `face.delete` |
|
||||
| MODEL | ARRANGE | **`solid.move`**, **`solid.mirror`**, **`solid.pattern.rectangular`**, `solid.align`, `solid.pattern.circular`, `solid.pattern.path` |
|
||||
| MODEL | CONFIGURE | **`parameters.change`**, **`config.table`**, `config.configure` |
|
||||
| MODEL | CONSTRUCT | **`construct.plane.offset`**, `construct.plane.at_angle`, `construct.axis.through_cylinder`, `construct.axis.normal_to_face`, `construct.axis.two_planes`, `construct.axis.two_points`, `construct.axis.edge`, `construct.point.vertex`, `construct.point.two_edges`, `construct.point.three_planes`, `construct.point.center`, `construct.point.edge_plane`, `construct.point.along_path`, `construct.plane.midplane`, `construct.plane.two_edges`, `construct.plane.three_points`, `construct.plane.tangent`, `construct.plane.along_path`, `construct.plane.perpendicular` |
|
||||
| SURFACE | CREATE | **`surface.extrude`**, **`surface.revolve`**, **`surface.sweep`**, **`surface.loft`**, `surface.boundary`, `surface.patch`, `surface.offset`, `surface.ruled`, `surface.blend` |
|
||||
| SURFACE | EDIT | **`surface.trim`**, **`surface.untrim`**, **`surface.extend`**, **`surface.stitch`**, `surface.unstitch`, `surface.reverse_normal`, `surface.fair`, `surface.fillet`, `surface.match` |
|
||||
@@ -45,13 +44,10 @@ Always shown, in order: MODEL, SURFACE, ASSEMBLE, INSERT, INSPECT, MANAGE, VIEW,
|
||||
| SKETCH | CONSTRAIN | **`sketch.dimension`**, **`sketch.constraint.horizontal_vertical`**, **`sketch.constraint.coincident`**, **`sketch.constraint.tangent`**, **`sketch.constraint.equal`**, **`sketch.constraint.parallel`**, **`sketch.constraint.perpendicular`**, `sketch.construction`, `sketch.constrainer`, `sketch.constraint.fix`, `sketch.constraint.midpoint`, `sketch.constraint.concentric`, `sketch.constraint.collinear`, `sketch.constraint.symmetry`, `sketch.constraint.curvature`, `sketch.constraint.polygon`, `sketch.auto_constrain`, `sketch.auto_constrain_from_datum` |
|
||||
| SKETCH | REFERENCE | **`sketch.project`**, `sketch.intersect`, `sketch.include_3d`, `sketch.project_to_surface`, `sketch.intersection_curve`, `sketch.spun_profile`, `sketch.isoparametric_curve`, `sketch.fit_curves_to_section`, `sketch.insert_dxf`, `sketch.insert_svg`, `canvas.insert` |
|
||||
| SKETCH | FINISH SKETCH | **`sketch.finish`**, `sketch.finish_auto_constrain` |
|
||||
| SHEET METAL | SKETCH | **`sketch.create`** |
|
||||
| SHEET METAL | CREATE | **`sheet.flange`**, **`sheet.hem`**, **`sheet.fold`**, **`sheet.perforate`**, `sheet.convert` |
|
||||
| SHEET METAL | FLAT | **`sheet.unfold`**, **`sheet.refold`**, **`sheet.flat_pattern`** |
|
||||
| SHEET METAL | OUTPUT | **`sheet.export_dxf`**, **`sheet.export_bend_table`** |
|
||||
| SHEET METAL | RULES | **`sheet.manage_rules`** |
|
||||
| SHEET METAL | CONFIGURE | **`parameters.change`**, **`config.table`**, `config.configure` |
|
||||
| SKETCH | CONFIGURE | **`parameters.change`**, **`config.table`**, `config.configure` |
|
||||
| RENDER | SCENE | **`render.scene`** |
|
||||
| RENDER | IMAGE | **`render.start`** |
|
||||
|
||||
@@ -67,7 +63,6 @@ Explicit command-id lists override the default placement mapping. Shared command
|
||||
| RENDER / IMAGE | RENDER / IMAGE | `render.start` |
|
||||
| RENDER / SCENE | RENDER / SCENE | `render.scene` |
|
||||
| SHEET METAL / CREATE | MODEL / MODIFY | `sheet.convert` |
|
||||
| SHEET METAL / CREATE | SHEET METAL / SKETCH | `sketch.create` |
|
||||
| SHEET METAL / CREATE | SHEET METAL / CREATE | `sheet.flange`, `sheet.perforate`, `sheet.hem`, `sheet.fold` |
|
||||
| SHEET METAL / CREATE | SHEET METAL / FLAT | `sheet.flat_pattern` |
|
||||
| SHEET METAL / MODIFY | MANAGE / RULES | `sheet.manage_rules` |
|
||||
@@ -131,7 +126,7 @@ Additional old panels used for shared layouts migrate as follows: SELECT on any
|
||||
|
||||
## Automation handles
|
||||
|
||||
The tab handle form stays `tab:<UPPERCASE NAME>`. Added: `tab:MODEL`, `tab:ASSEMBLE`, `tab:INSERT`, `tab:INSPECT`, `tab:MANAGE`, `tab:VIEW`. SURFACE and RENDER retain their names; SKETCH is available only in context; SHEET METAL is always available. Legacy aliases remain: `tab:SOLID` and `tab:PLASTIC` → MODEL; `tab:MESH` → SURFACE; `tab:UTILITIES` → MANAGE. The Home and document-tab handles are unchanged.
|
||||
The tab handle form stays `tab:<UPPERCASE NAME>`. Added: `tab:MODEL`, `tab:ASSEMBLE`, `tab:INSERT`, `tab:INSPECT`, `tab:MANAGE`, `tab:VIEW`. SURFACE and RENDER retain their names; SKETCH and SHEET METAL are available only in context. Legacy aliases remain: `tab:SOLID` and `tab:PLASTIC` → MODEL; `tab:MESH` → SURFACE; `tab:UTILITIES` → MANAGE. The Home and document-tab handles are unchanged.
|
||||
|
||||
Section labels retain `panel:<tab>:<section>` and collapsed sections retain `collapsed:<tab>:<section>`, with destinations from the tables above. In particular, `panel:SOLID:CREATE` becomes `panel:MODEL:CREATE`, selection-filter panels become `panel:VIEW:SELECT`, and `panel:SKETCH:CONSTRAINTS` becomes `panel:SKETCH:CONSTRAIN`. Command button and menu handles keep their command ids.
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
# step-conformance parts
|
||||
|
||||
STEP files of parts whose exact shape is known, generated from formulas by
|
||||
[step-conformance](https://github.com/dan335/step-conformance) (`corpus/generated`, commit
|
||||
96f30bb). The corpus is dedicated to the public domain under CC0 1.0.
|
||||
|
||||
`step_in::tests::step_conformance_parts_read_back_their_exact_shape` in `crates/kernel` reads
|
||||
each one and checks its volume against the generator's profile, its box and its face count.
|
||||
@@ -1,56 +0,0 @@
|
||||
ISO-10303-21;
|
||||
HEADER;
|
||||
FILE_DESCRIPTION(('step-conformance generated part'),'2;1');
|
||||
FILE_NAME('cone_point.step','2026-10-12T00:00:00',(''),(''),'step-conformance generate','step-conformance generate','');
|
||||
FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1=( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) );
|
||||
#2=( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) );
|
||||
#3=( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() );
|
||||
#4=UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-7),#1,'distance_accuracy_value','confusion accuracy');
|
||||
#5=( GEOMETRIC_REPRESENTATION_CONTEXT(3) GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#4)) GLOBAL_UNIT_ASSIGNED_CONTEXT((#1,#2,#3)) REPRESENTATION_CONTEXT('','3D') );
|
||||
#6=APPLICATION_CONTEXT('automotive design');
|
||||
#7=APPLICATION_PROTOCOL_DEFINITION('international standard','automotive_design',2000,#6);
|
||||
#8=PRODUCT_CONTEXT('',#6,'mechanical');
|
||||
#9=PRODUCT_DEFINITION_CONTEXT('part definition',#6,'design');
|
||||
#10=PRODUCT('cone_point','cone_point','',(#8));
|
||||
#11=PRODUCT_DEFINITION_FORMATION('','',#10);
|
||||
#12=PRODUCT_DEFINITION('design','',#11,#9);
|
||||
#13=PRODUCT_DEFINITION_SHAPE('','',#12);
|
||||
#14=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#15=DIRECTION('',(0.0,0.0,1.0));
|
||||
#16=DIRECTION('',(1.0,0.0,0.0));
|
||||
#17=AXIS2_PLACEMENT_3D('',#14,#15,#16);
|
||||
#18=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#19=DIRECTION('',(0.0,0.0,1.0));
|
||||
#20=DIRECTION('',(1.0,0.0,0.0));
|
||||
#21=AXIS2_PLACEMENT_3D('',#18,#19,#20);
|
||||
#22=CIRCLE('',#21,10.0);
|
||||
#23=CARTESIAN_POINT('',(10.0,0.0,0.0));
|
||||
#24=VERTEX_POINT('',#23);
|
||||
#25=EDGE_CURVE('',#24,#24,#22,.T.);
|
||||
#26=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#27=DIRECTION('',(0.0,0.0,1.0));
|
||||
#28=DIRECTION('',(1.0,0.0,0.0));
|
||||
#29=AXIS2_PLACEMENT_3D('',#26,#27,#28);
|
||||
#30=PLANE('',#29);
|
||||
#31=ORIENTED_EDGE('',*,*,#25,.F.);
|
||||
#32=EDGE_LOOP('',(#31));
|
||||
#33=FACE_OUTER_BOUND('',#32,.T.);
|
||||
#34=ADVANCED_FACE('',(#33),#30,.F.);
|
||||
#35=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#36=DIRECTION('',(0.0,0.0,-1.0));
|
||||
#37=DIRECTION('',(1.0,0.0,0.0));
|
||||
#38=AXIS2_PLACEMENT_3D('',#35,#36,#37);
|
||||
#39=CONICAL_SURFACE('',#38,10.0,0.3217505543966422);
|
||||
#40=ORIENTED_EDGE('',*,*,#25,.T.);
|
||||
#41=EDGE_LOOP('',(#40));
|
||||
#42=FACE_OUTER_BOUND('',#41,.T.);
|
||||
#43=ADVANCED_FACE('',(#42),#39,.T.);
|
||||
#44=CLOSED_SHELL('',(#34,#43));
|
||||
#45=MANIFOLD_SOLID_BREP('',#44);
|
||||
#46=ADVANCED_BREP_SHAPE_REPRESENTATION('',(#17,#45),#5);
|
||||
#47=SHAPE_DEFINITION_REPRESENTATION(#13,#46);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
@@ -1,60 +0,0 @@
|
||||
ISO-10303-21;
|
||||
HEADER;
|
||||
FILE_DESCRIPTION(('step-conformance generated part'),'2;1');
|
||||
FILE_NAME('cone_point_vertex.step','2026-10-12T00:00:00',(''),(''),'step-conformance generate','step-conformance generate','');
|
||||
FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1=( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) );
|
||||
#2=( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) );
|
||||
#3=( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() );
|
||||
#4=UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-7),#1,'distance_accuracy_value','confusion accuracy');
|
||||
#5=( GEOMETRIC_REPRESENTATION_CONTEXT(3) GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#4)) GLOBAL_UNIT_ASSIGNED_CONTEXT((#1,#2,#3)) REPRESENTATION_CONTEXT('','3D') );
|
||||
#6=APPLICATION_CONTEXT('automotive design');
|
||||
#7=APPLICATION_PROTOCOL_DEFINITION('international standard','automotive_design',2000,#6);
|
||||
#8=PRODUCT_CONTEXT('',#6,'mechanical');
|
||||
#9=PRODUCT_DEFINITION_CONTEXT('part definition',#6,'design');
|
||||
#10=PRODUCT('cone_point_vertex','cone_point_vertex','',(#8));
|
||||
#11=PRODUCT_DEFINITION_FORMATION('','',#10);
|
||||
#12=PRODUCT_DEFINITION('design','',#11,#9);
|
||||
#13=PRODUCT_DEFINITION_SHAPE('','',#12);
|
||||
#14=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#15=DIRECTION('',(0.0,0.0,1.0));
|
||||
#16=DIRECTION('',(1.0,0.0,0.0));
|
||||
#17=AXIS2_PLACEMENT_3D('',#14,#15,#16);
|
||||
#18=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#19=DIRECTION('',(0.0,0.0,1.0));
|
||||
#20=DIRECTION('',(1.0,0.0,0.0));
|
||||
#21=AXIS2_PLACEMENT_3D('',#18,#19,#20);
|
||||
#22=CIRCLE('',#21,10.0);
|
||||
#23=CARTESIAN_POINT('',(10.0,0.0,0.0));
|
||||
#24=VERTEX_POINT('',#23);
|
||||
#25=EDGE_CURVE('',#24,#24,#22,.T.);
|
||||
#26=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#27=DIRECTION('',(0.0,0.0,1.0));
|
||||
#28=DIRECTION('',(1.0,0.0,0.0));
|
||||
#29=AXIS2_PLACEMENT_3D('',#26,#27,#28);
|
||||
#30=PLANE('',#29);
|
||||
#31=ORIENTED_EDGE('',*,*,#25,.F.);
|
||||
#32=EDGE_LOOP('',(#31));
|
||||
#33=FACE_OUTER_BOUND('',#32,.T.);
|
||||
#34=ADVANCED_FACE('',(#33),#30,.F.);
|
||||
#35=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#36=DIRECTION('',(0.0,0.0,-1.0));
|
||||
#37=DIRECTION('',(1.0,0.0,0.0));
|
||||
#38=AXIS2_PLACEMENT_3D('',#35,#36,#37);
|
||||
#39=CONICAL_SURFACE('',#38,10.0,0.3217505543966422);
|
||||
#40=ORIENTED_EDGE('',*,*,#25,.T.);
|
||||
#41=EDGE_LOOP('',(#40));
|
||||
#42=CARTESIAN_POINT('',(0.0,0.0,30.0));
|
||||
#43=VERTEX_POINT('',#42);
|
||||
#44=VERTEX_LOOP('',#43);
|
||||
#45=FACE_OUTER_BOUND('',#41,.T.);
|
||||
#46=FACE_BOUND('',#44,.T.);
|
||||
#47=ADVANCED_FACE('',(#45,#46),#39,.T.);
|
||||
#48=CLOSED_SHELL('',(#34,#47));
|
||||
#49=MANIFOLD_SOLID_BREP('',#48);
|
||||
#50=ADVANCED_BREP_SHAPE_REPRESENTATION('',(#17,#49),#5);
|
||||
#51=SHAPE_DEFINITION_REPRESENTATION(#13,#50);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
@@ -1,76 +0,0 @@
|
||||
ISO-10303-21;
|
||||
HEADER;
|
||||
FILE_DESCRIPTION(('step-conformance generated part'),'2;1');
|
||||
FILE_NAME('cylinder.step','2026-10-12T00:00:00',(''),(''),'step-conformance generate','step-conformance generate','');
|
||||
FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1=( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) );
|
||||
#2=( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) );
|
||||
#3=( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() );
|
||||
#4=UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-7),#1,'distance_accuracy_value','confusion accuracy');
|
||||
#5=( GEOMETRIC_REPRESENTATION_CONTEXT(3) GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#4)) GLOBAL_UNIT_ASSIGNED_CONTEXT((#1,#2,#3)) REPRESENTATION_CONTEXT('','3D') );
|
||||
#6=APPLICATION_CONTEXT('automotive design');
|
||||
#7=APPLICATION_PROTOCOL_DEFINITION('international standard','automotive_design',2000,#6);
|
||||
#8=PRODUCT_CONTEXT('',#6,'mechanical');
|
||||
#9=PRODUCT_DEFINITION_CONTEXT('part definition',#6,'design');
|
||||
#10=PRODUCT('cylinder','cylinder','',(#8));
|
||||
#11=PRODUCT_DEFINITION_FORMATION('','',#10);
|
||||
#12=PRODUCT_DEFINITION('design','',#11,#9);
|
||||
#13=PRODUCT_DEFINITION_SHAPE('','',#12);
|
||||
#14=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#15=DIRECTION('',(0.0,0.0,1.0));
|
||||
#16=DIRECTION('',(1.0,0.0,0.0));
|
||||
#17=AXIS2_PLACEMENT_3D('',#14,#15,#16);
|
||||
#18=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#19=DIRECTION('',(0.0,0.0,1.0));
|
||||
#20=DIRECTION('',(1.0,0.0,0.0));
|
||||
#21=AXIS2_PLACEMENT_3D('',#18,#19,#20);
|
||||
#22=CIRCLE('',#21,10.0);
|
||||
#23=CARTESIAN_POINT('',(10.0,0.0,0.0));
|
||||
#24=VERTEX_POINT('',#23);
|
||||
#25=EDGE_CURVE('',#24,#24,#22,.T.);
|
||||
#26=CARTESIAN_POINT('',(0.0,0.0,30.0));
|
||||
#27=DIRECTION('',(0.0,0.0,1.0));
|
||||
#28=DIRECTION('',(1.0,0.0,0.0));
|
||||
#29=AXIS2_PLACEMENT_3D('',#26,#27,#28);
|
||||
#30=CIRCLE('',#29,10.0);
|
||||
#31=CARTESIAN_POINT('',(10.0,0.0,30.0));
|
||||
#32=VERTEX_POINT('',#31);
|
||||
#33=EDGE_CURVE('',#32,#32,#30,.T.);
|
||||
#34=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#35=DIRECTION('',(0.0,0.0,1.0));
|
||||
#36=DIRECTION('',(1.0,0.0,0.0));
|
||||
#37=AXIS2_PLACEMENT_3D('',#34,#35,#36);
|
||||
#38=PLANE('',#37);
|
||||
#39=ORIENTED_EDGE('',*,*,#25,.F.);
|
||||
#40=EDGE_LOOP('',(#39));
|
||||
#41=FACE_OUTER_BOUND('',#40,.T.);
|
||||
#42=ADVANCED_FACE('',(#41),#38,.F.);
|
||||
#43=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#44=DIRECTION('',(0.0,0.0,1.0));
|
||||
#45=DIRECTION('',(1.0,0.0,0.0));
|
||||
#46=AXIS2_PLACEMENT_3D('',#43,#44,#45);
|
||||
#47=CYLINDRICAL_SURFACE('',#46,10.0);
|
||||
#48=ORIENTED_EDGE('',*,*,#25,.T.);
|
||||
#49=EDGE_LOOP('',(#48));
|
||||
#50=ORIENTED_EDGE('',*,*,#33,.F.);
|
||||
#51=EDGE_LOOP('',(#50));
|
||||
#52=FACE_OUTER_BOUND('',#49,.T.);
|
||||
#53=FACE_BOUND('',#51,.T.);
|
||||
#54=ADVANCED_FACE('',(#52,#53),#47,.T.);
|
||||
#55=CARTESIAN_POINT('',(0.0,0.0,30.0));
|
||||
#56=DIRECTION('',(0.0,0.0,1.0));
|
||||
#57=DIRECTION('',(1.0,0.0,0.0));
|
||||
#58=AXIS2_PLACEMENT_3D('',#55,#56,#57);
|
||||
#59=PLANE('',#58);
|
||||
#60=ORIENTED_EDGE('',*,*,#33,.T.);
|
||||
#61=EDGE_LOOP('',(#60));
|
||||
#62=FACE_OUTER_BOUND('',#61,.T.);
|
||||
#63=ADVANCED_FACE('',(#62),#59,.T.);
|
||||
#64=CLOSED_SHELL('',(#42,#54,#63));
|
||||
#65=MANIFOLD_SOLID_BREP('',#64);
|
||||
#66=ADVANCED_BREP_SHAPE_REPRESENTATION('',(#17,#65),#5);
|
||||
#67=SHAPE_DEFINITION_REPRESENTATION(#13,#66);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
@@ -1,79 +0,0 @@
|
||||
ISO-10303-21;
|
||||
HEADER;
|
||||
FILE_DESCRIPTION(('step-conformance generated part'),'2;1');
|
||||
FILE_NAME('cylinder_inch.step','2026-10-12T00:00:00',(''),(''),'step-conformance generate','step-conformance generate','');
|
||||
FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1=( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) );
|
||||
#2=DIMENSIONAL_EXPONENTS(1.,0.,0.,0.,0.,0.,0.);
|
||||
#3=LENGTH_MEASURE_WITH_UNIT(LENGTH_MEASURE(25.4),#1);
|
||||
#4=( CONVERSION_BASED_UNIT('INCH',#3) LENGTH_UNIT() NAMED_UNIT(#2) );
|
||||
#5=( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) );
|
||||
#6=( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() );
|
||||
#7=UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(3.937007874015748E-9),#4,'distance_accuracy_value','confusion accuracy');
|
||||
#8=( GEOMETRIC_REPRESENTATION_CONTEXT(3) GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#7)) GLOBAL_UNIT_ASSIGNED_CONTEXT((#4,#5,#6)) REPRESENTATION_CONTEXT('','3D') );
|
||||
#9=APPLICATION_CONTEXT('automotive design');
|
||||
#10=APPLICATION_PROTOCOL_DEFINITION('international standard','automotive_design',2000,#9);
|
||||
#11=PRODUCT_CONTEXT('',#9,'mechanical');
|
||||
#12=PRODUCT_DEFINITION_CONTEXT('part definition',#9,'design');
|
||||
#13=PRODUCT('cylinder_inch','cylinder_inch','',(#11));
|
||||
#14=PRODUCT_DEFINITION_FORMATION('','',#13);
|
||||
#15=PRODUCT_DEFINITION('design','',#14,#12);
|
||||
#16=PRODUCT_DEFINITION_SHAPE('','',#15);
|
||||
#17=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#18=DIRECTION('',(0.0,0.0,1.0));
|
||||
#19=DIRECTION('',(1.0,0.0,0.0));
|
||||
#20=AXIS2_PLACEMENT_3D('',#17,#18,#19);
|
||||
#21=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#22=DIRECTION('',(0.0,0.0,1.0));
|
||||
#23=DIRECTION('',(1.0,0.0,0.0));
|
||||
#24=AXIS2_PLACEMENT_3D('',#21,#22,#23);
|
||||
#25=CIRCLE('',#24,0.3937007874015748);
|
||||
#26=CARTESIAN_POINT('',(0.3937007874015748,0.0,0.0));
|
||||
#27=VERTEX_POINT('',#26);
|
||||
#28=EDGE_CURVE('',#27,#27,#25,.T.);
|
||||
#29=CARTESIAN_POINT('',(0.0,0.0,1.1811023622047245));
|
||||
#30=DIRECTION('',(0.0,0.0,1.0));
|
||||
#31=DIRECTION('',(1.0,0.0,0.0));
|
||||
#32=AXIS2_PLACEMENT_3D('',#29,#30,#31);
|
||||
#33=CIRCLE('',#32,0.3937007874015748);
|
||||
#34=CARTESIAN_POINT('',(0.3937007874015748,0.0,1.1811023622047245));
|
||||
#35=VERTEX_POINT('',#34);
|
||||
#36=EDGE_CURVE('',#35,#35,#33,.T.);
|
||||
#37=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#38=DIRECTION('',(0.0,0.0,1.0));
|
||||
#39=DIRECTION('',(1.0,0.0,0.0));
|
||||
#40=AXIS2_PLACEMENT_3D('',#37,#38,#39);
|
||||
#41=PLANE('',#40);
|
||||
#42=ORIENTED_EDGE('',*,*,#28,.F.);
|
||||
#43=EDGE_LOOP('',(#42));
|
||||
#44=FACE_OUTER_BOUND('',#43,.T.);
|
||||
#45=ADVANCED_FACE('',(#44),#41,.F.);
|
||||
#46=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#47=DIRECTION('',(0.0,0.0,1.0));
|
||||
#48=DIRECTION('',(1.0,0.0,0.0));
|
||||
#49=AXIS2_PLACEMENT_3D('',#46,#47,#48);
|
||||
#50=CYLINDRICAL_SURFACE('',#49,0.3937007874015748);
|
||||
#51=ORIENTED_EDGE('',*,*,#28,.T.);
|
||||
#52=EDGE_LOOP('',(#51));
|
||||
#53=ORIENTED_EDGE('',*,*,#36,.F.);
|
||||
#54=EDGE_LOOP('',(#53));
|
||||
#55=FACE_OUTER_BOUND('',#52,.T.);
|
||||
#56=FACE_BOUND('',#54,.T.);
|
||||
#57=ADVANCED_FACE('',(#55,#56),#50,.T.);
|
||||
#58=CARTESIAN_POINT('',(0.0,0.0,1.1811023622047245));
|
||||
#59=DIRECTION('',(0.0,0.0,1.0));
|
||||
#60=DIRECTION('',(1.0,0.0,0.0));
|
||||
#61=AXIS2_PLACEMENT_3D('',#58,#59,#60);
|
||||
#62=PLANE('',#61);
|
||||
#63=ORIENTED_EDGE('',*,*,#36,.T.);
|
||||
#64=EDGE_LOOP('',(#63));
|
||||
#65=FACE_OUTER_BOUND('',#64,.T.);
|
||||
#66=ADVANCED_FACE('',(#65),#62,.T.);
|
||||
#67=CLOSED_SHELL('',(#45,#57,#66));
|
||||
#68=MANIFOLD_SOLID_BREP('',#67);
|
||||
#69=ADVANCED_BREP_SHAPE_REPRESENTATION('',(#20,#68),#8);
|
||||
#70=SHAPE_DEFINITION_REPRESENTATION(#16,#69);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
@@ -1,100 +0,0 @@
|
||||
ISO-10303-21;
|
||||
HEADER;
|
||||
FILE_DESCRIPTION(('step-conformance generated part'),'2;1');
|
||||
FILE_NAME('cylinder_nurbs.step','2026-10-12T00:00:00',(''),(''),'step-conformance generate','step-conformance generate','');
|
||||
FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1=( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) );
|
||||
#2=( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) );
|
||||
#3=( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() );
|
||||
#4=UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-7),#1,'distance_accuracy_value','confusion accuracy');
|
||||
#5=( GEOMETRIC_REPRESENTATION_CONTEXT(3) GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#4)) GLOBAL_UNIT_ASSIGNED_CONTEXT((#1,#2,#3)) REPRESENTATION_CONTEXT('','3D') );
|
||||
#6=APPLICATION_CONTEXT('automotive design');
|
||||
#7=APPLICATION_PROTOCOL_DEFINITION('international standard','automotive_design',2000,#6);
|
||||
#8=PRODUCT_CONTEXT('',#6,'mechanical');
|
||||
#9=PRODUCT_DEFINITION_CONTEXT('part definition',#6,'design');
|
||||
#10=PRODUCT('cylinder_nurbs','cylinder_nurbs','',(#8));
|
||||
#11=PRODUCT_DEFINITION_FORMATION('','',#10);
|
||||
#12=PRODUCT_DEFINITION('design','',#11,#9);
|
||||
#13=PRODUCT_DEFINITION_SHAPE('','',#12);
|
||||
#14=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#15=DIRECTION('',(0.0,0.0,1.0));
|
||||
#16=DIRECTION('',(1.0,0.0,0.0));
|
||||
#17=AXIS2_PLACEMENT_3D('',#14,#15,#16);
|
||||
#18=CARTESIAN_POINT('',(10.0,0.0,0.0));
|
||||
#19=CARTESIAN_POINT('',(10.000000000000002,10.0,0.0));
|
||||
#20=CARTESIAN_POINT('',(6.123233995736766E-16,10.0,0.0));
|
||||
#21=CARTESIAN_POINT('',(-10.0,10.000000000000002,0.0));
|
||||
#22=CARTESIAN_POINT('',(-10.0,1.2246467991473533E-15,0.0));
|
||||
#23=CARTESIAN_POINT('',(-10.000000000000002,-10.0,0.0));
|
||||
#24=CARTESIAN_POINT('',(-1.8369701987210296E-15,-10.0,0.0));
|
||||
#25=CARTESIAN_POINT('',(9.999999999999998,-10.000000000000002,0.0));
|
||||
#26=CARTESIAN_POINT('',(10.0,-2.4492935982947065E-15,0.0));
|
||||
#27=( BOUNDED_CURVE() B_SPLINE_CURVE(2,(#18,#19,#20,#21,#22,#23,#24,#25,#26),.UNSPECIFIED.,.T.,.F.) B_SPLINE_CURVE_WITH_KNOTS((3,2,2,2,3),(0.0,0.25,0.5,0.75,1.0),.UNSPECIFIED.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() RATIONAL_B_SPLINE_CURVE((1.0,0.7071067811865476,1.0,0.7071067811865476,1.0,0.7071067811865476,1.0,0.7071067811865476,1.0)) REPRESENTATION_ITEM('') );
|
||||
#28=CARTESIAN_POINT('',(10.0,0.0,0.0));
|
||||
#29=VERTEX_POINT('',#28);
|
||||
#30=EDGE_CURVE('',#29,#29,#27,.T.);
|
||||
#31=CARTESIAN_POINT('',(10.0,0.0,30.0));
|
||||
#32=CARTESIAN_POINT('',(10.000000000000002,10.0,30.0));
|
||||
#33=CARTESIAN_POINT('',(6.123233995736766E-16,10.0,30.0));
|
||||
#34=CARTESIAN_POINT('',(-10.0,10.000000000000002,30.0));
|
||||
#35=CARTESIAN_POINT('',(-10.0,1.2246467991473533E-15,30.0));
|
||||
#36=CARTESIAN_POINT('',(-10.000000000000002,-10.0,30.0));
|
||||
#37=CARTESIAN_POINT('',(-1.8369701987210296E-15,-10.0,30.0));
|
||||
#38=CARTESIAN_POINT('',(9.999999999999998,-10.000000000000002,30.0));
|
||||
#39=CARTESIAN_POINT('',(10.0,-2.4492935982947065E-15,30.0));
|
||||
#40=( BOUNDED_CURVE() B_SPLINE_CURVE(2,(#31,#32,#33,#34,#35,#36,#37,#38,#39),.UNSPECIFIED.,.T.,.F.) B_SPLINE_CURVE_WITH_KNOTS((3,2,2,2,3),(0.0,0.25,0.5,0.75,1.0),.UNSPECIFIED.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() RATIONAL_B_SPLINE_CURVE((1.0,0.7071067811865476,1.0,0.7071067811865476,1.0,0.7071067811865476,1.0,0.7071067811865476,1.0)) REPRESENTATION_ITEM('') );
|
||||
#41=CARTESIAN_POINT('',(10.0,0.0,30.0));
|
||||
#42=VERTEX_POINT('',#41);
|
||||
#43=EDGE_CURVE('',#42,#42,#40,.T.);
|
||||
#44=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#45=DIRECTION('',(0.0,0.0,1.0));
|
||||
#46=DIRECTION('',(1.0,0.0,0.0));
|
||||
#47=AXIS2_PLACEMENT_3D('',#44,#45,#46);
|
||||
#48=PLANE('',#47);
|
||||
#49=ORIENTED_EDGE('',*,*,#30,.F.);
|
||||
#50=EDGE_LOOP('',(#49));
|
||||
#51=FACE_OUTER_BOUND('',#50,.T.);
|
||||
#52=ADVANCED_FACE('',(#51),#48,.F.);
|
||||
#53=CARTESIAN_POINT('',(10.0,0.0,0.0));
|
||||
#54=CARTESIAN_POINT('',(10.0,0.0,30.0));
|
||||
#55=CARTESIAN_POINT('',(10.000000000000002,10.0,0.0));
|
||||
#56=CARTESIAN_POINT('',(10.000000000000002,10.0,30.0));
|
||||
#57=CARTESIAN_POINT('',(6.123233995736766E-16,10.0,0.0));
|
||||
#58=CARTESIAN_POINT('',(6.123233995736766E-16,10.0,30.0));
|
||||
#59=CARTESIAN_POINT('',(-10.0,10.000000000000002,0.0));
|
||||
#60=CARTESIAN_POINT('',(-10.0,10.000000000000002,30.0));
|
||||
#61=CARTESIAN_POINT('',(-10.0,1.2246467991473533E-15,0.0));
|
||||
#62=CARTESIAN_POINT('',(-10.0,1.2246467991473533E-15,30.0));
|
||||
#63=CARTESIAN_POINT('',(-10.000000000000002,-10.0,0.0));
|
||||
#64=CARTESIAN_POINT('',(-10.000000000000002,-10.0,30.0));
|
||||
#65=CARTESIAN_POINT('',(-1.8369701987210296E-15,-10.0,0.0));
|
||||
#66=CARTESIAN_POINT('',(-1.8369701987210296E-15,-10.0,30.0));
|
||||
#67=CARTESIAN_POINT('',(9.999999999999998,-10.000000000000002,0.0));
|
||||
#68=CARTESIAN_POINT('',(9.999999999999998,-10.000000000000002,30.0));
|
||||
#69=CARTESIAN_POINT('',(10.0,-2.4492935982947065E-15,0.0));
|
||||
#70=CARTESIAN_POINT('',(10.0,-2.4492935982947065E-15,30.0));
|
||||
#71=( BOUNDED_SURFACE() B_SPLINE_SURFACE(2,1,((#53,#54),(#55,#56),(#57,#58),(#59,#60),(#61,#62),(#63,#64),(#65,#66),(#67,#68),(#69,#70)),.UNSPECIFIED.,.T.,.F.,.F.) B_SPLINE_SURFACE_WITH_KNOTS((3,2,2,2,3),(2,2),(0.0,0.25,0.5,0.75,1.0),(0.0,1.0),.UNSPECIFIED.) GEOMETRIC_REPRESENTATION_ITEM() RATIONAL_B_SPLINE_SURFACE(((1.0,1.0),(0.7071067811865476,0.7071067811865476),(1.0,1.0),(0.7071067811865476,0.7071067811865476),(1.0,1.0),(0.7071067811865476,0.7071067811865476),(1.0,1.0),(0.7071067811865476,0.7071067811865476),(1.0,1.0))) REPRESENTATION_ITEM('') SURFACE() );
|
||||
#72=ORIENTED_EDGE('',*,*,#30,.T.);
|
||||
#73=EDGE_LOOP('',(#72));
|
||||
#74=ORIENTED_EDGE('',*,*,#43,.F.);
|
||||
#75=EDGE_LOOP('',(#74));
|
||||
#76=FACE_OUTER_BOUND('',#73,.T.);
|
||||
#77=FACE_BOUND('',#75,.T.);
|
||||
#78=ADVANCED_FACE('',(#76,#77),#71,.T.);
|
||||
#79=CARTESIAN_POINT('',(0.0,0.0,30.0));
|
||||
#80=DIRECTION('',(0.0,0.0,1.0));
|
||||
#81=DIRECTION('',(1.0,0.0,0.0));
|
||||
#82=AXIS2_PLACEMENT_3D('',#79,#80,#81);
|
||||
#83=PLANE('',#82);
|
||||
#84=ORIENTED_EDGE('',*,*,#43,.T.);
|
||||
#85=EDGE_LOOP('',(#84));
|
||||
#86=FACE_OUTER_BOUND('',#85,.T.);
|
||||
#87=ADVANCED_FACE('',(#86),#83,.T.);
|
||||
#88=CLOSED_SHELL('',(#52,#78,#87));
|
||||
#89=MANIFOLD_SOLID_BREP('',#88);
|
||||
#90=ADVANCED_BREP_SHAPE_REPRESENTATION('',(#17,#89),#5);
|
||||
#91=SHAPE_DEFINITION_REPRESENTATION(#13,#90);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
@@ -1,99 +0,0 @@
|
||||
ISO-10303-21;
|
||||
HEADER;
|
||||
FILE_DESCRIPTION(('step-conformance generated part'),'2;1');
|
||||
FILE_NAME('cylinder_placed.step','2026-10-12T00:00:00',(''),(''),'step-conformance generate','step-conformance generate','');
|
||||
FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1=( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) );
|
||||
#2=( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) );
|
||||
#3=( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() );
|
||||
#4=UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-7),#1,'distance_accuracy_value','confusion accuracy');
|
||||
#5=( GEOMETRIC_REPRESENTATION_CONTEXT(3) GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#4)) GLOBAL_UNIT_ASSIGNED_CONTEXT((#1,#2,#3)) REPRESENTATION_CONTEXT('','3D') );
|
||||
#6=APPLICATION_CONTEXT('automotive design');
|
||||
#7=APPLICATION_PROTOCOL_DEFINITION('international standard','automotive_design',2000,#6);
|
||||
#8=PRODUCT_CONTEXT('',#6,'mechanical');
|
||||
#9=PRODUCT_DEFINITION_CONTEXT('part definition',#6,'design');
|
||||
#10=PRODUCT('cylinder_placed assembly','cylinder_placed assembly','',(#8));
|
||||
#11=PRODUCT_DEFINITION_FORMATION('','',#10);
|
||||
#12=PRODUCT_DEFINITION('design','',#11,#9);
|
||||
#13=PRODUCT_DEFINITION_SHAPE('','',#12);
|
||||
#14=APPLICATION_CONTEXT('automotive design');
|
||||
#15=APPLICATION_PROTOCOL_DEFINITION('international standard','automotive_design',2000,#14);
|
||||
#16=PRODUCT_CONTEXT('',#14,'mechanical');
|
||||
#17=PRODUCT_DEFINITION_CONTEXT('part definition',#14,'design');
|
||||
#18=PRODUCT('cylinder_placed','cylinder_placed','',(#16));
|
||||
#19=PRODUCT_DEFINITION_FORMATION('','',#18);
|
||||
#20=PRODUCT_DEFINITION('design','',#19,#17);
|
||||
#21=PRODUCT_DEFINITION_SHAPE('','',#20);
|
||||
#22=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#23=DIRECTION('',(0.0,0.0,1.0));
|
||||
#24=DIRECTION('',(1.0,0.0,0.0));
|
||||
#25=AXIS2_PLACEMENT_3D('',#22,#23,#24);
|
||||
#26=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#27=DIRECTION('',(0.0,0.0,1.0));
|
||||
#28=DIRECTION('',(1.0,0.0,0.0));
|
||||
#29=AXIS2_PLACEMENT_3D('',#26,#27,#28);
|
||||
#30=CIRCLE('',#29,10.0);
|
||||
#31=CARTESIAN_POINT('',(10.0,0.0,0.0));
|
||||
#32=VERTEX_POINT('',#31);
|
||||
#33=EDGE_CURVE('',#32,#32,#30,.T.);
|
||||
#34=CARTESIAN_POINT('',(0.0,0.0,30.0));
|
||||
#35=DIRECTION('',(0.0,0.0,1.0));
|
||||
#36=DIRECTION('',(1.0,0.0,0.0));
|
||||
#37=AXIS2_PLACEMENT_3D('',#34,#35,#36);
|
||||
#38=CIRCLE('',#37,10.0);
|
||||
#39=CARTESIAN_POINT('',(10.0,0.0,30.0));
|
||||
#40=VERTEX_POINT('',#39);
|
||||
#41=EDGE_CURVE('',#40,#40,#38,.T.);
|
||||
#42=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#43=DIRECTION('',(0.0,0.0,1.0));
|
||||
#44=DIRECTION('',(1.0,0.0,0.0));
|
||||
#45=AXIS2_PLACEMENT_3D('',#42,#43,#44);
|
||||
#46=PLANE('',#45);
|
||||
#47=ORIENTED_EDGE('',*,*,#33,.F.);
|
||||
#48=EDGE_LOOP('',(#47));
|
||||
#49=FACE_OUTER_BOUND('',#48,.T.);
|
||||
#50=ADVANCED_FACE('',(#49),#46,.F.);
|
||||
#51=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#52=DIRECTION('',(0.0,0.0,1.0));
|
||||
#53=DIRECTION('',(1.0,0.0,0.0));
|
||||
#54=AXIS2_PLACEMENT_3D('',#51,#52,#53);
|
||||
#55=CYLINDRICAL_SURFACE('',#54,10.0);
|
||||
#56=ORIENTED_EDGE('',*,*,#33,.T.);
|
||||
#57=EDGE_LOOP('',(#56));
|
||||
#58=ORIENTED_EDGE('',*,*,#41,.F.);
|
||||
#59=EDGE_LOOP('',(#58));
|
||||
#60=FACE_OUTER_BOUND('',#57,.T.);
|
||||
#61=FACE_BOUND('',#59,.T.);
|
||||
#62=ADVANCED_FACE('',(#60,#61),#55,.T.);
|
||||
#63=CARTESIAN_POINT('',(0.0,0.0,30.0));
|
||||
#64=DIRECTION('',(0.0,0.0,1.0));
|
||||
#65=DIRECTION('',(1.0,0.0,0.0));
|
||||
#66=AXIS2_PLACEMENT_3D('',#63,#64,#65);
|
||||
#67=PLANE('',#66);
|
||||
#68=ORIENTED_EDGE('',*,*,#41,.T.);
|
||||
#69=EDGE_LOOP('',(#68));
|
||||
#70=FACE_OUTER_BOUND('',#69,.T.);
|
||||
#71=ADVANCED_FACE('',(#70),#67,.T.);
|
||||
#72=CLOSED_SHELL('',(#50,#62,#71));
|
||||
#73=MANIFOLD_SOLID_BREP('',#72);
|
||||
#74=ADVANCED_BREP_SHAPE_REPRESENTATION('',(#73,#25),#5);
|
||||
#75=SHAPE_DEFINITION_REPRESENTATION(#21,#74);
|
||||
#76=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#77=DIRECTION('',(0.0,0.0,1.0));
|
||||
#78=DIRECTION('',(1.0,0.0,0.0));
|
||||
#79=AXIS2_PLACEMENT_3D('',#76,#77,#78);
|
||||
#80=CARTESIAN_POINT('',(40.0,-25.0,10.0));
|
||||
#81=DIRECTION('',(0.5773502691896258,0.5773502691896258,0.5773502691896258));
|
||||
#82=DIRECTION('',(0.7071067811865476,-0.7071067811865476,0.0));
|
||||
#83=AXIS2_PLACEMENT_3D('',#80,#81,#82);
|
||||
#84=SHAPE_REPRESENTATION('',(#79,#83),#5);
|
||||
#85=SHAPE_DEFINITION_REPRESENTATION(#13,#84);
|
||||
#86=NEXT_ASSEMBLY_USAGE_OCCURRENCE('1','cylinder_placed','',#12,#20,$);
|
||||
#87=PRODUCT_DEFINITION_SHAPE('','',#86);
|
||||
#88=ITEM_DEFINED_TRANSFORMATION('','',#25,#83);
|
||||
#89=( REPRESENTATION_RELATIONSHIP('','',#74,#84) REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#88) SHAPE_REPRESENTATION_RELATIONSHIP() );
|
||||
#90=CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#89,#87);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
@@ -1,81 +0,0 @@
|
||||
ISO-10303-21;
|
||||
HEADER;
|
||||
FILE_DESCRIPTION(('step-conformance generated part'),'2;1');
|
||||
FILE_NAME('cylinder_seam.step','2026-10-12T00:00:00',(''),(''),'step-conformance generate','step-conformance generate','');
|
||||
FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1=( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) );
|
||||
#2=( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) );
|
||||
#3=( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() );
|
||||
#4=UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-7),#1,'distance_accuracy_value','confusion accuracy');
|
||||
#5=( GEOMETRIC_REPRESENTATION_CONTEXT(3) GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#4)) GLOBAL_UNIT_ASSIGNED_CONTEXT((#1,#2,#3)) REPRESENTATION_CONTEXT('','3D') );
|
||||
#6=APPLICATION_CONTEXT('automotive design');
|
||||
#7=APPLICATION_PROTOCOL_DEFINITION('international standard','automotive_design',2000,#6);
|
||||
#8=PRODUCT_CONTEXT('',#6,'mechanical');
|
||||
#9=PRODUCT_DEFINITION_CONTEXT('part definition',#6,'design');
|
||||
#10=PRODUCT('cylinder_seam','cylinder_seam','',(#8));
|
||||
#11=PRODUCT_DEFINITION_FORMATION('','',#10);
|
||||
#12=PRODUCT_DEFINITION('design','',#11,#9);
|
||||
#13=PRODUCT_DEFINITION_SHAPE('','',#12);
|
||||
#14=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#15=DIRECTION('',(0.0,0.0,1.0));
|
||||
#16=DIRECTION('',(1.0,0.0,0.0));
|
||||
#17=AXIS2_PLACEMENT_3D('',#14,#15,#16);
|
||||
#18=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#19=DIRECTION('',(0.0,0.0,1.0));
|
||||
#20=DIRECTION('',(1.0,0.0,0.0));
|
||||
#21=AXIS2_PLACEMENT_3D('',#18,#19,#20);
|
||||
#22=CIRCLE('',#21,10.0);
|
||||
#23=CARTESIAN_POINT('',(10.0,0.0,0.0));
|
||||
#24=VERTEX_POINT('',#23);
|
||||
#25=EDGE_CURVE('',#24,#24,#22,.T.);
|
||||
#26=CARTESIAN_POINT('',(0.0,0.0,30.0));
|
||||
#27=DIRECTION('',(0.0,0.0,1.0));
|
||||
#28=DIRECTION('',(1.0,0.0,0.0));
|
||||
#29=AXIS2_PLACEMENT_3D('',#26,#27,#28);
|
||||
#30=CIRCLE('',#29,10.0);
|
||||
#31=CARTESIAN_POINT('',(10.0,0.0,30.0));
|
||||
#32=VERTEX_POINT('',#31);
|
||||
#33=EDGE_CURVE('',#32,#32,#30,.T.);
|
||||
#34=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#35=DIRECTION('',(0.0,0.0,1.0));
|
||||
#36=DIRECTION('',(1.0,0.0,0.0));
|
||||
#37=AXIS2_PLACEMENT_3D('',#34,#35,#36);
|
||||
#38=PLANE('',#37);
|
||||
#39=ORIENTED_EDGE('',*,*,#25,.F.);
|
||||
#40=EDGE_LOOP('',(#39));
|
||||
#41=FACE_OUTER_BOUND('',#40,.T.);
|
||||
#42=ADVANCED_FACE('',(#41),#38,.F.);
|
||||
#43=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#44=DIRECTION('',(0.0,0.0,1.0));
|
||||
#45=DIRECTION('',(1.0,0.0,0.0));
|
||||
#46=AXIS2_PLACEMENT_3D('',#43,#44,#45);
|
||||
#47=CYLINDRICAL_SURFACE('',#46,10.0);
|
||||
#48=CARTESIAN_POINT('',(10.0,0.0,0.0));
|
||||
#49=DIRECTION('',(0.0,0.0,1.0));
|
||||
#50=VECTOR('',#49,30.0);
|
||||
#51=LINE('',#48,#50);
|
||||
#52=EDGE_CURVE('',#24,#32,#51,.T.);
|
||||
#53=ORIENTED_EDGE('',*,*,#25,.T.);
|
||||
#54=ORIENTED_EDGE('',*,*,#52,.T.);
|
||||
#55=ORIENTED_EDGE('',*,*,#33,.F.);
|
||||
#56=ORIENTED_EDGE('',*,*,#52,.F.);
|
||||
#57=EDGE_LOOP('',(#53,#54,#55,#56));
|
||||
#58=FACE_OUTER_BOUND('',#57,.T.);
|
||||
#59=ADVANCED_FACE('',(#58),#47,.T.);
|
||||
#60=CARTESIAN_POINT('',(0.0,0.0,30.0));
|
||||
#61=DIRECTION('',(0.0,0.0,1.0));
|
||||
#62=DIRECTION('',(1.0,0.0,0.0));
|
||||
#63=AXIS2_PLACEMENT_3D('',#60,#61,#62);
|
||||
#64=PLANE('',#63);
|
||||
#65=ORIENTED_EDGE('',*,*,#33,.T.);
|
||||
#66=EDGE_LOOP('',(#65));
|
||||
#67=FACE_OUTER_BOUND('',#66,.T.);
|
||||
#68=ADVANCED_FACE('',(#67),#64,.T.);
|
||||
#69=CLOSED_SHELL('',(#42,#59,#68));
|
||||
#70=MANIFOLD_SOLID_BREP('',#69);
|
||||
#71=ADVANCED_BREP_SHAPE_REPRESENTATION('',(#17,#70),#5);
|
||||
#72=SHAPE_DEFINITION_REPRESENTATION(#13,#71);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
@@ -1,108 +0,0 @@
|
||||
ISO-10303-21;
|
||||
HEADER;
|
||||
FILE_DESCRIPTION(('step-conformance generated part'),'2;1');
|
||||
FILE_NAME('cylinder_twice.step','2026-10-12T00:00:00',(''),(''),'step-conformance generate','step-conformance generate','');
|
||||
FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1=( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) );
|
||||
#2=( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) );
|
||||
#3=( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() );
|
||||
#4=UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-7),#1,'distance_accuracy_value','confusion accuracy');
|
||||
#5=( GEOMETRIC_REPRESENTATION_CONTEXT(3) GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#4)) GLOBAL_UNIT_ASSIGNED_CONTEXT((#1,#2,#3)) REPRESENTATION_CONTEXT('','3D') );
|
||||
#6=APPLICATION_CONTEXT('automotive design');
|
||||
#7=APPLICATION_PROTOCOL_DEFINITION('international standard','automotive_design',2000,#6);
|
||||
#8=PRODUCT_CONTEXT('',#6,'mechanical');
|
||||
#9=PRODUCT_DEFINITION_CONTEXT('part definition',#6,'design');
|
||||
#10=PRODUCT('cylinder_twice assembly','cylinder_twice assembly','',(#8));
|
||||
#11=PRODUCT_DEFINITION_FORMATION('','',#10);
|
||||
#12=PRODUCT_DEFINITION('design','',#11,#9);
|
||||
#13=PRODUCT_DEFINITION_SHAPE('','',#12);
|
||||
#14=APPLICATION_CONTEXT('automotive design');
|
||||
#15=APPLICATION_PROTOCOL_DEFINITION('international standard','automotive_design',2000,#14);
|
||||
#16=PRODUCT_CONTEXT('',#14,'mechanical');
|
||||
#17=PRODUCT_DEFINITION_CONTEXT('part definition',#14,'design');
|
||||
#18=PRODUCT('cylinder_twice','cylinder_twice','',(#16));
|
||||
#19=PRODUCT_DEFINITION_FORMATION('','',#18);
|
||||
#20=PRODUCT_DEFINITION('design','',#19,#17);
|
||||
#21=PRODUCT_DEFINITION_SHAPE('','',#20);
|
||||
#22=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#23=DIRECTION('',(0.0,0.0,1.0));
|
||||
#24=DIRECTION('',(1.0,0.0,0.0));
|
||||
#25=AXIS2_PLACEMENT_3D('',#22,#23,#24);
|
||||
#26=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#27=DIRECTION('',(0.0,0.0,1.0));
|
||||
#28=DIRECTION('',(1.0,0.0,0.0));
|
||||
#29=AXIS2_PLACEMENT_3D('',#26,#27,#28);
|
||||
#30=CIRCLE('',#29,10.0);
|
||||
#31=CARTESIAN_POINT('',(10.0,0.0,0.0));
|
||||
#32=VERTEX_POINT('',#31);
|
||||
#33=EDGE_CURVE('',#32,#32,#30,.T.);
|
||||
#34=CARTESIAN_POINT('',(0.0,0.0,30.0));
|
||||
#35=DIRECTION('',(0.0,0.0,1.0));
|
||||
#36=DIRECTION('',(1.0,0.0,0.0));
|
||||
#37=AXIS2_PLACEMENT_3D('',#34,#35,#36);
|
||||
#38=CIRCLE('',#37,10.0);
|
||||
#39=CARTESIAN_POINT('',(10.0,0.0,30.0));
|
||||
#40=VERTEX_POINT('',#39);
|
||||
#41=EDGE_CURVE('',#40,#40,#38,.T.);
|
||||
#42=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#43=DIRECTION('',(0.0,0.0,1.0));
|
||||
#44=DIRECTION('',(1.0,0.0,0.0));
|
||||
#45=AXIS2_PLACEMENT_3D('',#42,#43,#44);
|
||||
#46=PLANE('',#45);
|
||||
#47=ORIENTED_EDGE('',*,*,#33,.F.);
|
||||
#48=EDGE_LOOP('',(#47));
|
||||
#49=FACE_OUTER_BOUND('',#48,.T.);
|
||||
#50=ADVANCED_FACE('',(#49),#46,.F.);
|
||||
#51=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#52=DIRECTION('',(0.0,0.0,1.0));
|
||||
#53=DIRECTION('',(1.0,0.0,0.0));
|
||||
#54=AXIS2_PLACEMENT_3D('',#51,#52,#53);
|
||||
#55=CYLINDRICAL_SURFACE('',#54,10.0);
|
||||
#56=ORIENTED_EDGE('',*,*,#33,.T.);
|
||||
#57=EDGE_LOOP('',(#56));
|
||||
#58=ORIENTED_EDGE('',*,*,#41,.F.);
|
||||
#59=EDGE_LOOP('',(#58));
|
||||
#60=FACE_OUTER_BOUND('',#57,.T.);
|
||||
#61=FACE_BOUND('',#59,.T.);
|
||||
#62=ADVANCED_FACE('',(#60,#61),#55,.T.);
|
||||
#63=CARTESIAN_POINT('',(0.0,0.0,30.0));
|
||||
#64=DIRECTION('',(0.0,0.0,1.0));
|
||||
#65=DIRECTION('',(1.0,0.0,0.0));
|
||||
#66=AXIS2_PLACEMENT_3D('',#63,#64,#65);
|
||||
#67=PLANE('',#66);
|
||||
#68=ORIENTED_EDGE('',*,*,#41,.T.);
|
||||
#69=EDGE_LOOP('',(#68));
|
||||
#70=FACE_OUTER_BOUND('',#69,.T.);
|
||||
#71=ADVANCED_FACE('',(#70),#67,.T.);
|
||||
#72=CLOSED_SHELL('',(#50,#62,#71));
|
||||
#73=MANIFOLD_SOLID_BREP('',#72);
|
||||
#74=ADVANCED_BREP_SHAPE_REPRESENTATION('',(#73,#25),#5);
|
||||
#75=SHAPE_DEFINITION_REPRESENTATION(#21,#74);
|
||||
#76=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#77=DIRECTION('',(0.0,0.0,1.0));
|
||||
#78=DIRECTION('',(1.0,0.0,0.0));
|
||||
#79=AXIS2_PLACEMENT_3D('',#76,#77,#78);
|
||||
#80=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#81=DIRECTION('',(0.0,0.0,1.0));
|
||||
#82=DIRECTION('',(1.0,0.0,0.0));
|
||||
#83=AXIS2_PLACEMENT_3D('',#80,#81,#82);
|
||||
#84=CARTESIAN_POINT('',(60.0,0.0,10.0));
|
||||
#85=DIRECTION('',(1.0,0.0,0.0));
|
||||
#86=DIRECTION('',(0.0,1.0,0.0));
|
||||
#87=AXIS2_PLACEMENT_3D('',#84,#85,#86);
|
||||
#88=SHAPE_REPRESENTATION('',(#79,#83,#87),#5);
|
||||
#89=SHAPE_DEFINITION_REPRESENTATION(#13,#88);
|
||||
#90=NEXT_ASSEMBLY_USAGE_OCCURRENCE('1','cylinder_twice','',#12,#20,$);
|
||||
#91=PRODUCT_DEFINITION_SHAPE('','',#90);
|
||||
#92=ITEM_DEFINED_TRANSFORMATION('','',#25,#83);
|
||||
#93=( REPRESENTATION_RELATIONSHIP('','',#74,#88) REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#92) SHAPE_REPRESENTATION_RELATIONSHIP() );
|
||||
#94=CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#93,#91);
|
||||
#95=NEXT_ASSEMBLY_USAGE_OCCURRENCE('2','cylinder_twice','',#12,#20,$);
|
||||
#96=PRODUCT_DEFINITION_SHAPE('','',#95);
|
||||
#97=ITEM_DEFINED_TRANSFORMATION('','',#25,#87);
|
||||
#98=( REPRESENTATION_RELATIONSHIP('','',#74,#88) REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#97) SHAPE_REPRESENTATION_RELATIONSHIP() );
|
||||
#99=CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#98,#96);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
@@ -1,56 +0,0 @@
|
||||
ISO-10303-21;
|
||||
HEADER;
|
||||
FILE_DESCRIPTION(('step-conformance generated part'),'2;1');
|
||||
FILE_NAME('dome.step','2026-10-12T00:00:00',(''),(''),'step-conformance generate','step-conformance generate','');
|
||||
FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1=( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) );
|
||||
#2=( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) );
|
||||
#3=( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() );
|
||||
#4=UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-7),#1,'distance_accuracy_value','confusion accuracy');
|
||||
#5=( GEOMETRIC_REPRESENTATION_CONTEXT(3) GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#4)) GLOBAL_UNIT_ASSIGNED_CONTEXT((#1,#2,#3)) REPRESENTATION_CONTEXT('','3D') );
|
||||
#6=APPLICATION_CONTEXT('automotive design');
|
||||
#7=APPLICATION_PROTOCOL_DEFINITION('international standard','automotive_design',2000,#6);
|
||||
#8=PRODUCT_CONTEXT('',#6,'mechanical');
|
||||
#9=PRODUCT_DEFINITION_CONTEXT('part definition',#6,'design');
|
||||
#10=PRODUCT('dome','dome','',(#8));
|
||||
#11=PRODUCT_DEFINITION_FORMATION('','',#10);
|
||||
#12=PRODUCT_DEFINITION('design','',#11,#9);
|
||||
#13=PRODUCT_DEFINITION_SHAPE('','',#12);
|
||||
#14=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#15=DIRECTION('',(0.0,0.0,1.0));
|
||||
#16=DIRECTION('',(1.0,0.0,0.0));
|
||||
#17=AXIS2_PLACEMENT_3D('',#14,#15,#16);
|
||||
#18=CARTESIAN_POINT('',(0.0,0.0,1.2246467991473533E-15));
|
||||
#19=DIRECTION('',(0.0,0.0,1.0));
|
||||
#20=DIRECTION('',(1.0,0.0,0.0));
|
||||
#21=AXIS2_PLACEMENT_3D('',#18,#19,#20);
|
||||
#22=CIRCLE('',#21,20.0);
|
||||
#23=CARTESIAN_POINT('',(20.0,0.0,1.2246467991473533E-15));
|
||||
#24=VERTEX_POINT('',#23);
|
||||
#25=EDGE_CURVE('',#24,#24,#22,.T.);
|
||||
#26=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#27=DIRECTION('',(0.0,0.0,1.0));
|
||||
#28=DIRECTION('',(1.0,0.0,0.0));
|
||||
#29=AXIS2_PLACEMENT_3D('',#26,#27,#28);
|
||||
#30=PLANE('',#29);
|
||||
#31=ORIENTED_EDGE('',*,*,#25,.F.);
|
||||
#32=EDGE_LOOP('',(#31));
|
||||
#33=FACE_OUTER_BOUND('',#32,.T.);
|
||||
#34=ADVANCED_FACE('',(#33),#30,.F.);
|
||||
#35=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#36=DIRECTION('',(0.0,0.0,1.0));
|
||||
#37=DIRECTION('',(1.0,0.0,0.0));
|
||||
#38=AXIS2_PLACEMENT_3D('',#35,#36,#37);
|
||||
#39=SPHERICAL_SURFACE('',#38,20.0);
|
||||
#40=ORIENTED_EDGE('',*,*,#25,.T.);
|
||||
#41=EDGE_LOOP('',(#40));
|
||||
#42=FACE_OUTER_BOUND('',#41,.T.);
|
||||
#43=ADVANCED_FACE('',(#42),#39,.T.);
|
||||
#44=CLOSED_SHELL('',(#34,#43));
|
||||
#45=MANIFOLD_SOLID_BREP('',#44);
|
||||
#46=ADVANCED_BREP_SHAPE_REPRESENTATION('',(#17,#45),#5);
|
||||
#47=SHAPE_DEFINITION_REPRESENTATION(#13,#46);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
@@ -1,84 +0,0 @@
|
||||
ISO-10303-21;
|
||||
HEADER;
|
||||
FILE_DESCRIPTION(('step-conformance generated part'),'2;1');
|
||||
FILE_NAME('dome_nurbs.step','2026-10-12T00:00:00',(''),(''),'step-conformance generate','step-conformance generate','');
|
||||
FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1=( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) );
|
||||
#2=( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) );
|
||||
#3=( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() );
|
||||
#4=UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-7),#1,'distance_accuracy_value','confusion accuracy');
|
||||
#5=( GEOMETRIC_REPRESENTATION_CONTEXT(3) GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#4)) GLOBAL_UNIT_ASSIGNED_CONTEXT((#1,#2,#3)) REPRESENTATION_CONTEXT('','3D') );
|
||||
#6=APPLICATION_CONTEXT('automotive design');
|
||||
#7=APPLICATION_PROTOCOL_DEFINITION('international standard','automotive_design',2000,#6);
|
||||
#8=PRODUCT_CONTEXT('',#6,'mechanical');
|
||||
#9=PRODUCT_DEFINITION_CONTEXT('part definition',#6,'design');
|
||||
#10=PRODUCT('dome_nurbs','dome_nurbs','',(#8));
|
||||
#11=PRODUCT_DEFINITION_FORMATION('','',#10);
|
||||
#12=PRODUCT_DEFINITION('design','',#11,#9);
|
||||
#13=PRODUCT_DEFINITION_SHAPE('','',#12);
|
||||
#14=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#15=DIRECTION('',(0.0,0.0,1.0));
|
||||
#16=DIRECTION('',(1.0,0.0,0.0));
|
||||
#17=AXIS2_PLACEMENT_3D('',#14,#15,#16);
|
||||
#18=CARTESIAN_POINT('',(20.0,0.0,1.2246467991473533E-15));
|
||||
#19=CARTESIAN_POINT('',(20.000000000000004,20.0,1.2246467991473533E-15));
|
||||
#20=CARTESIAN_POINT('',(1.2246467991473533E-15,20.0,1.2246467991473533E-15));
|
||||
#21=CARTESIAN_POINT('',(-20.0,20.000000000000004,1.2246467991473533E-15));
|
||||
#22=CARTESIAN_POINT('',(-20.0,2.4492935982947065E-15,1.2246467991473533E-15));
|
||||
#23=CARTESIAN_POINT('',(-20.000000000000004,-20.0,1.2246467991473533E-15));
|
||||
#24=CARTESIAN_POINT('',(-3.673940397442059E-15,-20.0,1.2246467991473533E-15));
|
||||
#25=CARTESIAN_POINT('',(19.999999999999996,-20.000000000000004,1.2246467991473533E-15));
|
||||
#26=CARTESIAN_POINT('',(20.0,-4.898587196589413E-15,1.2246467991473533E-15));
|
||||
#27=( BOUNDED_CURVE() B_SPLINE_CURVE(2,(#18,#19,#20,#21,#22,#23,#24,#25,#26),.UNSPECIFIED.,.T.,.F.) B_SPLINE_CURVE_WITH_KNOTS((3,2,2,2,3),(0.0,0.25,0.5,0.75,1.0),.UNSPECIFIED.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() RATIONAL_B_SPLINE_CURVE((1.0,0.7071067811865476,1.0,0.7071067811865476,1.0,0.7071067811865476,1.0,0.7071067811865476,1.0)) REPRESENTATION_ITEM('') );
|
||||
#28=CARTESIAN_POINT('',(20.0,0.0,1.2246467991473533E-15));
|
||||
#29=VERTEX_POINT('',#28);
|
||||
#30=EDGE_CURVE('',#29,#29,#27,.T.);
|
||||
#31=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#32=DIRECTION('',(0.0,0.0,1.0));
|
||||
#33=DIRECTION('',(1.0,0.0,0.0));
|
||||
#34=AXIS2_PLACEMENT_3D('',#31,#32,#33);
|
||||
#35=PLANE('',#34);
|
||||
#36=ORIENTED_EDGE('',*,*,#30,.F.);
|
||||
#37=EDGE_LOOP('',(#36));
|
||||
#38=FACE_OUTER_BOUND('',#37,.T.);
|
||||
#39=ADVANCED_FACE('',(#38),#35,.F.);
|
||||
#40=CARTESIAN_POINT('',(20.0,0.0,1.2246467991473533E-15));
|
||||
#41=CARTESIAN_POINT('',(19.999999999999996,0.0,20.0));
|
||||
#42=CARTESIAN_POINT('',(0.0,0.0,20.0));
|
||||
#43=CARTESIAN_POINT('',(20.000000000000004,20.0,1.2246467991473533E-15));
|
||||
#44=CARTESIAN_POINT('',(20.0,19.999999999999996,20.0));
|
||||
#45=CARTESIAN_POINT('',(0.0,0.0,20.0));
|
||||
#46=CARTESIAN_POINT('',(1.2246467991473533E-15,20.0,1.2246467991473533E-15));
|
||||
#47=CARTESIAN_POINT('',(1.224646799147353E-15,19.999999999999996,20.0));
|
||||
#48=CARTESIAN_POINT('',(0.0,0.0,20.0));
|
||||
#49=CARTESIAN_POINT('',(-20.0,20.000000000000004,1.2246467991473533E-15));
|
||||
#50=CARTESIAN_POINT('',(-19.999999999999996,20.0,20.0));
|
||||
#51=CARTESIAN_POINT('',(0.0,0.0,20.0));
|
||||
#52=CARTESIAN_POINT('',(-20.0,2.4492935982947065E-15,1.2246467991473533E-15));
|
||||
#53=CARTESIAN_POINT('',(-19.999999999999996,2.449293598294706E-15,20.0));
|
||||
#54=CARTESIAN_POINT('',(0.0,0.0,20.0));
|
||||
#55=CARTESIAN_POINT('',(-20.000000000000004,-20.0,1.2246467991473533E-15));
|
||||
#56=CARTESIAN_POINT('',(-20.0,-19.999999999999996,20.0));
|
||||
#57=CARTESIAN_POINT('',(0.0,0.0,20.0));
|
||||
#58=CARTESIAN_POINT('',(-3.673940397442059E-15,-20.0,1.2246467991473533E-15));
|
||||
#59=CARTESIAN_POINT('',(-3.673940397442058E-15,-19.999999999999996,20.0));
|
||||
#60=CARTESIAN_POINT('',(0.0,0.0,20.0));
|
||||
#61=CARTESIAN_POINT('',(19.999999999999996,-20.000000000000004,1.2246467991473533E-15));
|
||||
#62=CARTESIAN_POINT('',(19.999999999999993,-20.0,20.0));
|
||||
#63=CARTESIAN_POINT('',(0.0,0.0,20.0));
|
||||
#64=CARTESIAN_POINT('',(20.0,-4.898587196589413E-15,1.2246467991473533E-15));
|
||||
#65=CARTESIAN_POINT('',(19.999999999999996,-4.898587196589412E-15,20.0));
|
||||
#66=CARTESIAN_POINT('',(0.0,0.0,20.0));
|
||||
#67=( BOUNDED_SURFACE() B_SPLINE_SURFACE(2,2,((#40,#41,#42),(#43,#44,#45),(#46,#47,#48),(#49,#50,#51),(#52,#53,#54),(#55,#56,#57),(#58,#59,#60),(#61,#62,#63),(#64,#65,#66)),.UNSPECIFIED.,.T.,.F.,.F.) B_SPLINE_SURFACE_WITH_KNOTS((3,2,2,2,3),(3,3),(0.0,0.25,0.5,0.75,1.0),(0.0,1.0),.UNSPECIFIED.) GEOMETRIC_REPRESENTATION_ITEM() RATIONAL_B_SPLINE_SURFACE(((1.0,0.7071067811865476,1.0),(0.7071067811865476,0.5000000000000001,0.7071067811865476),(1.0,0.7071067811865476,1.0),(0.7071067811865476,0.5000000000000001,0.7071067811865476),(1.0,0.7071067811865476,1.0),(0.7071067811865476,0.5000000000000001,0.7071067811865476),(1.0,0.7071067811865476,1.0),(0.7071067811865476,0.5000000000000001,0.7071067811865476),(1.0,0.7071067811865476,1.0))) REPRESENTATION_ITEM('') SURFACE() );
|
||||
#68=ORIENTED_EDGE('',*,*,#30,.T.);
|
||||
#69=EDGE_LOOP('',(#68));
|
||||
#70=FACE_OUTER_BOUND('',#69,.T.);
|
||||
#71=ADVANCED_FACE('',(#70),#67,.T.);
|
||||
#72=CLOSED_SHELL('',(#39,#71));
|
||||
#73=MANIFOLD_SOLID_BREP('',#72);
|
||||
#74=ADVANCED_BREP_SHAPE_REPRESENTATION('',(#17,#73),#5);
|
||||
#75=SHAPE_DEFINITION_REPRESENTATION(#13,#74);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
@@ -1,76 +0,0 @@
|
||||
ISO-10303-21;
|
||||
HEADER;
|
||||
FILE_DESCRIPTION(('step-conformance generated part'),'2;1');
|
||||
FILE_NAME('far_cylinder.step','2026-10-12T00:00:00',(''),(''),'step-conformance generate','step-conformance generate','');
|
||||
FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1=( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) );
|
||||
#2=( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) );
|
||||
#3=( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() );
|
||||
#4=UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-7),#1,'distance_accuracy_value','confusion accuracy');
|
||||
#5=( GEOMETRIC_REPRESENTATION_CONTEXT(3) GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#4)) GLOBAL_UNIT_ASSIGNED_CONTEXT((#1,#2,#3)) REPRESENTATION_CONTEXT('','3D') );
|
||||
#6=APPLICATION_CONTEXT('automotive design');
|
||||
#7=APPLICATION_PROTOCOL_DEFINITION('international standard','automotive_design',2000,#6);
|
||||
#8=PRODUCT_CONTEXT('',#6,'mechanical');
|
||||
#9=PRODUCT_DEFINITION_CONTEXT('part definition',#6,'design');
|
||||
#10=PRODUCT('far_cylinder','far_cylinder','',(#8));
|
||||
#11=PRODUCT_DEFINITION_FORMATION('','',#10);
|
||||
#12=PRODUCT_DEFINITION('design','',#11,#9);
|
||||
#13=PRODUCT_DEFINITION_SHAPE('','',#12);
|
||||
#14=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#15=DIRECTION('',(0.0,0.0,1.0));
|
||||
#16=DIRECTION('',(1.0,0.0,0.0));
|
||||
#17=AXIS2_PLACEMENT_3D('',#14,#15,#16);
|
||||
#18=CARTESIAN_POINT('',(100000.0,0.0,0.0));
|
||||
#19=DIRECTION('',(0.0,0.0,1.0));
|
||||
#20=DIRECTION('',(1.0,0.0,0.0));
|
||||
#21=AXIS2_PLACEMENT_3D('',#18,#19,#20);
|
||||
#22=CIRCLE('',#21,10.0);
|
||||
#23=CARTESIAN_POINT('',(100010.0,0.0,0.0));
|
||||
#24=VERTEX_POINT('',#23);
|
||||
#25=EDGE_CURVE('',#24,#24,#22,.T.);
|
||||
#26=CARTESIAN_POINT('',(100000.0,0.0,30.0));
|
||||
#27=DIRECTION('',(0.0,0.0,1.0));
|
||||
#28=DIRECTION('',(1.0,0.0,0.0));
|
||||
#29=AXIS2_PLACEMENT_3D('',#26,#27,#28);
|
||||
#30=CIRCLE('',#29,10.0);
|
||||
#31=CARTESIAN_POINT('',(100010.0,0.0,30.0));
|
||||
#32=VERTEX_POINT('',#31);
|
||||
#33=EDGE_CURVE('',#32,#32,#30,.T.);
|
||||
#34=CARTESIAN_POINT('',(100000.0,0.0,0.0));
|
||||
#35=DIRECTION('',(0.0,0.0,1.0));
|
||||
#36=DIRECTION('',(1.0,0.0,0.0));
|
||||
#37=AXIS2_PLACEMENT_3D('',#34,#35,#36);
|
||||
#38=PLANE('',#37);
|
||||
#39=ORIENTED_EDGE('',*,*,#25,.F.);
|
||||
#40=EDGE_LOOP('',(#39));
|
||||
#41=FACE_OUTER_BOUND('',#40,.T.);
|
||||
#42=ADVANCED_FACE('',(#41),#38,.F.);
|
||||
#43=CARTESIAN_POINT('',(100000.0,0.0,0.0));
|
||||
#44=DIRECTION('',(0.0,0.0,1.0));
|
||||
#45=DIRECTION('',(1.0,0.0,0.0));
|
||||
#46=AXIS2_PLACEMENT_3D('',#43,#44,#45);
|
||||
#47=CYLINDRICAL_SURFACE('',#46,10.0);
|
||||
#48=ORIENTED_EDGE('',*,*,#25,.T.);
|
||||
#49=EDGE_LOOP('',(#48));
|
||||
#50=ORIENTED_EDGE('',*,*,#33,.F.);
|
||||
#51=EDGE_LOOP('',(#50));
|
||||
#52=FACE_OUTER_BOUND('',#49,.T.);
|
||||
#53=FACE_BOUND('',#51,.T.);
|
||||
#54=ADVANCED_FACE('',(#52,#53),#47,.T.);
|
||||
#55=CARTESIAN_POINT('',(100000.0,0.0,30.0));
|
||||
#56=DIRECTION('',(0.0,0.0,1.0));
|
||||
#57=DIRECTION('',(1.0,0.0,0.0));
|
||||
#58=AXIS2_PLACEMENT_3D('',#55,#56,#57);
|
||||
#59=PLANE('',#58);
|
||||
#60=ORIENTED_EDGE('',*,*,#33,.T.);
|
||||
#61=EDGE_LOOP('',(#60));
|
||||
#62=FACE_OUTER_BOUND('',#61,.T.);
|
||||
#63=ADVANCED_FACE('',(#62),#59,.T.);
|
||||
#64=CLOSED_SHELL('',(#42,#54,#63));
|
||||
#65=MANIFOLD_SOLID_BREP('',#64);
|
||||
#66=ADVANCED_BREP_SHAPE_REPRESENTATION('',(#17,#65),#5);
|
||||
#67=SHAPE_DEFINITION_REPRESENTATION(#13,#66);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -1,76 +0,0 @@
|
||||
ISO-10303-21;
|
||||
HEADER;
|
||||
FILE_DESCRIPTION(('step-conformance generated part'),'2;1');
|
||||
FILE_NAME('sheet_disc.step','2026-10-12T00:00:00',(''),(''),'step-conformance generate','step-conformance generate','');
|
||||
FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1=( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) );
|
||||
#2=( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) );
|
||||
#3=( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() );
|
||||
#4=UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-7),#1,'distance_accuracy_value','confusion accuracy');
|
||||
#5=( GEOMETRIC_REPRESENTATION_CONTEXT(3) GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#4)) GLOBAL_UNIT_ASSIGNED_CONTEXT((#1,#2,#3)) REPRESENTATION_CONTEXT('','3D') );
|
||||
#6=APPLICATION_CONTEXT('automotive design');
|
||||
#7=APPLICATION_PROTOCOL_DEFINITION('international standard','automotive_design',2000,#6);
|
||||
#8=PRODUCT_CONTEXT('',#6,'mechanical');
|
||||
#9=PRODUCT_DEFINITION_CONTEXT('part definition',#6,'design');
|
||||
#10=PRODUCT('sheet_disc','sheet_disc','',(#8));
|
||||
#11=PRODUCT_DEFINITION_FORMATION('','',#10);
|
||||
#12=PRODUCT_DEFINITION('design','',#11,#9);
|
||||
#13=PRODUCT_DEFINITION_SHAPE('','',#12);
|
||||
#14=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#15=DIRECTION('',(0.0,0.0,1.0));
|
||||
#16=DIRECTION('',(1.0,0.0,0.0));
|
||||
#17=AXIS2_PLACEMENT_3D('',#14,#15,#16);
|
||||
#18=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#19=DIRECTION('',(0.0,0.0,1.0));
|
||||
#20=DIRECTION('',(1.0,0.0,0.0));
|
||||
#21=AXIS2_PLACEMENT_3D('',#18,#19,#20);
|
||||
#22=CIRCLE('',#21,1800.0);
|
||||
#23=CARTESIAN_POINT('',(1800.0,0.0,0.0));
|
||||
#24=VERTEX_POINT('',#23);
|
||||
#25=EDGE_CURVE('',#24,#24,#22,.T.);
|
||||
#26=CARTESIAN_POINT('',(0.0,0.0,19.0));
|
||||
#27=DIRECTION('',(0.0,0.0,1.0));
|
||||
#28=DIRECTION('',(1.0,0.0,0.0));
|
||||
#29=AXIS2_PLACEMENT_3D('',#26,#27,#28);
|
||||
#30=CIRCLE('',#29,1800.0);
|
||||
#31=CARTESIAN_POINT('',(1800.0,0.0,19.0));
|
||||
#32=VERTEX_POINT('',#31);
|
||||
#33=EDGE_CURVE('',#32,#32,#30,.T.);
|
||||
#34=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#35=DIRECTION('',(0.0,0.0,1.0));
|
||||
#36=DIRECTION('',(1.0,0.0,0.0));
|
||||
#37=AXIS2_PLACEMENT_3D('',#34,#35,#36);
|
||||
#38=PLANE('',#37);
|
||||
#39=ORIENTED_EDGE('',*,*,#25,.F.);
|
||||
#40=EDGE_LOOP('',(#39));
|
||||
#41=FACE_OUTER_BOUND('',#40,.T.);
|
||||
#42=ADVANCED_FACE('',(#41),#38,.F.);
|
||||
#43=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#44=DIRECTION('',(0.0,0.0,1.0));
|
||||
#45=DIRECTION('',(1.0,0.0,0.0));
|
||||
#46=AXIS2_PLACEMENT_3D('',#43,#44,#45);
|
||||
#47=CYLINDRICAL_SURFACE('',#46,1800.0);
|
||||
#48=ORIENTED_EDGE('',*,*,#25,.T.);
|
||||
#49=EDGE_LOOP('',(#48));
|
||||
#50=ORIENTED_EDGE('',*,*,#33,.F.);
|
||||
#51=EDGE_LOOP('',(#50));
|
||||
#52=FACE_OUTER_BOUND('',#49,.T.);
|
||||
#53=FACE_BOUND('',#51,.T.);
|
||||
#54=ADVANCED_FACE('',(#52,#53),#47,.T.);
|
||||
#55=CARTESIAN_POINT('',(0.0,0.0,19.0));
|
||||
#56=DIRECTION('',(0.0,0.0,1.0));
|
||||
#57=DIRECTION('',(1.0,0.0,0.0));
|
||||
#58=AXIS2_PLACEMENT_3D('',#55,#56,#57);
|
||||
#59=PLANE('',#58);
|
||||
#60=ORIENTED_EDGE('',*,*,#33,.T.);
|
||||
#61=EDGE_LOOP('',(#60));
|
||||
#62=FACE_OUTER_BOUND('',#61,.T.);
|
||||
#63=ADVANCED_FACE('',(#62),#59,.T.);
|
||||
#64=CLOSED_SHELL('',(#42,#54,#63));
|
||||
#65=MANIFOLD_SOLID_BREP('',#64);
|
||||
#66=ADVANCED_BREP_SHAPE_REPRESENTATION('',(#17,#65),#5);
|
||||
#67=SHAPE_DEFINITION_REPRESENTATION(#13,#66);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
@@ -1,76 +0,0 @@
|
||||
ISO-10303-21;
|
||||
HEADER;
|
||||
FILE_DESCRIPTION(('step-conformance generated part'),'2;1');
|
||||
FILE_NAME('tiny_pin.step','2026-10-12T00:00:00',(''),(''),'step-conformance generate','step-conformance generate','');
|
||||
FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1=( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) );
|
||||
#2=( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) );
|
||||
#3=( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() );
|
||||
#4=UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-7),#1,'distance_accuracy_value','confusion accuracy');
|
||||
#5=( GEOMETRIC_REPRESENTATION_CONTEXT(3) GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#4)) GLOBAL_UNIT_ASSIGNED_CONTEXT((#1,#2,#3)) REPRESENTATION_CONTEXT('','3D') );
|
||||
#6=APPLICATION_CONTEXT('automotive design');
|
||||
#7=APPLICATION_PROTOCOL_DEFINITION('international standard','automotive_design',2000,#6);
|
||||
#8=PRODUCT_CONTEXT('',#6,'mechanical');
|
||||
#9=PRODUCT_DEFINITION_CONTEXT('part definition',#6,'design');
|
||||
#10=PRODUCT('tiny_pin','tiny_pin','',(#8));
|
||||
#11=PRODUCT_DEFINITION_FORMATION('','',#10);
|
||||
#12=PRODUCT_DEFINITION('design','',#11,#9);
|
||||
#13=PRODUCT_DEFINITION_SHAPE('','',#12);
|
||||
#14=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#15=DIRECTION('',(0.0,0.0,1.0));
|
||||
#16=DIRECTION('',(1.0,0.0,0.0));
|
||||
#17=AXIS2_PLACEMENT_3D('',#14,#15,#16);
|
||||
#18=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#19=DIRECTION('',(0.0,0.0,1.0));
|
||||
#20=DIRECTION('',(1.0,0.0,0.0));
|
||||
#21=AXIS2_PLACEMENT_3D('',#18,#19,#20);
|
||||
#22=CIRCLE('',#21,0.25);
|
||||
#23=CARTESIAN_POINT('',(0.25,0.0,0.0));
|
||||
#24=VERTEX_POINT('',#23);
|
||||
#25=EDGE_CURVE('',#24,#24,#22,.T.);
|
||||
#26=CARTESIAN_POINT('',(0.0,0.0,2.0));
|
||||
#27=DIRECTION('',(0.0,0.0,1.0));
|
||||
#28=DIRECTION('',(1.0,0.0,0.0));
|
||||
#29=AXIS2_PLACEMENT_3D('',#26,#27,#28);
|
||||
#30=CIRCLE('',#29,0.25);
|
||||
#31=CARTESIAN_POINT('',(0.25,0.0,2.0));
|
||||
#32=VERTEX_POINT('',#31);
|
||||
#33=EDGE_CURVE('',#32,#32,#30,.T.);
|
||||
#34=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#35=DIRECTION('',(0.0,0.0,1.0));
|
||||
#36=DIRECTION('',(1.0,0.0,0.0));
|
||||
#37=AXIS2_PLACEMENT_3D('',#34,#35,#36);
|
||||
#38=PLANE('',#37);
|
||||
#39=ORIENTED_EDGE('',*,*,#25,.F.);
|
||||
#40=EDGE_LOOP('',(#39));
|
||||
#41=FACE_OUTER_BOUND('',#40,.T.);
|
||||
#42=ADVANCED_FACE('',(#41),#38,.F.);
|
||||
#43=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#44=DIRECTION('',(0.0,0.0,1.0));
|
||||
#45=DIRECTION('',(1.0,0.0,0.0));
|
||||
#46=AXIS2_PLACEMENT_3D('',#43,#44,#45);
|
||||
#47=CYLINDRICAL_SURFACE('',#46,0.25);
|
||||
#48=ORIENTED_EDGE('',*,*,#25,.T.);
|
||||
#49=EDGE_LOOP('',(#48));
|
||||
#50=ORIENTED_EDGE('',*,*,#33,.F.);
|
||||
#51=EDGE_LOOP('',(#50));
|
||||
#52=FACE_OUTER_BOUND('',#49,.T.);
|
||||
#53=FACE_BOUND('',#51,.T.);
|
||||
#54=ADVANCED_FACE('',(#52,#53),#47,.T.);
|
||||
#55=CARTESIAN_POINT('',(0.0,0.0,2.0));
|
||||
#56=DIRECTION('',(0.0,0.0,1.0));
|
||||
#57=DIRECTION('',(1.0,0.0,0.0));
|
||||
#58=AXIS2_PLACEMENT_3D('',#55,#56,#57);
|
||||
#59=PLANE('',#58);
|
||||
#60=ORIENTED_EDGE('',*,*,#33,.T.);
|
||||
#61=EDGE_LOOP('',(#60));
|
||||
#62=FACE_OUTER_BOUND('',#61,.T.);
|
||||
#63=ADVANCED_FACE('',(#62),#59,.T.);
|
||||
#64=CLOSED_SHELL('',(#42,#54,#63));
|
||||
#65=MANIFOLD_SOLID_BREP('',#64);
|
||||
#66=ADVANCED_BREP_SHAPE_REPRESENTATION('',(#17,#65),#5);
|
||||
#67=SHAPE_DEFINITION_REPRESENTATION(#13,#66);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
@@ -1,121 +0,0 @@
|
||||
ISO-10303-21;
|
||||
HEADER;
|
||||
FILE_DESCRIPTION(('step-conformance generated part'),'2;1');
|
||||
FILE_NAME('torus_ring_nurbs.step','2026-10-12T00:00:00',(''),(''),'step-conformance generate','step-conformance generate','');
|
||||
FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1=( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) );
|
||||
#2=( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) );
|
||||
#3=( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() );
|
||||
#4=UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-7),#1,'distance_accuracy_value','confusion accuracy');
|
||||
#5=( GEOMETRIC_REPRESENTATION_CONTEXT(3) GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#4)) GLOBAL_UNIT_ASSIGNED_CONTEXT((#1,#2,#3)) REPRESENTATION_CONTEXT('','3D') );
|
||||
#6=APPLICATION_CONTEXT('automotive design');
|
||||
#7=APPLICATION_PROTOCOL_DEFINITION('international standard','automotive_design',2000,#6);
|
||||
#8=PRODUCT_CONTEXT('',#6,'mechanical');
|
||||
#9=PRODUCT_DEFINITION_CONTEXT('part definition',#6,'design');
|
||||
#10=PRODUCT('torus_ring_nurbs','torus_ring_nurbs','',(#8));
|
||||
#11=PRODUCT_DEFINITION_FORMATION('','',#10);
|
||||
#12=PRODUCT_DEFINITION('design','',#11,#9);
|
||||
#13=PRODUCT_DEFINITION_SHAPE('','',#12);
|
||||
#14=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#15=DIRECTION('',(0.0,0.0,1.0));
|
||||
#16=DIRECTION('',(1.0,0.0,0.0));
|
||||
#17=AXIS2_PLACEMENT_3D('',#14,#15,#16);
|
||||
#18=CARTESIAN_POINT('',(15.0,0.0,0.0));
|
||||
#19=CARTESIAN_POINT('',(15.000000000000004,15.0,0.0));
|
||||
#20=CARTESIAN_POINT('',(9.18485099360515E-16,15.0,0.0));
|
||||
#21=CARTESIAN_POINT('',(-15.0,15.000000000000004,0.0));
|
||||
#22=CARTESIAN_POINT('',(-15.0,1.83697019872103E-15,0.0));
|
||||
#23=CARTESIAN_POINT('',(-15.000000000000004,-15.0,0.0));
|
||||
#24=CARTESIAN_POINT('',(-2.7554552980815444E-15,-15.0,0.0));
|
||||
#25=CARTESIAN_POINT('',(14.999999999999996,-15.000000000000004,0.0));
|
||||
#26=CARTESIAN_POINT('',(15.0,-3.67394039744206E-15,0.0));
|
||||
#27=( BOUNDED_CURVE() B_SPLINE_CURVE(2,(#18,#19,#20,#21,#22,#23,#24,#25,#26),.UNSPECIFIED.,.T.,.F.) B_SPLINE_CURVE_WITH_KNOTS((3,2,2,2,3),(0.0,0.25,0.5,0.75,1.0),.UNSPECIFIED.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() RATIONAL_B_SPLINE_CURVE((1.0,0.7071067811865476,1.0,0.7071067811865476,1.0,0.7071067811865476,1.0,0.7071067811865476,1.0)) REPRESENTATION_ITEM('') );
|
||||
#28=CARTESIAN_POINT('',(15.0,0.0,0.0));
|
||||
#29=VERTEX_POINT('',#28);
|
||||
#30=EDGE_CURVE('',#29,#29,#27,.T.);
|
||||
#31=CARTESIAN_POINT('',(25.0,0.0,3.061616997868383E-16));
|
||||
#32=CARTESIAN_POINT('',(25.000000000000007,25.0,3.061616997868383E-16));
|
||||
#33=CARTESIAN_POINT('',(1.5308084989341915E-15,25.0,3.061616997868383E-16));
|
||||
#34=CARTESIAN_POINT('',(-25.0,25.000000000000007,3.061616997868383E-16));
|
||||
#35=CARTESIAN_POINT('',(-25.0,3.061616997868383E-15,3.061616997868383E-16));
|
||||
#36=CARTESIAN_POINT('',(-25.000000000000007,-25.0,3.061616997868383E-16));
|
||||
#37=CARTESIAN_POINT('',(-4.592425496802574E-15,-25.0,3.061616997868383E-16));
|
||||
#38=CARTESIAN_POINT('',(24.999999999999993,-25.000000000000007,3.061616997868383E-16));
|
||||
#39=CARTESIAN_POINT('',(25.0,-6.123233995736766E-15,3.061616997868383E-16));
|
||||
#40=( BOUNDED_CURVE() B_SPLINE_CURVE(2,(#31,#32,#33,#34,#35,#36,#37,#38,#39),.UNSPECIFIED.,.T.,.F.) B_SPLINE_CURVE_WITH_KNOTS((3,2,2,2,3),(0.0,0.25,0.5,0.75,1.0),.UNSPECIFIED.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() RATIONAL_B_SPLINE_CURVE((1.0,0.7071067811865476,1.0,0.7071067811865476,1.0,0.7071067811865476,1.0,0.7071067811865476,1.0)) REPRESENTATION_ITEM('') );
|
||||
#41=CARTESIAN_POINT('',(25.0,0.0,3.061616997868383E-16));
|
||||
#42=VERTEX_POINT('',#41);
|
||||
#43=EDGE_CURVE('',#42,#42,#40,.T.);
|
||||
#44=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#45=DIRECTION('',(0.0,0.0,1.0));
|
||||
#46=DIRECTION('',(1.0,0.0,0.0));
|
||||
#47=AXIS2_PLACEMENT_3D('',#44,#45,#46);
|
||||
#48=PLANE('',#47);
|
||||
#49=ORIENTED_EDGE('',*,*,#30,.T.);
|
||||
#50=EDGE_LOOP('',(#49));
|
||||
#51=ORIENTED_EDGE('',*,*,#43,.F.);
|
||||
#52=EDGE_LOOP('',(#51));
|
||||
#53=FACE_OUTER_BOUND('',#52,.T.);
|
||||
#54=FACE_BOUND('',#50,.T.);
|
||||
#55=ADVANCED_FACE('',(#53,#54),#48,.F.);
|
||||
#56=CARTESIAN_POINT('',(25.0,0.0,3.061616997868383E-16));
|
||||
#57=CARTESIAN_POINT('',(25.0,0.0,5.0));
|
||||
#58=CARTESIAN_POINT('',(20.0,0.0,5.0));
|
||||
#59=CARTESIAN_POINT('',(15.0,0.0,5.0));
|
||||
#60=CARTESIAN_POINT('',(15.0,0.0,3.061616997868383E-16));
|
||||
#61=CARTESIAN_POINT('',(25.000000000000007,25.0,3.061616997868383E-16));
|
||||
#62=CARTESIAN_POINT('',(25.000000000000007,25.0,5.0));
|
||||
#63=CARTESIAN_POINT('',(20.000000000000004,20.0,5.0));
|
||||
#64=CARTESIAN_POINT('',(15.000000000000004,15.0,5.0));
|
||||
#65=CARTESIAN_POINT('',(15.000000000000004,15.0,3.061616997868383E-16));
|
||||
#66=CARTESIAN_POINT('',(1.5308084989341915E-15,25.0,3.061616997868383E-16));
|
||||
#67=CARTESIAN_POINT('',(1.5308084989341915E-15,25.0,5.0));
|
||||
#68=CARTESIAN_POINT('',(1.2246467991473533E-15,20.0,5.0));
|
||||
#69=CARTESIAN_POINT('',(9.18485099360515E-16,15.0,5.0));
|
||||
#70=CARTESIAN_POINT('',(9.18485099360515E-16,15.0,3.061616997868383E-16));
|
||||
#71=CARTESIAN_POINT('',(-25.0,25.000000000000007,3.061616997868383E-16));
|
||||
#72=CARTESIAN_POINT('',(-25.0,25.000000000000007,5.0));
|
||||
#73=CARTESIAN_POINT('',(-20.0,20.000000000000004,5.0));
|
||||
#74=CARTESIAN_POINT('',(-15.0,15.000000000000004,5.0));
|
||||
#75=CARTESIAN_POINT('',(-15.0,15.000000000000004,3.061616997868383E-16));
|
||||
#76=CARTESIAN_POINT('',(-25.0,3.061616997868383E-15,3.061616997868383E-16));
|
||||
#77=CARTESIAN_POINT('',(-25.0,3.061616997868383E-15,5.0));
|
||||
#78=CARTESIAN_POINT('',(-20.0,2.4492935982947065E-15,5.0));
|
||||
#79=CARTESIAN_POINT('',(-15.0,1.83697019872103E-15,5.0));
|
||||
#80=CARTESIAN_POINT('',(-15.0,1.83697019872103E-15,3.061616997868383E-16));
|
||||
#81=CARTESIAN_POINT('',(-25.000000000000007,-25.0,3.061616997868383E-16));
|
||||
#82=CARTESIAN_POINT('',(-25.000000000000007,-25.0,5.0));
|
||||
#83=CARTESIAN_POINT('',(-20.000000000000004,-20.0,5.0));
|
||||
#84=CARTESIAN_POINT('',(-15.000000000000004,-15.0,5.0));
|
||||
#85=CARTESIAN_POINT('',(-15.000000000000004,-15.0,3.061616997868383E-16));
|
||||
#86=CARTESIAN_POINT('',(-4.592425496802574E-15,-25.0,3.061616997868383E-16));
|
||||
#87=CARTESIAN_POINT('',(-4.592425496802574E-15,-25.0,5.0));
|
||||
#88=CARTESIAN_POINT('',(-3.673940397442059E-15,-20.0,5.0));
|
||||
#89=CARTESIAN_POINT('',(-2.7554552980815444E-15,-15.0,5.0));
|
||||
#90=CARTESIAN_POINT('',(-2.7554552980815444E-15,-15.0,3.061616997868383E-16));
|
||||
#91=CARTESIAN_POINT('',(24.999999999999993,-25.000000000000007,3.061616997868383E-16));
|
||||
#92=CARTESIAN_POINT('',(24.999999999999993,-25.000000000000007,5.0));
|
||||
#93=CARTESIAN_POINT('',(19.999999999999996,-20.000000000000004,5.0));
|
||||
#94=CARTESIAN_POINT('',(14.999999999999996,-15.000000000000004,5.0));
|
||||
#95=CARTESIAN_POINT('',(14.999999999999996,-15.000000000000004,3.061616997868383E-16));
|
||||
#96=CARTESIAN_POINT('',(25.0,-6.123233995736766E-15,3.061616997868383E-16));
|
||||
#97=CARTESIAN_POINT('',(25.0,-6.123233995736766E-15,5.0));
|
||||
#98=CARTESIAN_POINT('',(20.0,-4.898587196589413E-15,5.0));
|
||||
#99=CARTESIAN_POINT('',(15.0,-3.67394039744206E-15,5.0));
|
||||
#100=CARTESIAN_POINT('',(15.0,-3.67394039744206E-15,3.061616997868383E-16));
|
||||
#101=( BOUNDED_SURFACE() B_SPLINE_SURFACE(2,2,((#56,#57,#58,#59,#60),(#61,#62,#63,#64,#65),(#66,#67,#68,#69,#70),(#71,#72,#73,#74,#75),(#76,#77,#78,#79,#80),(#81,#82,#83,#84,#85),(#86,#87,#88,#89,#90),(#91,#92,#93,#94,#95),(#96,#97,#98,#99,#100)),.UNSPECIFIED.,.T.,.F.,.F.) B_SPLINE_SURFACE_WITH_KNOTS((3,2,2,2,3),(3,2,3),(0.0,0.25,0.5,0.75,1.0),(0.0,0.5,1.0),.UNSPECIFIED.) GEOMETRIC_REPRESENTATION_ITEM() RATIONAL_B_SPLINE_SURFACE(((1.0,0.7071067811865476,1.0,0.7071067811865476,1.0),(0.7071067811865476,0.5000000000000001,0.7071067811865476,0.5000000000000001,0.7071067811865476),(1.0,0.7071067811865476,1.0,0.7071067811865476,1.0),(0.7071067811865476,0.5000000000000001,0.7071067811865476,0.5000000000000001,0.7071067811865476),(1.0,0.7071067811865476,1.0,0.7071067811865476,1.0),(0.7071067811865476,0.5000000000000001,0.7071067811865476,0.5000000000000001,0.7071067811865476),(1.0,0.7071067811865476,1.0,0.7071067811865476,1.0),(0.7071067811865476,0.5000000000000001,0.7071067811865476,0.5000000000000001,0.7071067811865476),(1.0,0.7071067811865476,1.0,0.7071067811865476,1.0))) REPRESENTATION_ITEM('') SURFACE() );
|
||||
#102=ORIENTED_EDGE('',*,*,#43,.T.);
|
||||
#103=EDGE_LOOP('',(#102));
|
||||
#104=ORIENTED_EDGE('',*,*,#30,.F.);
|
||||
#105=EDGE_LOOP('',(#104));
|
||||
#106=FACE_OUTER_BOUND('',#103,.T.);
|
||||
#107=FACE_BOUND('',#105,.T.);
|
||||
#108=ADVANCED_FACE('',(#106,#107),#101,.T.);
|
||||
#109=CLOSED_SHELL('',(#55,#108));
|
||||
#110=MANIFOLD_SOLID_BREP('',#109);
|
||||
#111=ADVANCED_BREP_SHAPE_REPRESENTATION('',(#17,#110),#5);
|
||||
#112=SHAPE_DEFINITION_REPRESENTATION(#13,#111);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
@@ -1,110 +0,0 @@
|
||||
ISO-10303-21;
|
||||
HEADER;
|
||||
FILE_DESCRIPTION(('step-conformance generated part'),'2;1');
|
||||
FILE_NAME('washer.step','2026-10-12T00:00:00',(''),(''),'step-conformance generate','step-conformance generate','');
|
||||
FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1=( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) );
|
||||
#2=( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) );
|
||||
#3=( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() );
|
||||
#4=UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-7),#1,'distance_accuracy_value','confusion accuracy');
|
||||
#5=( GEOMETRIC_REPRESENTATION_CONTEXT(3) GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#4)) GLOBAL_UNIT_ASSIGNED_CONTEXT((#1,#2,#3)) REPRESENTATION_CONTEXT('','3D') );
|
||||
#6=APPLICATION_CONTEXT('automotive design');
|
||||
#7=APPLICATION_PROTOCOL_DEFINITION('international standard','automotive_design',2000,#6);
|
||||
#8=PRODUCT_CONTEXT('',#6,'mechanical');
|
||||
#9=PRODUCT_DEFINITION_CONTEXT('part definition',#6,'design');
|
||||
#10=PRODUCT('washer','washer','',(#8));
|
||||
#11=PRODUCT_DEFINITION_FORMATION('','',#10);
|
||||
#12=PRODUCT_DEFINITION('design','',#11,#9);
|
||||
#13=PRODUCT_DEFINITION_SHAPE('','',#12);
|
||||
#14=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#15=DIRECTION('',(0.0,0.0,1.0));
|
||||
#16=DIRECTION('',(1.0,0.0,0.0));
|
||||
#17=AXIS2_PLACEMENT_3D('',#14,#15,#16);
|
||||
#18=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#19=DIRECTION('',(0.0,0.0,1.0));
|
||||
#20=DIRECTION('',(1.0,0.0,0.0));
|
||||
#21=AXIS2_PLACEMENT_3D('',#18,#19,#20);
|
||||
#22=CIRCLE('',#21,5.0);
|
||||
#23=CARTESIAN_POINT('',(5.0,0.0,0.0));
|
||||
#24=VERTEX_POINT('',#23);
|
||||
#25=EDGE_CURVE('',#24,#24,#22,.T.);
|
||||
#26=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#27=DIRECTION('',(0.0,0.0,1.0));
|
||||
#28=DIRECTION('',(1.0,0.0,0.0));
|
||||
#29=AXIS2_PLACEMENT_3D('',#26,#27,#28);
|
||||
#30=CIRCLE('',#29,20.0);
|
||||
#31=CARTESIAN_POINT('',(20.0,0.0,0.0));
|
||||
#32=VERTEX_POINT('',#31);
|
||||
#33=EDGE_CURVE('',#32,#32,#30,.T.);
|
||||
#34=CARTESIAN_POINT('',(0.0,0.0,3.0));
|
||||
#35=DIRECTION('',(0.0,0.0,1.0));
|
||||
#36=DIRECTION('',(1.0,0.0,0.0));
|
||||
#37=AXIS2_PLACEMENT_3D('',#34,#35,#36);
|
||||
#38=CIRCLE('',#37,20.0);
|
||||
#39=CARTESIAN_POINT('',(20.0,0.0,3.0));
|
||||
#40=VERTEX_POINT('',#39);
|
||||
#41=EDGE_CURVE('',#40,#40,#38,.T.);
|
||||
#42=CARTESIAN_POINT('',(0.0,0.0,3.0));
|
||||
#43=DIRECTION('',(0.0,0.0,1.0));
|
||||
#44=DIRECTION('',(1.0,0.0,0.0));
|
||||
#45=AXIS2_PLACEMENT_3D('',#42,#43,#44);
|
||||
#46=CIRCLE('',#45,5.0);
|
||||
#47=CARTESIAN_POINT('',(5.0,0.0,3.0));
|
||||
#48=VERTEX_POINT('',#47);
|
||||
#49=EDGE_CURVE('',#48,#48,#46,.T.);
|
||||
#50=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#51=DIRECTION('',(0.0,0.0,1.0));
|
||||
#52=DIRECTION('',(1.0,0.0,0.0));
|
||||
#53=AXIS2_PLACEMENT_3D('',#50,#51,#52);
|
||||
#54=PLANE('',#53);
|
||||
#55=ORIENTED_EDGE('',*,*,#25,.T.);
|
||||
#56=EDGE_LOOP('',(#55));
|
||||
#57=ORIENTED_EDGE('',*,*,#33,.F.);
|
||||
#58=EDGE_LOOP('',(#57));
|
||||
#59=FACE_OUTER_BOUND('',#58,.T.);
|
||||
#60=FACE_BOUND('',#56,.T.);
|
||||
#61=ADVANCED_FACE('',(#59,#60),#54,.F.);
|
||||
#62=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#63=DIRECTION('',(0.0,0.0,1.0));
|
||||
#64=DIRECTION('',(1.0,0.0,0.0));
|
||||
#65=AXIS2_PLACEMENT_3D('',#62,#63,#64);
|
||||
#66=CYLINDRICAL_SURFACE('',#65,20.0);
|
||||
#67=ORIENTED_EDGE('',*,*,#33,.T.);
|
||||
#68=EDGE_LOOP('',(#67));
|
||||
#69=ORIENTED_EDGE('',*,*,#41,.F.);
|
||||
#70=EDGE_LOOP('',(#69));
|
||||
#71=FACE_OUTER_BOUND('',#68,.T.);
|
||||
#72=FACE_BOUND('',#70,.T.);
|
||||
#73=ADVANCED_FACE('',(#71,#72),#66,.T.);
|
||||
#74=CARTESIAN_POINT('',(0.0,0.0,3.0));
|
||||
#75=DIRECTION('',(0.0,0.0,1.0));
|
||||
#76=DIRECTION('',(1.0,0.0,0.0));
|
||||
#77=AXIS2_PLACEMENT_3D('',#74,#75,#76);
|
||||
#78=PLANE('',#77);
|
||||
#79=ORIENTED_EDGE('',*,*,#41,.T.);
|
||||
#80=EDGE_LOOP('',(#79));
|
||||
#81=ORIENTED_EDGE('',*,*,#49,.F.);
|
||||
#82=EDGE_LOOP('',(#81));
|
||||
#83=FACE_OUTER_BOUND('',#80,.T.);
|
||||
#84=FACE_BOUND('',#82,.T.);
|
||||
#85=ADVANCED_FACE('',(#83,#84),#78,.T.);
|
||||
#86=CARTESIAN_POINT('',(0.0,0.0,3.0));
|
||||
#87=DIRECTION('',(0.0,0.0,1.0));
|
||||
#88=DIRECTION('',(1.0,0.0,0.0));
|
||||
#89=AXIS2_PLACEMENT_3D('',#86,#87,#88);
|
||||
#90=CYLINDRICAL_SURFACE('',#89,5.0);
|
||||
#91=ORIENTED_EDGE('',*,*,#49,.T.);
|
||||
#92=EDGE_LOOP('',(#91));
|
||||
#93=ORIENTED_EDGE('',*,*,#25,.F.);
|
||||
#94=EDGE_LOOP('',(#93));
|
||||
#95=FACE_OUTER_BOUND('',#92,.T.);
|
||||
#96=FACE_BOUND('',#94,.T.);
|
||||
#97=ADVANCED_FACE('',(#95,#96),#90,.F.);
|
||||
#98=CLOSED_SHELL('',(#61,#73,#85,#97));
|
||||
#99=MANIFOLD_SOLID_BREP('',#98);
|
||||
#100=ADVANCED_BREP_SHAPE_REPRESENTATION('',(#17,#99),#5);
|
||||
#101=SHAPE_DEFINITION_REPRESENTATION(#13,#100);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
Vendored
-5
@@ -30,11 +30,6 @@ Changes:
|
||||
With no direction to go by, the edge goes where both of its ends are in one wire.
|
||||
- `transversal/divide_face/mod.rs`: a face piece whose wire is not simple makes the operation
|
||||
give up (`Face::try_new`) instead of panicking.
|
||||
- `transversal/divide_face/mod.rs`: a loop whose polygon in the face's parameters has no area
|
||||
(a small hole's rim divided at the tolerance of a much larger operand: one chord per half
|
||||
circle) is divided again, finer, before its area's sign tells a hole from a face piece. The
|
||||
sign of a zero area was random: the hole became a face of its own, inside out, and the face it
|
||||
was cut in stayed whole.
|
||||
- `lib.rs`: `tr!` reports where an operation gives up when `SHAPEOPS_TRACE` is set; compiler
|
||||
warnings no longer fail the build.
|
||||
- Face and edge history (`transversal/history.rs`, new): `and_with_history` / `or_with_history`
|
||||
|
||||
@@ -86,61 +86,6 @@ where
|
||||
Some(PolylineCurve(vec))
|
||||
}
|
||||
|
||||
/// SolidCraft: whether a loop's polygon in the parameter plane has no area to speak of: two
|
||||
/// points (a closed loop of two edges divided at their ends only), or an area that is rounding
|
||||
/// beside the polygon's extent. Its sign then tells nothing.
|
||||
fn flat_polygon(poly: &PolylineCurve<Point2>) -> bool {
|
||||
let (mut lo, mut hi) = (Point2::new(f64::INFINITY, f64::INFINITY), Point2::new(f64::NEG_INFINITY, f64::NEG_INFINITY));
|
||||
for p in poly.iter() {
|
||||
lo = Point2::new(lo.x.min(p.x), lo.y.min(p.y));
|
||||
hi = Point2::new(hi.x.max(p.x), hi.y.max(p.y));
|
||||
}
|
||||
let diag2 = (hi.x - lo.x).powi(2) + (hi.y - lo.y).powi(2);
|
||||
poly.len() < 4 || !(poly.area().abs() > 1e-9 * diag2)
|
||||
}
|
||||
|
||||
/// SolidCraft: a loop whose polygon is flat ([`flat_polygon`]) divided again, finer, to the
|
||||
/// loop's own size. A small hole's wire is two half circles; divided at a tolerance that
|
||||
/// follows a much larger body (a few millimetres for a part over a metre long) each half is a
|
||||
/// single chord, the polygon has no area, and its sign, which tells a hole from a face piece,
|
||||
/// comes out at random: the hole became a face of its own, inside out, and the face it was cut
|
||||
/// in was left whole. The polygon only places the loop; the edges are not changed.
|
||||
fn finer_if_flat<C, S>(face: &Face<Point3, C, S>, wire: &Wire<Point3, C>, poly: PolylineCurve<Point2>, tol: f64) -> PolylineCurve<Point2>
|
||||
where
|
||||
C: BoundedCurve<Point = Point3> + ParameterDivision1D<Point = Point3>,
|
||||
S: Clone + ParametricSurface3D + SearchParameter<D2, Point = Point3> + SearchNearestParameter<D2, Point = Point3>,
|
||||
{
|
||||
if !flat_polygon(&poly) {
|
||||
return poly;
|
||||
}
|
||||
// The loop's extent: the diagonal of the box round its edges' ends and middles.
|
||||
let mut pts = Vec::new();
|
||||
for edge in wire.edge_iter() {
|
||||
let curve = edge.curve();
|
||||
let (t0, t1) = curve.range_tuple();
|
||||
pts.extend([curve.subs(t0), curve.subs((t0 + t1) / 2.0), curve.subs(t1)]);
|
||||
}
|
||||
let (mut lo, mut hi) = (Point3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY), Point3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY));
|
||||
for p in &pts {
|
||||
lo = Point3::new(lo.x.min(p.x), lo.y.min(p.y), lo.z.min(p.z));
|
||||
hi = Point3::new(hi.x.max(p.x), hi.y.max(p.y), hi.z.max(p.z));
|
||||
}
|
||||
let size = if pts.is_empty() { 0.0 } else { hi.distance(lo) };
|
||||
let mut fine = tol;
|
||||
for _ in 0..3 {
|
||||
fine = (fine / 8.0).min(size * 0.01);
|
||||
if !(fine > 0.0 && fine.is_finite()) {
|
||||
break;
|
||||
}
|
||||
let mut map = HashMap::default();
|
||||
let again = create_parameter_boundary(face, wire, &mut map, fine, false).or_else(|| create_parameter_boundary(face, wire, &mut map, fine, true));
|
||||
if let Some(again) = again.filter(|p| !flat_polygon(p)) {
|
||||
return again;
|
||||
}
|
||||
}
|
||||
poly
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct WireChunk<'a, C> {
|
||||
poly: PolylineCurve<Point2>,
|
||||
@@ -164,7 +109,6 @@ where
|
||||
create_parameter_boundary(face, wire, &mut map, tol, false).or_else(|| create_parameter_boundary(face, wire, &mut map, tol, true)),
|
||||
"parameter boundary"
|
||||
);
|
||||
let poly = finer_if_flat(face, wire, poly, tol);
|
||||
match poly.area() > 0.0 {
|
||||
true => pre_faces.push(vec![WireChunk { poly, wire }]),
|
||||
false => negative_wires.push(WireChunk { poly, wire }),
|
||||
|
||||
Reference in New Issue
Block a user