mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-04 02:55:54 +00:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d3315e0e4 | |||
| 4098ff9dc8 | |||
| 97383454c6 | |||
| 10d7bd045e | |||
| 6e87fb1a25 | |||
| 30e0db1e05 | |||
| 2b1cb2c4e1 | |||
| f815491936 | |||
| a595805fc8 | |||
| 0379ee76c6 | |||
| 4f7ff8c4d8 | |||
| d2188fb7ab | |||
| 438abbcf82 | |||
| 079a600481 | |||
| 94bd8719aa | |||
| fc3dedfd5c | |||
| 41a62d210c | |||
| 5638259737 | |||
| 090119a5c5 |
@@ -1,6 +1,5 @@
|
||||
# cargo-nextest settings for CI (`cargo xtask test` does not use nextest).
|
||||
# CI builds the tests once and runs them in shards: 2 for a pull request, 4 for main and the nightly
|
||||
# (.github/workflows/ci.yml).
|
||||
# CI builds the tests once and runs them in one job on a 32 vCPU runner (.github/workflows/ci.yml).
|
||||
|
||||
[profile.ci]
|
||||
# Heavy tests (one to twelve minutes each on a CI runner) that pull requests skip. They run on
|
||||
@@ -34,12 +33,14 @@ slow-timeout = { period = "60s" }
|
||||
[profile.ci.junit]
|
||||
path = "junit.xml"
|
||||
|
||||
# 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.
|
||||
# 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.
|
||||
[[profile.ci.overrides]]
|
||||
filter = "binary(action_matrix) & test(=action_matrix_sample)"
|
||||
threads-required = "num-cpus"
|
||||
threads-required = 12
|
||||
priority = 100
|
||||
|
||||
|
||||
# The slowest tests start first, so a shard does not end with one of them running alone on a
|
||||
|
||||
@@ -7,4 +7,5 @@ self-hosted-runner:
|
||||
- blacksmith-4vcpu-ubuntu-2404
|
||||
- blacksmith-8vcpu-ubuntu-2404
|
||||
- blacksmith-16vcpu-ubuntu-2404
|
||||
- blacksmith-32vcpu-ubuntu-2404
|
||||
- blacksmith-8vcpu-windows-2025
|
||||
|
||||
+116
-47
@@ -8,12 +8,13 @@
|
||||
# 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 once and run in shards. A few heavy tests (see .config/nextest.toml) are
|
||||
# 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
|
||||
# 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. On pull requests the test build and the
|
||||
# shards start only after the format and repository checks pass.
|
||||
# main saves the build caches; pull requests restore them. sccache is on a Blacksmith sticky disk that
|
||||
# only main commits.
|
||||
# A push to main runs only the repository checks when a passing merge-queue run tested that very
|
||||
# commit, or when nothing a build reads changed since the last push to main whose run passed.
|
||||
# Otherwise it runs everything.
|
||||
@@ -29,14 +30,13 @@
|
||||
# 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: 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.
|
||||
# per job: 32 vCPU for the tests, 16 for Clippy, the archive build (nightly) and the merge queue's, 8 for compiles, doc and reference tests, 4 for the light checks, 2 for Select checks and ci.
|
||||
# The Windows shader job runs on Blacksmith's Windows Server 2025 runners (blacksmith-8vcpu-windows-2025);
|
||||
# the Windows app startup smoke stays on windows-latest, which has a desktop session that presents frames.
|
||||
# Pull requests, forks included, the merge queue, pushes to main and the nightly run all use them:
|
||||
# the runners are ephemeral, fork runs get no secrets, and main saves the build caches that every
|
||||
# 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
|
||||
# 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
|
||||
# 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]
|
||||
branches: [main, kernel-smlib] # kernel-smlib: full suite after each merge into the kernel branch
|
||||
schedule:
|
||||
- cron: "17 6 * * *"
|
||||
workflow_dispatch:
|
||||
@@ -178,9 +178,12 @@ jobs:
|
||||
git diff --no-renames --name-only -z "$BASE_SHA...$HEAD_SHA" > "$RUNNER_TEMP/ci-paths"
|
||||
while IFS= read -r -d '' path; do
|
||||
case "$path" in crates/kernel/*|vendor/truck-*) kernel=true ;; esac
|
||||
# What can change a robustness golden: the kernel and the crates a design is built with.
|
||||
# What a pull request re-checks the goldens for: the kernel, truck and the goldens
|
||||
# themselves (a re-record is checked before it is queued). The goldens are frozen, so
|
||||
# on any other change they would only re-check unchanged results; the merge queue,
|
||||
# main and the nightly run every seed before anything lands.
|
||||
case "$path" in
|
||||
crates/kernel/*|crates/engine/*|crates/doc/*|crates/sketch/*|crates/geom/*|vendor/truck-*|tests/robustness/*) goldens=true ;;
|
||||
crates/kernel/*|vendor/truck-*|tests/robustness/*) goldens=true ;;
|
||||
esac
|
||||
# What the Windows D3D12 jobs exercise: the GPU crate and its shaders, the app and
|
||||
# its startup, the viewport's wgpu glue, the vendored egui-wgpu, their scripts.
|
||||
@@ -337,25 +340,33 @@ jobs:
|
||||
|
||||
build-tests:
|
||||
name: Build tests
|
||||
# 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') }}
|
||||
# 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') }}
|
||||
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_GHA_ENABLED: "true"
|
||||
SCCACHE_IGNORE_SERVER_IO_ERROR: "1"
|
||||
SCCACHE_DIR: /home/runner/.cache/sccache
|
||||
SCCACHE_CACHE_SIZE: 20G
|
||||
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
|
||||
@@ -387,63 +398,94 @@ jobs:
|
||||
run: sccache --show-stats
|
||||
|
||||
tests:
|
||||
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' }}
|
||||
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
|
||||
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 }}
|
||||
SHARD: ${{ matrix.shard }}
|
||||
SHARDS: ${{ github.event_name == 'pull_request' && 2 || 4 }}
|
||||
FULL: ${{ needs.changes.outputs.full }}
|
||||
PACKAGES: ${{ needs.changes.outputs.packages }}
|
||||
# The robustness golden seeds a pull request skips: each takes one to twenty minutes under
|
||||
# CI load, together about four fifths of the goldens' time. Pushes to main, the merge
|
||||
# queue and the nightly run every seed.
|
||||
SLOW_SEEDS: 81|45|20|24|95|02|57|40|18|48|32|46|85|61|77|34|14
|
||||
run: |
|
||||
# Pull requests skip the heavy tests (default-filter in .config/nextest.toml).
|
||||
filter=()
|
||||
# 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
|
||||
if [ "$EVENT_NAME" != pull_request ]; then
|
||||
# The boolean property tests draw new random inputs every run; they run only in the
|
||||
# nightly property-tests job, so a known kernel bug cannot turn main red at random.
|
||||
filter=(--ignore-default-filter -E 'not (package(solidcraft-kernel) & binary(boolean_props))')
|
||||
base='not (package(solidcraft-kernel) & binary(boolean_props))'
|
||||
elif [ "$GOLDENS" = true ]; then
|
||||
# A pull request that can change a robustness golden (tests/robustness/goldens.json)
|
||||
# also checks every seed but the slow ones, in the same run so they share its cores.
|
||||
filter=(--ignore-default-filter -E "default() | (package(solidcraft-engine) & test(/^tests::robustness_goldens::/) & not test(/^tests::robustness_goldens::seed_($SLOW_SEEDS)\$/))")
|
||||
base="(default() & not (binary(action_matrix) & test(=action_matrix_sample))) | (package(solidcraft-engine) & test(/^tests::robustness_goldens::/) & not test(/^tests::robustness_goldens::seed_($SLOW_SEEDS)\$/))"
|
||||
fi
|
||||
junit=${CARGO_TARGET_DIR:-target}/nextest/ci/junit.xml
|
||||
rm -f "$junit"
|
||||
# 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
|
||||
# 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
|
||||
# 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:"
|
||||
@@ -451,11 +493,14 @@ 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-${{ matrix.shard }}
|
||||
name: junit
|
||||
path: ${{ env.CARGO_TARGET_DIR || 'target' }}/nextest/ci/junit.xml
|
||||
retention-days: 7
|
||||
|
||||
@@ -560,13 +605,23 @@ jobs:
|
||||
env:
|
||||
CARGO_INCREMENTAL: "0"
|
||||
RUSTC_WRAPPER: sccache
|
||||
SCCACHE_GHA_ENABLED: "true"
|
||||
SCCACHE_IGNORE_SERVER_IO_ERROR: "1"
|
||||
SCCACHE_DIR: /home/runner/.cache/sccache
|
||||
SCCACHE_CACHE_SIZE: 20G
|
||||
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:
|
||||
@@ -608,13 +663,23 @@ jobs:
|
||||
env:
|
||||
CARGO_INCREMENTAL: "0"
|
||||
RUSTC_WRAPPER: sccache
|
||||
SCCACHE_GHA_ENABLED: "true"
|
||||
SCCACHE_IGNORE_SERVER_IO_ERROR: "1"
|
||||
SCCACHE_DIR: /home/runner/.cache/sccache
|
||||
SCCACHE_CACHE_SIZE: 20G
|
||||
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:
|
||||
@@ -746,6 +811,7 @@ 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 }}
|
||||
@@ -760,7 +826,10 @@ jobs:
|
||||
fi
|
||||
if [ "$NATIVE" = true ]; then
|
||||
test "$CLIPPY" = success
|
||||
test "$BUILD" = 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 "$TESTS" = success
|
||||
fi
|
||||
if [ "$DOCTESTS" = true ]; then
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#
|
||||
# Linux x86_64, aarch64 AppImage (+ .zsync), .deb, .rpm, .flatpak, .snap, .tar.gz packaging/linux
|
||||
# FreeBSD x86_64 .tar.gz (built in a FreeBSD VM) packaging/freebsd
|
||||
# macOS universal .dmg, and the CLI as a .zip (both archs built in parallel) packaging/macos
|
||||
# macOS universal .dmg, and the CLI as a .zip packaging/macos
|
||||
# Windows x64, x86, arm64 .msi and a portable .zip; x64 also the plain .exe packaging/windows
|
||||
# Web a .zip of the static site packaging/web
|
||||
#
|
||||
@@ -83,6 +83,28 @@ jobs:
|
||||
{ echo "version=$VERSION"; echo "date=$(date -u +%Y-%m-%d)"; echo "prerelease=$pre"; } >>"$GITHUB_OUTPUT"
|
||||
echo "### SolidCraft v$VERSION (\`${GITHUB_SHA::9}\`)" >>"$GITHUB_STEP_SUMMARY"
|
||||
|
||||
# The app icon in every size, drawn by solidcraft-cli (no image files are committed), for the
|
||||
# Windows jobs: the ARM64 build is cross-compiled and can't run there to draw its own.
|
||||
icons:
|
||||
needs: version
|
||||
if: needs.version.outputs.build == 'true'
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: release-icons
|
||||
- run: cargo build --release --locked -p solidcraft-cli
|
||||
- run: |
|
||||
. packaging/env.sh
|
||||
make_icons target/release/solidcraft-cli icons
|
||||
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: icons
|
||||
path: icons
|
||||
if-no-files-found: error
|
||||
|
||||
linux:
|
||||
name: Linux ${{ matrix.arch }}
|
||||
needs: version
|
||||
@@ -271,19 +293,11 @@ jobs:
|
||||
path: dist/release/*
|
||||
if-no-files-found: error
|
||||
|
||||
# The two halves of the universal binary build in parallel on their own runners (each alone
|
||||
# took about half of the old single job's 45 minutes); `macos` below joins and packages them.
|
||||
macos-build:
|
||||
name: macOS build ${{ matrix.arch }}
|
||||
macos:
|
||||
name: macOS universal
|
||||
needs: version
|
||||
if: needs.version.outputs.build == 'true'
|
||||
runs-on: macos-14
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- { arch: aarch64, target: aarch64-apple-darwin }
|
||||
- { arch: x86_64, target: x86_64-apple-darwin }
|
||||
env:
|
||||
SOLIDCRAFT_VERSION: ${{ needs.version.outputs.version }}
|
||||
SOLIDCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
|
||||
@@ -291,45 +305,12 @@ jobs:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
targets: aarch64-apple-darwin, x86_64-apple-darwin
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: release-macos-${{ matrix.arch }}
|
||||
- name: Build
|
||||
run: packaging/macos/package.sh --build-only ${{ matrix.target }}
|
||||
# Not named release-*: the release job collects every release-* artifact as an asset.
|
||||
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: macos-bin-${{ matrix.arch }}
|
||||
path: |
|
||||
target/${{ matrix.target }}/release/solidcraft
|
||||
target/${{ matrix.target }}/release/solidcraft-cli
|
||||
target/${{ matrix.target }}/release/solvecraft-cli
|
||||
if-no-files-found: error
|
||||
|
||||
macos:
|
||||
name: macOS universal
|
||||
needs: [version, macos-build]
|
||||
if: needs.version.outputs.build == 'true'
|
||||
runs-on: macos-14
|
||||
env:
|
||||
SOLIDCRAFT_VERSION: ${{ needs.version.outputs.version }}
|
||||
SOLIDCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
name: macos-bin-aarch64
|
||||
path: target/aarch64-apple-darwin/release
|
||||
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
name: macos-bin-x86_64
|
||||
path: target/x86_64-apple-darwin/release
|
||||
- name: Join, package and check (dmg, CLI zip; the dmg is mounted and checked)
|
||||
run: |
|
||||
# Artifacts don't keep the executable bit.
|
||||
chmod +x target/*-apple-darwin/release/solidcraft target/*-apple-darwin/release/solidcraft-cli target/*-apple-darwin/release/solvecraft-cli
|
||||
packaging/macos/package.sh --arch universal --skip-build
|
||||
key: release-macos-universal
|
||||
- name: Build and package (dmg, CLI zip; the dmg is mounted and checked)
|
||||
run: packaging/macos/package.sh --arch universal
|
||||
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: release-macos
|
||||
@@ -338,12 +319,9 @@ jobs:
|
||||
|
||||
windows:
|
||||
name: Windows ${{ matrix.arch }}
|
||||
needs: version
|
||||
needs: [version, icons]
|
||||
if: needs.version.outputs.build == 'true'
|
||||
# An 8 vCPU runner: the release build was 35 minutes on a 2 vCPU windows-latest and is the
|
||||
# longest job of the release. The installer icon is the committed
|
||||
# assets/icons/app/windows/exports/solidcraft.ico (package.ps1's default), so nothing waits.
|
||||
runs-on: blacksmith-8vcpu-windows-2025
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -356,6 +334,10 @@ jobs:
|
||||
SOLIDCRAFT_VERSION: ${{ needs.version.outputs.version }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
name: icons
|
||||
path: icons
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
@@ -369,6 +351,8 @@ jobs:
|
||||
wix extension add -g WixToolset.UI.wixext/5.0.2 WixToolset.Util.wixext/5.0.2
|
||||
- name: Build and package (msi, portable zip)
|
||||
shell: pwsh
|
||||
env:
|
||||
SOLIDCRAFT_ICONS: ${{ github.workspace }}/icons
|
||||
run: ./packaging/windows/package.ps1 -Arch ${{ matrix.arch }}
|
||||
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
|
||||
@@ -57,6 +57,23 @@ 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 |
|
||||
|
||||
+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 matching the workflow's geometry paths run all but the slowest, `SLOW_SEEDS` in `ci.yml`),
|
||||
the seeds of `tests::robustness_goldens` (`seed_00` … `seed_95`; pull requests that change the kernel, truck or the goldens run all but the slowest, `SLOW_SEEDS` in `ci.yml`),
|
||||
`pattern_suppress_tests::enclosure_arc_extinction_keeps_a_real_sharp_inner_lip_after_resize_and_reopen`,
|
||||
`pattern_suppress_tests::enclosure_datums_follow_wall_height_and_board_depth_with_material_proofs`,
|
||||
`cmd::plastic::tests::enclosure_snap_fit_follows_resized_front_wall`, the `atomic_commands` contract tests
|
||||
|
||||
@@ -4689,6 +4689,11 @@ 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
|
||||
|
||||
@@ -549,3 +549,29 @@ 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,8 +1864,11 @@ 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 })) => {
|
||||
out.push(add_c(sk, ConstraintKind::Symmetric { p: a0, q: b0, l })?);
|
||||
out.push(add_c(sk, ConstraintKind::Symmetric { p: a1, q: b1, l })?);
|
||||
// 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 })?);
|
||||
}
|
||||
(Some(_), Some(_)) => {
|
||||
let pa = round_center(sk, ca).ok_or_else(|| bad(cmd, "symmetry needs two lines or two circles/arcs"))?;
|
||||
@@ -1886,6 +1889,25 @@ 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`).
|
||||
|
||||
@@ -374,6 +374,66 @@ 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, Sketch, TextAlign, TextExprs, TextStyle};
|
||||
use solidcraft_sketch::{ConstraintKind, CurveKind, LinkKind, LinkSource, Shape, Sketch, TextAlign, TextExprs, TextStyle};
|
||||
|
||||
/// The style parameters of `sketch.text` and `sketch.edit_text`.
|
||||
macro_rules! text_style_doc {
|
||||
@@ -822,6 +822,39 @@ 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 {
|
||||
@@ -842,10 +875,14 @@ 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 nearly touches: move the centre so the guessed circle
|
||||
// sits between the curves.
|
||||
// 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,
|
||||
};
|
||||
let first_new = sk.points.len();
|
||||
let c = sk.add_circle(near, r0, None, None)?;
|
||||
let c = sk.add_circle(start, r0, None, None)?;
|
||||
let mut cons = Vec::new();
|
||||
for k in &cs {
|
||||
cons.push(add_c(sk, ConstraintKind::Tangent { a: *k, b: c })?);
|
||||
|
||||
@@ -438,6 +438,51 @@ 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,16 +431,24 @@ 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.
|
||||
//!
|
||||
//! A running app keeps one recovery entry in the recovery folder: `<id>.solidcraft` (the design
|
||||
//! Each open design keeps its own 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 app's autosaver.
|
||||
/// One open design's autosaver. Keep it with that design when switching tabs.
|
||||
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 app's lock.
|
||||
/// Start autosaving into `dir` (created if needed), holding this design'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,6 +361,20 @@ 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,6 +1785,23 @@ 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,6 +541,17 @@ 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.
|
||||
|
||||
@@ -660,6 +660,8 @@ impl<'a> Tr<'a> {
|
||||
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 {
|
||||
@@ -685,11 +687,21 @@ impl<'a> Tr<'a> {
|
||||
if knots.windows(2).any(|w| w[1] < w[0]) {
|
||||
return Err(format!("entity {seq}: knots out of order"));
|
||||
}
|
||||
let nb = Nurbs { deg: m, knots, pts };
|
||||
let mut 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)
|
||||
}
|
||||
|
||||
|
||||
@@ -342,3 +342,42 @@ fn a_hole_whose_first_trim_piece_is_reversed_stays_a_hole() {
|
||||
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,6 +53,63 @@ 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();
|
||||
@@ -321,6 +378,35 @@ 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.
|
||||
@@ -555,5 +641,22 @@ 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,3 +236,40 @@ 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,6 +111,30 @@ 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);
|
||||
@@ -1746,3 +1770,53 @@ 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,3 +3012,45 @@ 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,6 +421,35 @@ 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();
|
||||
@@ -1346,6 +1375,27 @@ 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) =
|
||||
|
||||
+28
-19
@@ -500,12 +500,13 @@ impl Undo {
|
||||
}
|
||||
}
|
||||
|
||||
/// Go back; the number of steps undone.
|
||||
fn rollback(self, b: &mut dyn Backend, steps: usize) -> u64 {
|
||||
/// 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) {
|
||||
match self {
|
||||
Undo::Checkpoint(id) => match exec(b, "edit.restore_checkpoint", json!({"checkpoint": id})) {
|
||||
Ok(_) => steps as u64,
|
||||
Err(_) => 0,
|
||||
Ok(_) => (true, steps as u64),
|
||||
Err(_) => (false, 0),
|
||||
},
|
||||
Undo::Depth(before) => {
|
||||
let after = undo_depth(b).unwrap_or(before);
|
||||
@@ -516,9 +517,10 @@ impl Undo {
|
||||
}
|
||||
undone += 1;
|
||||
}
|
||||
undone
|
||||
(undone == steps as u64, undone)
|
||||
}
|
||||
Undo::None => 0,
|
||||
// Nothing remembered the design; only a first step that failed left nothing behind.
|
||||
Undo::None => (steps == 0, 0),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -583,12 +585,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 undone = start.rollback(b, i);
|
||||
let (rolled_back, undone) = start.rollback(b, i);
|
||||
let reply = json!({
|
||||
"ok": false,
|
||||
"completed": i,
|
||||
"failed": {"index": i, "command": command, "error": e},
|
||||
"rolled_back": rollback,
|
||||
"rolled_back": rolled_back,
|
||||
"undone": undone,
|
||||
"results": results,
|
||||
});
|
||||
@@ -616,17 +618,24 @@ 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) => (
|
||||
json!({
|
||||
"ok": false,
|
||||
"completed": r["completed"],
|
||||
"failed": failed,
|
||||
"rolled_back": rollback,
|
||||
"undone": r["undone"],
|
||||
"results": results,
|
||||
}),
|
||||
true,
|
||||
),
|
||||
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,
|
||||
)
|
||||
}
|
||||
};
|
||||
// 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};
|
||||
pub use shape::{Shape, intersections, intersections_within};
|
||||
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,9 +428,19 @@ 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) };
|
||||
let galley = ui.painter().layout_no_wrap(label_display, font, col);
|
||||
// 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 lw = galley.size().x;
|
||||
ui.painter().galley(pos2(x, cy - galley.size().y / 2.0), galley, col);
|
||||
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);
|
||||
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));
|
||||
@@ -446,6 +456,7 @@ 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;
|
||||
@@ -1237,7 +1248,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() }),
|
||||
_ => None,
|
||||
_ => Some(Sel::Vertex { body: String::new(), point: solidcraft_engine::geom::Vec3::ZERO }),
|
||||
};
|
||||
let (selected, mark) = lit_row(lit(&thing));
|
||||
let r = draw_row(
|
||||
@@ -1557,6 +1568,8 @@ 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,3 +239,16 @@ 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, SelInput, VERTICES};
|
||||
use crate::selection::{AXES, BODIES, EDGES, FACES, ORIGIN, 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;
|
||||
const POINTS: u16 = VERTICES | FACES | EDGES | ORIGIN;
|
||||
|
||||
/// A fresh Move dialog of a type (all values zero).
|
||||
pub fn kind(mode: usize) -> Kind {
|
||||
|
||||
@@ -69,3 +69,33 @@ 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 › Save as mesh, the Mesh tab, and the
|
||||
//! Every place that saves a mesh opens this dialog: File › Export STL / Export 3MF, 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,6 +39,14 @@ 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,
|
||||
@@ -87,6 +95,17 @@ 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,7 +18,9 @@ pub const MAX_OPEN: usize = 32;
|
||||
|
||||
/// A design that is not the active one: everything that belongs to it alone.
|
||||
pub struct DocSlot {
|
||||
session: Session,
|
||||
pub(crate) session: Session,
|
||||
pub(crate) autosave: Option<solidcraft_engine::recovery::Autosaver>,
|
||||
pub(crate) big_operation: bool,
|
||||
cam: Camera,
|
||||
pre_sketch_cam: Option<Camera>,
|
||||
pre_sketch_marker: Option<Option<usize>>,
|
||||
@@ -38,7 +40,16 @@ struct DocView {
|
||||
|
||||
impl DocSlot {
|
||||
fn new(session: Session) -> DocSlot {
|
||||
DocSlot { session, cam: Camera::default(), pre_sketch_cam: None, pre_sketch_marker: None, view: DocView::default(), occurrence_move: None }
|
||||
DocSlot {
|
||||
session,
|
||||
autosave: None,
|
||||
big_operation: false,
|
||||
cam: Camera::default(),
|
||||
pre_sketch_cam: None,
|
||||
pre_sketch_marker: None,
|
||||
view: DocView::default(),
|
||||
occurrence_move: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +69,10 @@ 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)
|
||||
@@ -107,14 +122,19 @@ 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 out = DocSlot {
|
||||
let mut 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;
|
||||
@@ -254,6 +274,10 @@ 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);
|
||||
@@ -273,7 +297,11 @@ pub fn close(app: &mut SolveApp, i: usize, force: bool) {
|
||||
let next = if i + 1 < n { i + 1 } else { i - 1 };
|
||||
switch(app, next);
|
||||
}
|
||||
app.docs.slots.remove(i);
|
||||
if let Some(slot) = app.docs.slots.remove(i)
|
||||
&& let Some(a) = slot.autosave
|
||||
{
|
||||
a.discard();
|
||||
}
|
||||
if app.docs.active > i {
|
||||
app.docs.active -= 1;
|
||||
}
|
||||
|
||||
@@ -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; contextual SKETCH and SHEET METAL join the seven design tabs.
|
||||
/// Ribbon tab; SHEET METAL and the contextual SKETCH join the seven design tabs.
|
||||
pub tab: String,
|
||||
/// Last contextual body/sketch, only used to select on entry.
|
||||
#[serde(skip)]
|
||||
@@ -430,7 +430,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<solidcraft_engine::recovery::Autosaver>,
|
||||
pub autosave: Option<recovery_ui::Autosaves>,
|
||||
/// 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`).
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
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;
|
||||
@@ -9,11 +12,71 @@ 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;
|
||||
|
||||
/// Autosave (when due, or after a big operation).
|
||||
/// 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.
|
||||
pub fn tick(app: &mut SolveApp) {
|
||||
let big = std::mem::take(&mut app.big_operation);
|
||||
let Some(a) = app.autosave.as_mut() else { return };
|
||||
if let Err(e) = a.tick(&app.session, big) {
|
||||
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 {
|
||||
let msg = format!("autosave failed: {e}");
|
||||
if app.status.as_ref().is_none_or(|(s, _, _)| *s != msg) {
|
||||
app.set_status(msg, true);
|
||||
@@ -23,10 +86,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, interval) {
|
||||
match Autosaver::new(dir, SHARED_CADENCE) {
|
||||
Ok(a) => {
|
||||
app.recoverable = recovery::orphans(dir);
|
||||
app.autosave = Some(a);
|
||||
app.autosave = Some(Autosaves { dir: dir.to_path_buf(), interval, active: Some(a), last_at: Instant::now() });
|
||||
}
|
||||
Err(e) => app.set_status(format!("autosave is off: {e}"), true),
|
||||
}
|
||||
@@ -34,8 +97,17 @@ 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(a) = app.autosave.take() {
|
||||
a.close(&app.session);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,8 +177,277 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
use solidcraft_engine::recovery::Entry;
|
||||
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();
|
||||
}
|
||||
|
||||
fn entry(id: &str, name: &str) -> Entry {
|
||||
Entry { id: id.into(), path: None, name: name.into(), saved_at: 0, features: 3 }
|
||||
|
||||
@@ -38,6 +38,8 @@ 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)]
|
||||
@@ -79,7 +81,8 @@ 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 a & VERTICES != 0 => Some(Sel::Vertex { body: body.clone(), point: *point }),
|
||||
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 }),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +105,24 @@ 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)>);
|
||||
@@ -118,6 +136,7 @@ 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));
|
||||
@@ -125,6 +144,7 @@ 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));
|
||||
@@ -639,6 +659,15 @@ 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,3 +649,33 @@ 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 lives in the ribbon Exit group. Toggles run `sketch.options`, line types
|
||||
//! degrees of freedom. Finish Sketch sits at the bottom, beside the ribbon's 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,6 +343,7 @@ 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);
|
||||
@@ -460,6 +461,10 @@ 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",
|
||||
@@ -480,13 +485,6 @@ 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));
|
||||
@@ -513,6 +511,9 @@ 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)]
|
||||
@@ -590,6 +591,18 @@ 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 near the second click)",
|
||||
"sketch.circle.two_tangent" => "2-tangent circle: click two curves (the circle goes between the two clicks)",
|
||||
"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,7 +1007,9 @@ 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);
|
||||
let near = t.pts.last().map(|p| xy(p.0)).unwrap_or(json!([0, 0]));
|
||||
// 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);
|
||||
json!({"curves": k, "radius": r, "near": near})
|
||||
}
|
||||
"sketch.circle.three_tangent" => {
|
||||
|
||||
@@ -555,8 +555,12 @@ fn file_menu(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
close = true;
|
||||
}
|
||||
ui.separator();
|
||||
if item_if(ui, "Save as mesh…", "", "mesh_export", shown) {
|
||||
app.start("file.save_mesh");
|
||||
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");
|
||||
close = true;
|
||||
}
|
||||
if item_if(ui, "Export STEP…", "", "export", shown) {
|
||||
@@ -771,6 +775,12 @@ 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);
|
||||
@@ -783,7 +793,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| matches!(**tab, "SKETCH" | "SHEET METAL")).count() as f32 * 6.0;
|
||||
+ tabs.iter().filter(|tab| contextual_tab(app, tab)).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.
|
||||
@@ -813,7 +823,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 = matches!(tab, "SKETCH" | "SHEET METAL");
|
||||
let contextual = contextual_tab(app, tab);
|
||||
if contextual {
|
||||
x += 6.0;
|
||||
}
|
||||
@@ -1893,6 +1903,27 @@ 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,6 +279,17 @@ 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,6 +609,9 @@ 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);
|
||||
|
||||
@@ -1172,6 +1172,7 @@ 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);
|
||||
@@ -1412,6 +1413,16 @@ fn pick_hits(app: &SolveApp, proj: &Proj, pos: Pos2, other_sketch_points: bool,
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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.
|
||||
@@ -3364,11 +3375,15 @@ 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 app.origin_visible()
|
||||
&& !app.session.visibility.hidden_origin.iter().any(|h| h == "O")
|
||||
if origin_point_shown(app)
|
||||
&& 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,6 +60,15 @@ 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",
|
||||
@@ -80,6 +89,7 @@ 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"] },
|
||||
@@ -232,7 +242,11 @@ 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",
|
||||
@@ -242,6 +256,7 @@ 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"] },
|
||||
];
|
||||
@@ -345,6 +360,8 @@ 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
|
||||
@@ -432,9 +449,9 @@ pub fn visible_tabs(app: &crate::SolveApp) -> Vec<&'static str> {
|
||||
if app.session.active_sketch.is_some() {
|
||||
tabs.push("SKETCH");
|
||||
}
|
||||
if sheet_body(app).is_some() {
|
||||
tabs.push("SHEET METAL");
|
||||
}
|
||||
// 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");
|
||||
tabs
|
||||
}
|
||||
|
||||
@@ -616,6 +633,7 @@ 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));
|
||||
@@ -625,7 +643,9 @@ mod tests {
|
||||
#[test]
|
||||
fn contextual_tabs_follow_the_active_body_and_allow_other_tabs() {
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
assert_eq!(visible_tabs(&h.app), TABS);
|
||||
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");
|
||||
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();
|
||||
@@ -645,15 +665,69 @@ 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"));
|
||||
assert!(visible_tabs(&h.app).contains(&"SHEET METAL"), "the tab stays reachable off a sheet body");
|
||||
assert!(sheet_body(&h.app).is_none());
|
||||
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}");
|
||||
@@ -777,4 +851,101 @@ 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": "Save as mesh\u2026"
|
||||
"widget": "Export STL\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 can change
|
||||
them run all but the slowest, `SLOW_SEEDS` in `ci.yml`),
|
||||
the seeds of `tests::robustness_goldens` (`seed_00` … `seed_95`; pull requests that change the kernel, truck or
|
||||
the goldens run all but the slowest, `SLOW_SEEDS` in `ci.yml`),
|
||||
`pattern_suppress_tests::enclosure_arc_extinction_keeps_a_real_sharp_inner_lip_after_resize_and_reopen`,
|
||||
`pattern_suppress_tests::enclosure_datums_follow_wall_height_and_board_depth_with_material_proofs`,
|
||||
`cmd::plastic::tests::enclosure_snap_fit_follows_resized_front_wall`, the `atomic_commands` contract tests
|
||||
|
||||
@@ -38,8 +38,9 @@ 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 → 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
|
||||
**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
|
||||
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).
|
||||
@@ -47,7 +48,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 → Save as mesh…** with the format **3MF with face tags** (`file.export` or `file.save_mesh` with `format: "3mf-tagged"`) writes an
|
||||
**File → Export 3MF…** with the format changed to **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
|
||||
@@ -61,8 +62,9 @@ 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 are autosaved to a recovery folder,
|
||||
and the next launch offers to recover them. Older design files are upgraded on open, and files
|
||||
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
|
||||
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 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**
|
||||
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**
|
||||
(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 and VIEW) hold sections of commands. SKETCH appears while
|
||||
editing a sketch; SHEET METAL appears for the active sheet body. Both select automatically
|
||||
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
|
||||
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). 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, `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.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; 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.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.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 → Save as mesh…** (format **3MF with face tags**), or `file.export` with
|
||||
the B-rep face it lies on (**File → Export 3MF…**, then the 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. 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.
|
||||
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.
|
||||
|
||||
**Bold command ids are default promoted buttons.** Every listed command remains in the section menu. Saved user pins can add buttons, as before.
|
||||
|
||||
@@ -12,6 +12,7 @@ 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` |
|
||||
@@ -44,10 +45,13 @@ 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`** |
|
||||
|
||||
@@ -63,6 +67,7 @@ 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` |
|
||||
@@ -126,7 +131,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 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.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -7,12 +7,6 @@
|
||||
# compatibility alias) with the licence files
|
||||
#
|
||||
# Usage: packaging/macos/package.sh [--arch universal|aarch64|x86_64] [--skip-build]
|
||||
# packaging/macos/package.sh --build-only <rust-target>
|
||||
#
|
||||
# --build-only builds just that target (aarch64-apple-darwin or x86_64-apple-darwin) and stops,
|
||||
# so the release workflow can build the two halves of the universal binary on separate runners;
|
||||
# a later `--arch universal --skip-build` run joins the binaries it finds under
|
||||
# $CARGO_TARGET_DIR/<target>/release/ and packages them.
|
||||
#
|
||||
# The app is ad-hoc signed (not notarized): the first launch needs right-click > Open.
|
||||
# Adapted from PhotoCraft's packaging (storytold/photocraft, Apache-2.0; see ATTRIBUTION.md).
|
||||
@@ -24,13 +18,11 @@ MACOS_BUNDLE_ID=ai.storyteller.solidcraft
|
||||
|
||||
ARCH=universal
|
||||
SKIP_BUILD=0
|
||||
BUILD_ONLY=
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--arch) ARCH="$2"; shift 2 ;;
|
||||
--skip-build) SKIP_BUILD=1; shift ;;
|
||||
--build-only) BUILD_ONLY="$2"; shift 2 ;;
|
||||
-h | --help) sed -n '2,16p' "$0"; exit 0 ;;
|
||||
-h | --help) sed -n '2,10p' "$0"; exit 0 ;;
|
||||
*) echo "unknown argument: $1" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
@@ -49,16 +41,6 @@ APP="$WORK/SolidCraft.app"
|
||||
DMG="$DIST/solidcraft-$VERSION-macos-$ARCH.dmg"
|
||||
CLI_ZIP="$DIST/solidcraft-cli-$VERSION-macos-$ARCH.zip"
|
||||
|
||||
if [ -n "$BUILD_ONLY" ]; then
|
||||
case "$BUILD_ONLY" in
|
||||
aarch64-apple-darwin | x86_64-apple-darwin) ;;
|
||||
*) echo "unknown --build-only target $BUILD_ONLY" >&2; exit 2 ;;
|
||||
esac
|
||||
echo "==> SolidCraft $VERSION for macOS ($BUILD_ONLY, build only)"
|
||||
(cd "$ROOT" && cargo build --release --locked -p solidcraft -p solidcraft-cli --target "$BUILD_ONLY")
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "==> SolidCraft $VERSION for macOS ($ARCH)"
|
||||
if [ "$SKIP_BUILD" = 0 ]; then
|
||||
args=()
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,56 @@
|
||||
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;
|
||||
@@ -0,0 +1,60 @@
|
||||
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;
|
||||
@@ -0,0 +1,76 @@
|
||||
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;
|
||||
@@ -0,0 +1,79 @@
|
||||
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;
|
||||
@@ -0,0 +1,100 @@
|
||||
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;
|
||||
@@ -0,0 +1,99 @@
|
||||
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;
|
||||
@@ -0,0 +1,81 @@
|
||||
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;
|
||||
@@ -0,0 +1,108 @@
|
||||
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;
|
||||
@@ -0,0 +1,56 @@
|
||||
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;
|
||||
@@ -0,0 +1,84 @@
|
||||
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;
|
||||
@@ -0,0 +1,76 @@
|
||||
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
@@ -0,0 +1,76 @@
|
||||
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;
|
||||
@@ -0,0 +1,76 @@
|
||||
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;
|
||||
@@ -0,0 +1,121 @@
|
||||
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;
|
||||
@@ -0,0 +1,110 @@
|
||||
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,6 +30,11 @@ 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,6 +86,61 @@ 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>,
|
||||
@@ -109,6 +164,7 @@ 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