mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-12 03:31:57 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 932b2c84bf |
@@ -9,9 +9,3 @@ self-hosted-runner:
|
||||
- blacksmith-16vcpu-ubuntu-2404
|
||||
- blacksmith-32vcpu-ubuntu-2404
|
||||
- blacksmith-8vcpu-windows-2025
|
||||
# Release builds (workflows/release.yml).
|
||||
- blacksmith-16vcpu-ubuntu-2204
|
||||
- blacksmith-16vcpu-ubuntu-2204-arm
|
||||
- blacksmith-4vcpu-ubuntu-2404-arm
|
||||
- blacksmith-6vcpu-macos-15
|
||||
- blacksmith-12vcpu-macos-15
|
||||
|
||||
@@ -8,9 +8,6 @@
|
||||
# The macOS app is signed and notarized when the APPLE_* secrets exist. The Windows binaries and
|
||||
# MSI are Authenticode-signed through SignPath Foundation when the SIGNPATH_API_TOKEN secret exists
|
||||
# (see the windows job). Nothing else is signed yet.
|
||||
# Runners: Blacksmith (blacksmith-<N>vcpu-<os>), as CI uses, except the `version` and `windows`
|
||||
# jobs, which stay GitHub-hosted: SignPath Foundation only signs a request when every job leading
|
||||
# up to it ran on a GitHub-hosted runner.
|
||||
#
|
||||
# Linux x86_64, aarch64 AppImage (+ .zsync), .deb, .rpm, .flatpak, .snap, .tar.gz packaging/linux
|
||||
# Arch Linux x86_64, aarch64 .pkg.tar.zst, repackaged from the .tar.gz the arch job
|
||||
@@ -117,18 +114,40 @@ 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@1.98.0
|
||||
- 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
|
||||
if: needs.version.outputs.build == 'true'
|
||||
# Ubuntu 22.04, the oldest image, so the binaries only need glibc >= 2.35.
|
||||
# The oldest GitHub-hosted image, so the binaries only need glibc >= 2.35.
|
||||
runs-on: ${{ matrix.runner }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- { arch: x86_64, runner: blacksmith-16vcpu-ubuntu-2204, deb: amd64 }
|
||||
- { arch: aarch64, runner: blacksmith-16vcpu-ubuntu-2204-arm, deb: arm64 }
|
||||
- { arch: x86_64, runner: ubuntu-22.04, deb: amd64 }
|
||||
- { arch: aarch64, runner: ubuntu-22.04-arm, deb: arm64 }
|
||||
env:
|
||||
SOLIDCRAFT_VERSION: ${{ needs.version.outputs.version }}
|
||||
SOLIDCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
|
||||
@@ -184,8 +203,8 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- { arch: x86_64, runner: blacksmith-4vcpu-ubuntu-2404 }
|
||||
- { arch: aarch64, runner: blacksmith-4vcpu-ubuntu-2404-arm }
|
||||
- { arch: x86_64, runner: ubuntu-24.04 }
|
||||
- { arch: aarch64, runner: ubuntu-24.04-arm }
|
||||
env:
|
||||
SOLIDCRAFT_VERSION: ${{ needs.version.outputs.version }}
|
||||
SOLIDCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
|
||||
@@ -214,7 +233,7 @@ jobs:
|
||||
needs: [version, linux]
|
||||
if: needs.version.outputs.build == 'true'
|
||||
# makepkg only repackages the tarball's prebuilt binaries, so both architectures build here.
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-24.04
|
||||
# A pre-release or nightly without an Arch package is still worth having; a stable release isn't.
|
||||
continue-on-error: ${{ contains(needs.version.outputs.version, '-') }}
|
||||
strategy:
|
||||
@@ -284,8 +303,8 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- { arch: x86_64, runner: blacksmith-4vcpu-ubuntu-2404 }
|
||||
- { arch: aarch64, runner: blacksmith-4vcpu-ubuntu-2404-arm }
|
||||
- { arch: x86_64, runner: ubuntu-24.04 }
|
||||
- { arch: aarch64, runner: ubuntu-24.04-arm }
|
||||
env:
|
||||
SOLIDCRAFT_VERSION: ${{ needs.version.outputs.version }}
|
||||
SOLIDCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
|
||||
@@ -330,8 +349,8 @@ jobs:
|
||||
name: FreeBSD x86_64
|
||||
needs: version
|
||||
if: needs.version.outputs.build == 'true'
|
||||
# There are no FreeBSD runners: build in a FreeBSD 14.3 VM (Blacksmith's x64 runners have KVM).
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
# GitHub has no FreeBSD runners: build in a FreeBSD 14.3 VM.
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 180
|
||||
env:
|
||||
SOLIDCRAFT_VERSION: ${{ needs.version.outputs.version }}
|
||||
@@ -344,8 +363,8 @@ jobs:
|
||||
with:
|
||||
release: "14.3"
|
||||
usesh: true
|
||||
cpu: 14
|
||||
mem: 40960
|
||||
cpu: 4
|
||||
mem: 12288
|
||||
# Passed into the VM by name; never templated into the script below.
|
||||
envs: "SOLIDCRAFT_VERSION SOLIDCRAFT_BUILD_DATE"
|
||||
prepare: |
|
||||
@@ -355,9 +374,9 @@ jobs:
|
||||
set -e
|
||||
. "$HOME/.cargo/env"
|
||||
git config --global --add safe.directory "$PWD"
|
||||
# Build outside the checkout so only dist/ is copied back out of the VM. About 3 GB of
|
||||
# VM memory per job: 4 jobs in 12 GB was the most that fit before.
|
||||
export CARGO_TARGET_DIR=/var/tmp/solidcraft-target CARGO_BUILD_JOBS=12
|
||||
# Build outside the checkout so only dist/ is copied back out of the VM. 4 jobs: more
|
||||
# runs the VM out of memory.
|
||||
export CARGO_TARGET_DIR=/var/tmp/solidcraft-target CARGO_BUILD_JOBS=4
|
||||
bash packaging/freebsd/package.sh
|
||||
tar -tzf dist/release/solidcraft-*-freebsd-x86_64.tar.gz \
|
||||
| grep -E '/bin/solidcraft$|/bin/solidcraft-cli$|/share/applications/io\.tangencylabs\.solidcraft\.desktop$|/share/doc/solidcraft/THIRD-PARTY-LICENSES\.txt$'
|
||||
@@ -369,12 +388,11 @@ jobs:
|
||||
|
||||
# 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.
|
||||
# Blacksmith's macOS runners are Apple silicon: the x86_64 half is cross-compiled.
|
||||
macos-build:
|
||||
name: macOS build ${{ matrix.arch }}
|
||||
needs: version
|
||||
if: needs.version.outputs.build == 'true'
|
||||
runs-on: blacksmith-12vcpu-macos-15
|
||||
runs-on: macos-15
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -408,7 +426,7 @@ jobs:
|
||||
name: macOS universal
|
||||
needs: [version, macos-build]
|
||||
if: needs.version.outputs.build == 'true'
|
||||
runs-on: blacksmith-6vcpu-macos-15
|
||||
runs-on: macos-15
|
||||
env:
|
||||
SOLIDCRAFT_VERSION: ${{ needs.version.outputs.version }}
|
||||
SOLIDCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
|
||||
@@ -448,11 +466,10 @@ jobs:
|
||||
|
||||
windows:
|
||||
name: Windows ${{ matrix.arch }}
|
||||
needs: version
|
||||
needs: [version, icons]
|
||||
if: needs.version.outputs.build == 'true'
|
||||
# Must stay a GitHub-hosted runner, like the `version` job before it: SignPath's open-source
|
||||
# policy only signs requests whose jobs leading up to them all ran on GitHub-hosted runners.
|
||||
# The installer icon is the committed assets/icons/app/windows/exports/solidcraft.ico.
|
||||
# Must stay a GitHub-hosted runner, like every job before it: SignPath's open-source policy
|
||||
# only signs builds whose jobs all ran on GitHub-hosted runners.
|
||||
runs-on: windows-latest
|
||||
# actions: read lets the SignPath action read this job's details and fetch the uploaded
|
||||
# artifact with GITHUB_TOKEN (the workflow default above grants contents: read only).
|
||||
@@ -481,6 +498,10 @@ jobs:
|
||||
SIGNPATH_PROJECT_SLUG: solidcraft
|
||||
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@1.98.0
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
@@ -495,6 +516,8 @@ jobs:
|
||||
- name: Build and package (msi, portable zip)
|
||||
if: env.SIGN_WINDOWS != 'true'
|
||||
shell: pwsh
|
||||
env:
|
||||
SOLIDCRAFT_ICONS: ${{ github.workspace }}/icons
|
||||
run: ./packaging/windows/package.ps1 -Arch ${{ matrix.arch }}
|
||||
|
||||
# ---- signed path ---------------------------------------------------------------------------
|
||||
@@ -508,6 +531,8 @@ jobs:
|
||||
- name: Build and stage the binaries for signing
|
||||
if: env.SIGN_WINDOWS == 'true'
|
||||
shell: pwsh
|
||||
env:
|
||||
SOLIDCRAFT_ICONS: ${{ github.workspace }}/icons
|
||||
run: ./packaging/windows/package.ps1 -Arch ${{ matrix.arch }} -Phase stage
|
||||
- name: Upload the unsigned binaries
|
||||
id: unsigned-exes
|
||||
@@ -551,6 +576,8 @@ jobs:
|
||||
- name: Package the signed binaries (msi, portable zip)
|
||||
if: env.SIGN_WINDOWS == 'true'
|
||||
shell: pwsh
|
||||
env:
|
||||
SOLIDCRAFT_ICONS: ${{ github.workspace }}/icons
|
||||
run: ./packaging/windows/package.ps1 -Arch ${{ matrix.arch }} -Phase package
|
||||
- name: Upload the unsigned MSI
|
||||
id: unsigned-msi
|
||||
@@ -600,7 +627,7 @@ jobs:
|
||||
name: Web (wasm)
|
||||
needs: version
|
||||
if: needs.version.outputs.build == 'true'
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
SOLIDCRAFT_VERSION: ${{ needs.version.outputs.version }}
|
||||
TRUNK_VERSION: 0.21.14
|
||||
@@ -629,7 +656,7 @@ jobs:
|
||||
needs: [version, linux, flatpak, arch, snap, freebsd, macos, windows, web]
|
||||
# Not for the release gate (packages_only): it builds and checks the packages, nothing more.
|
||||
if: needs.version.outputs.build == 'true' && !inputs.packages_only
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
env:
|
||||
|
||||
@@ -2653,7 +2653,7 @@
|
||||
"prefs.design_tools.imported_files": "الملفات المستوردة",
|
||||
"prefs.design_tools.note": "ملاحظة",
|
||||
"prefs.design_tools.off": "إيقاف",
|
||||
"prefs.design_tools.placement_group": "الميزات الموضوعة",
|
||||
"prefs.design_tools.placement_group": "مسند، نتوء، تعشيق مرن",
|
||||
"prefs.design_tools.rows": "صفوف",
|
||||
"prefs.devices": "الفأرة ثلاثية الأبعاد",
|
||||
"prefs.dialog_buttons": "أزرار مربعات الحوار",
|
||||
@@ -6070,7 +6070,7 @@
|
||||
"ui.pitch": "الخطوة",
|
||||
"ui.placement": "الموضع",
|
||||
"ui.placement_handles": "مقابض الموضع",
|
||||
"ui.placement_handles_tip": "علامة لسحب الميزة عبر النموذج، وحلقة لتدويرها حيث يمكن تدوير الميزة.",
|
||||
"ui.placement_handles_tip": "علامة لسحب الميزة عبر النموذج وحلقة لتدويرها.",
|
||||
"ui.plane": "المستوى",
|
||||
"ui.plane_at_angle": "مستوى بزاوية",
|
||||
"ui.plugins": "الإضافات",
|
||||
|
||||
@@ -3384,7 +3384,7 @@
|
||||
"ui.pitch": "Pitch",
|
||||
"ui.placement": "Placement",
|
||||
"ui.placement_handles": "Placement handles",
|
||||
"ui.placement_handles_tip": "Маркер за влачене на елемента по модела и, където елементът може да се завърта, пръстен за завъртането му.",
|
||||
"ui.placement_handles_tip": "Handles on the preview of a Rest, Boss or Snap Fit: drag the marker to move it across the model, turn it by its ring",
|
||||
"ui.plane": "Plane",
|
||||
"ui.plane_at_angle": "Plane at Angle",
|
||||
"ui.plugins": "Plugins",
|
||||
|
||||
@@ -4907,7 +4907,7 @@
|
||||
"ui.pitch": "Korak",
|
||||
"ui.placement": "Postavljanje",
|
||||
"ui.placement_handles": "Ručice za postavljanje",
|
||||
"ui.placement_handles_tip": "Oznaka za povlačenje operacije po modelu i, gdje se operacija može okretati, prsten za njeno okretanje.",
|
||||
"ui.placement_handles_tip": "Ručice na pregledu oslonca, ispusta ili škljocajućeg spoja: povucite oznaku da je pomjerite po modelu, okrenite je njenim prstenom",
|
||||
"ui.plane": "Ravan",
|
||||
"ui.plane_at_angle": "Ravan pod uglom",
|
||||
"ui.plugins": "Proširenja",
|
||||
|
||||
@@ -2653,7 +2653,7 @@
|
||||
"prefs.design_tools.imported_files": "Fitxers importats",
|
||||
"prefs.design_tools.note": "Nota",
|
||||
"prefs.design_tools.off": "Desactivat",
|
||||
"prefs.design_tools.placement_group": "Operacions col·locades",
|
||||
"prefs.design_tools.placement_group": "Repòs, Resalt, Encaix",
|
||||
"prefs.design_tools.rows": "Files",
|
||||
"prefs.devices": "Ratolí 3D",
|
||||
"prefs.dialog_buttons": "Botons de diàleg",
|
||||
@@ -6070,7 +6070,7 @@
|
||||
"ui.pitch": "Inclinació",
|
||||
"ui.placement": "Col·locació",
|
||||
"ui.placement_handles": "Tiradors de col·locació",
|
||||
"ui.placement_handles_tip": "Un marcador per arrossegar l'operació pel model i, on l'operació es pot girar, un anell per girar-la.",
|
||||
"ui.placement_handles_tip": "Un marcador per arrossegar l'operació pel model i un anell per girar-la.",
|
||||
"ui.plane": "Pla",
|
||||
"ui.plane_at_angle": "Pla amb angle",
|
||||
"ui.plugins": "Complements",
|
||||
|
||||
@@ -2562,7 +2562,7 @@
|
||||
"prefs.design_tools.imported_files": "Importované soubory",
|
||||
"prefs.design_tools.note": "Poznámka",
|
||||
"prefs.design_tools.off": "Vypnuto",
|
||||
"prefs.design_tools.placement_group": "Umisťované prvky",
|
||||
"prefs.design_tools.placement_group": "Opěra, Nálitek, Západka",
|
||||
"prefs.design_tools.rows": "Řádky",
|
||||
"prefs.devices": "3D myš",
|
||||
"prefs.dialog_buttons": "Tlačítka dialogu",
|
||||
@@ -5824,7 +5824,7 @@
|
||||
"ui.pitch": "Stoupání",
|
||||
"ui.placement": "Umístění",
|
||||
"ui.placement_handles": "Úchyty umístění",
|
||||
"ui.placement_handles_tip": "Značka pro přetažení prvku po modelu a tam, kde se prvek může otáčet, kroužek pro jeho otočení.",
|
||||
"ui.placement_handles_tip": "Úchyty na náhledu opěry, nálitku nebo západky: tažením značky ji přesunete po modelu, prstencem ji otočíte",
|
||||
"ui.plane": "Rovina",
|
||||
"ui.plane_at_angle": "Rovina pod úhlem",
|
||||
"ui.plugins": "Moduly plug-in",
|
||||
|
||||
@@ -4916,7 +4916,7 @@
|
||||
"ui.pitch": "Stigning",
|
||||
"ui.placement": "Placering",
|
||||
"ui.placement_handles": "Placeringshåndtag",
|
||||
"ui.placement_handles_tip": "En markør til at trække featuren hen over modellen og, hvor featuren kan drejes, en ring til at dreje den.",
|
||||
"ui.placement_handles_tip": "Håndtag på forhåndsvisningen af en Understøtning, Skruebøsning eller Snapforbindelse: Træk i markøren for at flytte den hen over modellen, og drej den ved dens ring",
|
||||
"ui.plane": "Plan",
|
||||
"ui.plane_at_angle": "Plan i vinkel",
|
||||
"ui.plugins": "Plugins",
|
||||
|
||||
@@ -2424,7 +2424,7 @@
|
||||
"prefs.design_tools.imported_files": "Importierte Dateien",
|
||||
"prefs.design_tools.note": "Hinweis",
|
||||
"prefs.design_tools.off": "Aus",
|
||||
"prefs.design_tools.placement_group": "Platzierte Funktionen",
|
||||
"prefs.design_tools.placement_group": "Auflage, Zapfen, Schnappverbindung",
|
||||
"prefs.design_tools.rows": "Zeilen",
|
||||
"prefs.devices": "3D-Maus",
|
||||
"prefs.dialog_buttons": "Dialogschaltflächen",
|
||||
@@ -5758,7 +5758,7 @@
|
||||
"ui.pitch": "Steigung",
|
||||
"ui.placement": "Platzierung",
|
||||
"ui.placement_handles": "Platzierungsgriffe",
|
||||
"ui.placement_handles_tip": "Eine Markierung zum Verschieben des Features über das Modell und, wo sich das Feature drehen lässt, ein Ring zum Drehen.",
|
||||
"ui.placement_handles_tip": "Eine Markierung zum Verschieben des Features über das Modell und ein Ring zum Drehen.",
|
||||
"ui.plane": "Ebene",
|
||||
"ui.plane_at_angle": "Ebene im Winkel",
|
||||
"ui.plugins": "Plugins",
|
||||
|
||||
@@ -2653,7 +2653,7 @@
|
||||
"prefs.design_tools.imported_files": "Imported files",
|
||||
"prefs.design_tools.note": "Note",
|
||||
"prefs.design_tools.off": "Off",
|
||||
"prefs.design_tools.placement_group": "Placed features",
|
||||
"prefs.design_tools.placement_group": "Rest, Boss, Snap Fit",
|
||||
"prefs.design_tools.rows": "Rows",
|
||||
"prefs.devices": "3D mouse",
|
||||
"prefs.dialog_buttons": "Dialog buttons",
|
||||
@@ -6070,7 +6070,7 @@
|
||||
"ui.pitch": "Pitch",
|
||||
"ui.placement": "Placement",
|
||||
"ui.placement_handles": "Placement handles",
|
||||
"ui.placement_handles_tip": "A marker to drag the feature across the model and, where the feature can turn, a ring to turn it.",
|
||||
"ui.placement_handles_tip": "A marker to drag the feature across the model and a ring to turn it.",
|
||||
"ui.plane": "Plane",
|
||||
"ui.plane_at_angle": "Plane at Angle",
|
||||
"ui.plugins": "Plugins",
|
||||
|
||||
@@ -2653,7 +2653,7 @@
|
||||
"prefs.design_tools.imported_files": "Archivos importados",
|
||||
"prefs.design_tools.note": "Nota",
|
||||
"prefs.design_tools.off": "Desactivado",
|
||||
"prefs.design_tools.placement_group": "Operaciones colocadas",
|
||||
"prefs.design_tools.placement_group": "Apoyo, Saliente, Encaje",
|
||||
"prefs.design_tools.rows": "Filas",
|
||||
"prefs.devices": "Ratón 3D",
|
||||
"prefs.dialog_buttons": "Botones de diálogo",
|
||||
@@ -6070,7 +6070,7 @@
|
||||
"ui.pitch": "Inclinación",
|
||||
"ui.placement": "Colocación",
|
||||
"ui.placement_handles": "Tiradores de colocación",
|
||||
"ui.placement_handles_tip": "Un marcador para arrastrar la operación por el modelo y, donde la operación puede girar, un anillo para girarla.",
|
||||
"ui.placement_handles_tip": "Tiradores en la vista previa de un Refuerzo, Saliente o Encaje a presión: arrastra el marcador para moverlo por el modelo, gíralo por su anillo.",
|
||||
"ui.plane": "Plano",
|
||||
"ui.plane_at_angle": "Plano en ángulo",
|
||||
"ui.plugins": "Complementos",
|
||||
|
||||
@@ -2581,7 +2581,7 @@
|
||||
"prefs.design_tools.imported_files": "Tuodut tiedostot",
|
||||
"prefs.design_tools.note": "Huomautus",
|
||||
"prefs.design_tools.off": "Pois",
|
||||
"prefs.design_tools.placement_group": "Sijoitettavat piirteet",
|
||||
"prefs.design_tools.placement_group": "Tuki, Tappi, Napsautusliitos",
|
||||
"prefs.design_tools.rows": "Rivit",
|
||||
"prefs.devices": "3D-hiiri",
|
||||
"prefs.dialog_buttons": "Valintaikkunan painikkeet",
|
||||
@@ -5850,7 +5850,7 @@
|
||||
"ui.pitch": "Nousu",
|
||||
"ui.placement": "Sijoitus",
|
||||
"ui.placement_handles": "Sijoituskahvat",
|
||||
"ui.placement_handles_tip": "Merkki piirteen vetämiseen mallin yli ja, jos piirrettä voi kääntää, rengas sen kääntämiseen.",
|
||||
"ui.placement_handles_tip": "Kahvat tuen, tapin tai napsautusliitoksen esikatselussa: siirrä merkkiä mallin pinnalla vetämällä ja käännä sitä renkaasta",
|
||||
"ui.plane": "Taso",
|
||||
"ui.plane_at_angle": "Taso kulmassa",
|
||||
"ui.plugins": "Laajennukset",
|
||||
|
||||
@@ -2343,7 +2343,7 @@
|
||||
"prefs.design_tools.imported_files": "Fichiers importés",
|
||||
"prefs.design_tools.note": "Remarque",
|
||||
"prefs.design_tools.off": "Désactivé",
|
||||
"prefs.design_tools.placement_group": "Fonctions placées",
|
||||
"prefs.design_tools.placement_group": "Appui, Bossage, Clip",
|
||||
"prefs.design_tools.rows": "Lignes",
|
||||
"prefs.devices": "Souris 3D",
|
||||
"prefs.dialog_buttons": "Boutons OK",
|
||||
@@ -5287,7 +5287,7 @@
|
||||
"ui.pitch": "Pas",
|
||||
"ui.placement": "Placement",
|
||||
"ui.placement_handles": "Poignées de placement",
|
||||
"ui.placement_handles_tip": "Un repère pour faire glisser la fonction sur le modèle et, là où la fonction peut pivoter, un anneau pour la faire pivoter.",
|
||||
"ui.placement_handles_tip": "Poignées sur l'aperçu d'un appui, d'un bossage ou d'un clip : faire glisser le repère pour le déplacer sur le modèle, le tourner par son anneau",
|
||||
"ui.plane": "Plan",
|
||||
"ui.plane_at_angle": "Plan incliné",
|
||||
"ui.plugins": "Modules externes",
|
||||
|
||||
@@ -4915,7 +4915,7 @@
|
||||
"ui.pitch": "Paso",
|
||||
"ui.placement": "Colocación",
|
||||
"ui.placement_handles": "Asas de colocación",
|
||||
"ui.placement_handles_tip": "Un marcador para arrastrar a operación polo modelo e, onde a operación pode xirar, un anel para xirala.",
|
||||
"ui.placement_handles_tip": "Asas na vista previa dun Apoio, un Saínte ou un Encaixe a presión: arrastra o marcador para movelo polo modelo, xírao polo seu anel",
|
||||
"ui.plane": "Plano",
|
||||
"ui.plane_at_angle": "Plano en ángulo",
|
||||
"ui.plugins": "Complementos",
|
||||
|
||||
@@ -2653,7 +2653,7 @@
|
||||
"prefs.design_tools.imported_files": "קבצים מיובאים",
|
||||
"prefs.design_tools.note": "הערה",
|
||||
"prefs.design_tools.off": "כבוי",
|
||||
"prefs.design_tools.placement_group": "פיצ'רים ממוקמים",
|
||||
"prefs.design_tools.placement_group": "משען, בליטה, חיבור נצמד",
|
||||
"prefs.design_tools.rows": "שורות",
|
||||
"prefs.devices": "עכבר תלת־ממדי",
|
||||
"prefs.dialog_buttons": "לחצני תיבות דו־שיח",
|
||||
@@ -6070,7 +6070,7 @@
|
||||
"ui.pitch": "פסיעה",
|
||||
"ui.placement": "מיקום",
|
||||
"ui.placement_handles": "ידיות מיקום",
|
||||
"ui.placement_handles_tip": "סמן לגרירת התכונה על המודל, וטבעת לסיבובה במקום שבו אפשר לסובב את התכונה.",
|
||||
"ui.placement_handles_tip": "סמן לגרירת התכונה על המודל וטבעת לסיבובה.",
|
||||
"ui.plane": "מישור",
|
||||
"ui.plane_at_angle": "מישור בזווית",
|
||||
"ui.plugins": "תוספים",
|
||||
|
||||
@@ -4907,7 +4907,7 @@
|
||||
"ui.pitch": "Korak",
|
||||
"ui.placement": "Postavljanje",
|
||||
"ui.placement_handles": "Ručice za postavljanje",
|
||||
"ui.placement_handles_tip": "Oznaka za povlačenje značajke po modelu i, gdje se značajka može okretati, prsten za njezino okretanje.",
|
||||
"ui.placement_handles_tip": "Ručice na pregledu oslonca, ispusta ili škljocajućeg spoja: povucite oznaku kako biste je pomaknuli po modelu, okrenite je njezinim prstenom",
|
||||
"ui.plane": "Ravnina",
|
||||
"ui.plane_at_angle": "Ravnina pod kutom",
|
||||
"ui.plugins": "Proširenja",
|
||||
|
||||
@@ -4915,7 +4915,7 @@
|
||||
"ui.pitch": "Emelkedés",
|
||||
"ui.placement": "Elhelyezés",
|
||||
"ui.placement_handles": "Elhelyezési fogantyúk",
|
||||
"ui.placement_handles_tip": "Egy jelölő az elem modellen való húzásához és, ahol az elem elforgatható, egy gyűrű az elforgatásához.",
|
||||
"ui.placement_handles_tip": "Fogantyúk egy Támasz, Dudor vagy Pattintós illesztés előnézetén: húzza a jelölőt a modell fölött, a gyűrűjénél fogva forgassa el",
|
||||
"ui.plane": "Sík",
|
||||
"ui.plane_at_angle": "Szögben álló sík",
|
||||
"ui.plugins": "Bővítmények",
|
||||
|
||||
@@ -4915,7 +4915,7 @@
|
||||
"ui.pitch": "Pitch",
|
||||
"ui.placement": "Penempatan",
|
||||
"ui.placement_handles": "Handle penempatan",
|
||||
"ui.placement_handles_tip": "Penanda untuk menyeret fitur di atas model dan, jika fitur dapat diputar, cincin untuk memutarnya.",
|
||||
"ui.placement_handles_tip": "Handle pada pratinjau Rest, Boss, atau Snap Fit: seret penanda untuk memindahkannya di model, putar lewat cincinnya",
|
||||
"ui.plane": "Bidang",
|
||||
"ui.plane_at_angle": "Bidang Bersudut",
|
||||
"ui.plugins": "Plugin",
|
||||
|
||||
@@ -2588,7 +2588,7 @@
|
||||
"prefs.design_tools.imported_files": "File importati",
|
||||
"prefs.design_tools.note": "Avvisa",
|
||||
"prefs.design_tools.off": "Disattivato",
|
||||
"prefs.design_tools.placement_group": "Lavorazioni posizionate",
|
||||
"prefs.design_tools.placement_group": "Appoggio, Risalto, Aggancio a scatto",
|
||||
"prefs.design_tools.rows": "Righe",
|
||||
"prefs.devices": "Mouse 3D",
|
||||
"prefs.dialog_buttons": "Pulsanti OK",
|
||||
@@ -5873,7 +5873,7 @@
|
||||
"ui.pitch": "Passo",
|
||||
"ui.placement": "Posizionamento",
|
||||
"ui.placement_handles": "Maniglie di posizionamento",
|
||||
"ui.placement_handles_tip": "Un indicatore per trascinare la lavorazione sul modello e, dove la lavorazione può ruotare, un anello per ruotarla.",
|
||||
"ui.placement_handles_tip": "Maniglie sull'anteprima di un Appoggio, Risalto o Aggancio a scatto: trascina il marcatore per spostarlo sul modello, ruotalo dal suo anello",
|
||||
"ui.plane": "Piano",
|
||||
"ui.plane_at_angle": "Piano inclinato",
|
||||
"ui.plugins": "Plugin",
|
||||
|
||||
@@ -2653,7 +2653,7 @@
|
||||
"prefs.design_tools.imported_files": "インポートしたファイル",
|
||||
"prefs.design_tools.note": "通知",
|
||||
"prefs.design_tools.off": "オフ",
|
||||
"prefs.design_tools.placement_group": "配置するフィーチャー",
|
||||
"prefs.design_tools.placement_group": "レスト、ボス、スナップフィット",
|
||||
"prefs.design_tools.rows": "行数",
|
||||
"prefs.devices": "3D マウス",
|
||||
"prefs.dialog_buttons": "ダイアログのボタン",
|
||||
@@ -6070,7 +6070,7 @@
|
||||
"ui.pitch": "ピッチ",
|
||||
"ui.placement": "配置",
|
||||
"ui.placement_handles": "配置ハンドル",
|
||||
"ui.placement_handles_tip": "モデル上でフィーチャーをドラッグするマーカーと、フィーチャーを回転できる場合は回転させるリング。",
|
||||
"ui.placement_handles_tip": "モデル上でフィーチャーをドラッグするマーカーと、回転させるリング。",
|
||||
"ui.plane": "平面",
|
||||
"ui.plane_at_angle": "傾斜平面",
|
||||
"ui.plugins": "プラグイン",
|
||||
|
||||
@@ -5073,7 +5073,7 @@
|
||||
"ui.pitch": "ბიჯი",
|
||||
"ui.placement": "განთავსება",
|
||||
"ui.placement_handles": "განთავსების სახელურები",
|
||||
"ui.placement_handles_tip": "მარკერი ოპერაციის მოდელზე გადასატანად და, სადაც ოპერაციის მობრუნება შეიძლება, რგოლი მის მოსაბრუნებლად.",
|
||||
"ui.placement_handles_tip": "სახელურები საყრდენის, ბოსის ან ჩამკეტის გადახედვაზე: მარკერი გადაათრიეთ მოდელზე მის გადასაადგილებლად, ხოლო რგოლით შეაბრუნეთ",
|
||||
"ui.plane": "სიბრტყე",
|
||||
"ui.plane_at_angle": "სიბრტყე კუთხით",
|
||||
"ui.plugins": "პლაგინები",
|
||||
|
||||
@@ -1996,7 +1996,7 @@
|
||||
"prefs.design_tools.imported_files": "가져온 파일",
|
||||
"prefs.design_tools.note": "알림",
|
||||
"prefs.design_tools.off": "끄기",
|
||||
"prefs.design_tools.placement_group": "배치 피처",
|
||||
"prefs.design_tools.placement_group": "레스트, 보스, 스냅 핏",
|
||||
"prefs.design_tools.rows": "행",
|
||||
"prefs.devices": "3D 마우스",
|
||||
"prefs.dialog_buttons": "대화 상자 버튼",
|
||||
@@ -5195,7 +5195,7 @@
|
||||
"ui.pipe": "파이프",
|
||||
"ui.pitch": "피치",
|
||||
"ui.placement": "배치",
|
||||
"ui.placement_handles_tip": "모델 위로 피처를 드래그하는 마커와, 피처를 회전할 수 있는 경우 회전하는 링입니다.",
|
||||
"ui.placement_handles_tip": "모델 위로 피처를 드래그하는 마커와 회전하는 링입니다.",
|
||||
"ui.plane": "평면",
|
||||
"ui.plane_at_angle": "각도 평면",
|
||||
"ui.plugins": "플러그인",
|
||||
|
||||
@@ -2582,7 +2582,7 @@
|
||||
"prefs.design_tools.imported_files": "Importētie faili",
|
||||
"prefs.design_tools.note": "Piezīme",
|
||||
"prefs.design_tools.off": "Izslēgts",
|
||||
"prefs.design_tools.placement_group": "Izvietojamās operācijas",
|
||||
"prefs.design_tools.placement_group": "Atbalsts, izcilnis, aizspiedsavienojums",
|
||||
"prefs.design_tools.rows": "Rindas",
|
||||
"prefs.devices": "3D pele",
|
||||
"prefs.dialog_buttons": "Dialoglodziņu pogas",
|
||||
@@ -5903,7 +5903,7 @@
|
||||
"ui.pitch": "Solis",
|
||||
"ui.placement": "Izvietojums",
|
||||
"ui.placement_handles": "Izvietošanas turi",
|
||||
"ui.placement_handles_tip": "Marķieris operācijas vilkšanai pa modeli un, kur operāciju var pagriezt, gredzens tās pagriešanai.",
|
||||
"ui.placement_handles_tip": "Marķieris operācijas vilkšanai pa modeli un gredzens tās pagriešanai.",
|
||||
"ui.plane": "Plakne",
|
||||
"ui.plane_at_angle": "Plakne leņķī",
|
||||
"ui.plugins": "Spraudņi",
|
||||
|
||||
@@ -4915,7 +4915,7 @@
|
||||
"ui.pitch": "Pic",
|
||||
"ui.placement": "Penempatan",
|
||||
"ui.placement_handles": "Pemegang penempatan",
|
||||
"ui.placement_handles_tip": "Penanda untuk menyeret ciri merentasi model dan, jika ciri boleh dipusingkan, gelang untuk memusingkannya.",
|
||||
"ui.placement_handles_tip": "Pemegang pada pratonton Penyokong, Bonggol atau Sambungan Sentap: seret penanda untuk menggerakkannya merentasi model, putarkannya melalui cincinnya",
|
||||
"ui.plane": "Satah",
|
||||
"ui.plane_at_angle": "Satah Bersudut",
|
||||
"ui.plugins": "Pemalam",
|
||||
|
||||
@@ -4915,7 +4915,7 @@
|
||||
"ui.pitch": "Stigning",
|
||||
"ui.placement": "Plassering",
|
||||
"ui.placement_handles": "Plasseringshåndtak",
|
||||
"ui.placement_handles_tip": "En markør for å dra funksjonen over modellen og, der funksjonen kan dreies, en ring for å dreie den.",
|
||||
"ui.placement_handles_tip": "Håndtak på forhåndsvisningen av et anlegg, skruefeste eller snap-fit: dra markøren for å flytte den over modellen, drei den med ringen",
|
||||
"ui.plane": "Plan",
|
||||
"ui.plane_at_angle": "Plan i vinkel",
|
||||
"ui.plugins": "Programtillegg",
|
||||
|
||||
@@ -4929,7 +4929,7 @@
|
||||
"ui.pitch": "Steek",
|
||||
"ui.placement": "Plaatsing",
|
||||
"ui.placement_handles": "Plaatsingsgrepen",
|
||||
"ui.placement_handles_tip": "Een markering om het kenmerk over het model te slepen en, waar het kenmerk kan draaien, een ring om het te draaien.",
|
||||
"ui.placement_handles_tip": "Grepen op het voorbeeld van een Rustvlak, Nok of Snapverbinding: sleep de markering om hem over het model te verplaatsen, draai hem aan de ring",
|
||||
"ui.plane": "Vlak",
|
||||
"ui.plane_at_angle": "Vlak onder hoek",
|
||||
"ui.plugins": "Plug-ins",
|
||||
|
||||
@@ -2653,7 +2653,7 @@
|
||||
"prefs.design_tools.imported_files": "Zaimportowane pliki",
|
||||
"prefs.design_tools.note": "Uwaga",
|
||||
"prefs.design_tools.off": "Wyłączone",
|
||||
"prefs.design_tools.placement_group": "Umieszczane operacje",
|
||||
"prefs.design_tools.placement_group": "Podparcie, Występ, Zatrzask",
|
||||
"prefs.design_tools.rows": "Wiersze",
|
||||
"prefs.devices": "Mysz 3D",
|
||||
"prefs.dialog_buttons": "Przyciski okien dialogowych",
|
||||
@@ -6070,7 +6070,7 @@
|
||||
"ui.pitch": "Skok",
|
||||
"ui.placement": "Umiejscowienie",
|
||||
"ui.placement_handles": "Uchwyty umieszczania",
|
||||
"ui.placement_handles_tip": "Znacznik do przeciągania operacji po modelu i, tam gdzie operację można obracać, pierścień do jej obracania.",
|
||||
"ui.placement_handles_tip": "Uchwyty na podglądzie podparcia, występu lub zatrzasku: przeciągnij znacznik, aby przesunąć element po modelu, obróć go pierścieniem",
|
||||
"ui.plane": "Płaszczyzna",
|
||||
"ui.plane_at_angle": "Płaszczyzna pod kątem",
|
||||
"ui.plugins": "Wtyczki",
|
||||
|
||||
@@ -2588,7 +2588,7 @@
|
||||
"prefs.design_tools.imported_files": "Arquivos importados",
|
||||
"prefs.design_tools.note": "Observação",
|
||||
"prefs.design_tools.off": "Desativado",
|
||||
"prefs.design_tools.placement_group": "Recursos posicionados",
|
||||
"prefs.design_tools.placement_group": "Apoio, Ressalto, Encaixe por pressão",
|
||||
"prefs.design_tools.rows": "Linhas",
|
||||
"prefs.devices": "Mouse 3D",
|
||||
"prefs.dialog_buttons": "Botões das caixas de diálogo",
|
||||
@@ -5927,7 +5927,7 @@
|
||||
"ui.pitch": "Passo",
|
||||
"ui.placement": "Posicionamento",
|
||||
"ui.placement_handles": "Alças de posicionamento",
|
||||
"ui.placement_handles_tip": "Um marcador para arrastar o recurso pelo modelo e, onde o recurso pode girar, um anel para girá-lo.",
|
||||
"ui.placement_handles_tip": "Um marcador para arrastar o recurso pelo modelo e um anel para girá-lo.",
|
||||
"ui.plane": "Plano",
|
||||
"ui.plane_at_angle": "Plano em ângulo",
|
||||
"ui.plugins": "Plug-ins",
|
||||
|
||||
@@ -4915,7 +4915,7 @@
|
||||
"ui.pitch": "Pas",
|
||||
"ui.placement": "Plasare",
|
||||
"ui.placement_handles": "Mânere de plasare",
|
||||
"ui.placement_handles_tip": "Un marcaj pentru a trage operația pe model și, acolo unde operația se poate roti, un inel pentru a o roti.",
|
||||
"ui.placement_handles_tip": "Mânere pe previzualizarea unui Reazem, Bosaj sau a unei Prinderi cu clichet: trageți marcajul pentru a-l muta pe model, rotiți-l cu inelul său",
|
||||
"ui.plane": "Plan",
|
||||
"ui.plane_at_angle": "Plan în unghi",
|
||||
"ui.plugins": "Pluginuri",
|
||||
|
||||
@@ -4870,7 +4870,7 @@
|
||||
"ui.pitch": "Шаг",
|
||||
"ui.placement": "Размещение",
|
||||
"ui.placement_handles": "Манипуляторы размещения",
|
||||
"ui.placement_handles_tip": "Маркер для перетаскивания операции по модели и, где операцию можно поворачивать, кольцо для её поворота.",
|
||||
"ui.placement_handles_tip": "Манипуляторы на предпросмотре «Опоры», «Бобышки» или «Защёлки»: перетащите маркер, чтобы переместить его по модели, поверните за кольцо",
|
||||
"ui.plane": "Плоскость",
|
||||
"ui.plane_at_angle": "Плоскость под углом",
|
||||
"ui.plugins": "Плагины",
|
||||
|
||||
@@ -4907,7 +4907,7 @@
|
||||
"ui.pitch": "Корак",
|
||||
"ui.placement": "Постављање",
|
||||
"ui.placement_handles": "Ручице за постављање",
|
||||
"ui.placement_handles_tip": "Ознака за превлачење операције по моделу и, где се операција може окретати, прстен за њено окретање.",
|
||||
"ui.placement_handles_tip": "Ручице на прегледу ослонца, испуста или шкљоцајућег споја: превуците ознаку да је померите по моделу, окрените је њеним прстеном",
|
||||
"ui.plane": "Раван",
|
||||
"ui.plane_at_angle": "Раван под углом",
|
||||
"ui.plugins": "Проширења",
|
||||
|
||||
@@ -4916,7 +4916,7 @@
|
||||
"ui.pitch": "Stigning",
|
||||
"ui.placement": "Placering",
|
||||
"ui.placement_handles": "Placeringshandtag",
|
||||
"ui.placement_handles_tip": "En markör för att dra funktionen över modellen och, där funktionen kan vridas, en ring för att vrida den.",
|
||||
"ui.placement_handles_tip": "Handtag på förhandsvisningen av ett stöd, nav eller snäppfäste: dra markören för att flytta det över modellen, vrid det med dess ring",
|
||||
"ui.plane": "Plan",
|
||||
"ui.plane_at_angle": "Plan i vinkel",
|
||||
"ui.plugins": "Insticksmoduler",
|
||||
|
||||
@@ -4916,7 +4916,7 @@
|
||||
"ui.pitch": "Adım",
|
||||
"ui.placement": "Yerleşim",
|
||||
"ui.placement_handles": "Yerleşim tutamaçları",
|
||||
"ui.placement_handles_tip": "Özelliği model üzerinde sürüklemek için bir işaretçi ve, özellik döndürülebiliyorsa, döndürmek için bir halka.",
|
||||
"ui.placement_handles_tip": "Bir Dayama, Kabarıklık veya Kilit Geçme önizlemesindeki tutamaçlar: işaretçiyi model üzerinde hareket ettirmek için sürükleyin, halkasıyla döndürün",
|
||||
"ui.plane": "Düzlem",
|
||||
"ui.plane_at_angle": "Açılı düzlem",
|
||||
"ui.plugins": "Eklentiler",
|
||||
|
||||
@@ -2623,7 +2623,7 @@
|
||||
"prefs.design_tools.imported_files": "Імпортовані файли",
|
||||
"prefs.design_tools.note": "Примітка",
|
||||
"prefs.design_tools.off": "Вимк.",
|
||||
"prefs.design_tools.placement_group": "Розміщувані операції",
|
||||
"prefs.design_tools.placement_group": "Упор, Бобишка, Защіпкове з'єднання",
|
||||
"prefs.design_tools.rows": "Рядки",
|
||||
"prefs.devices": "3D-миша",
|
||||
"prefs.dialog_buttons": "Кнопки діалогу",
|
||||
@@ -5989,7 +5989,7 @@
|
||||
"ui.pitch": "Крок",
|
||||
"ui.placement": "Розміщення",
|
||||
"ui.placement_handles": "Ручки розміщення",
|
||||
"ui.placement_handles_tip": "Маркер, щоб перетягнути елемент по моделі, і, де елемент можна повернути, кільце, щоб його повернути.",
|
||||
"ui.placement_handles_tip": "Маркер, щоб перетягнути елемент по моделі, і кільце, щоб його повернути.",
|
||||
"ui.plane": "Площина",
|
||||
"ui.plane_at_angle": "Площина під кутом",
|
||||
"ui.plugins": "Плагіни",
|
||||
|
||||
@@ -2653,7 +2653,7 @@
|
||||
"prefs.design_tools.imported_files": "Tệp đã nhập",
|
||||
"prefs.design_tools.note": "Thông báo",
|
||||
"prefs.design_tools.off": "Tắt",
|
||||
"prefs.design_tools.placement_group": "Các tính năng đặt vị trí",
|
||||
"prefs.design_tools.placement_group": "Gối đỡ, Trụ, Khớp gài",
|
||||
"prefs.design_tools.rows": "Hàng",
|
||||
"prefs.devices": "Chuột 3D",
|
||||
"prefs.dialog_buttons": "Nút hộp thoại",
|
||||
@@ -6070,7 +6070,7 @@
|
||||
"ui.pitch": "Bước xoắn",
|
||||
"ui.placement": "Vị trí đặt",
|
||||
"ui.placement_handles": "Tay nắm định vị",
|
||||
"ui.placement_handles_tip": "Dấu để kéo đặc trưng trên mô hình và, ở nơi đặc trưng có thể xoay, vòng để xoay.",
|
||||
"ui.placement_handles_tip": "Dấu để kéo đặc trưng trên mô hình và vòng để xoay.",
|
||||
"ui.plane": "Mặt phẳng",
|
||||
"ui.plane_at_angle": "Mặt phẳng theo góc",
|
||||
"ui.plugins": "Plugin",
|
||||
|
||||
@@ -2588,7 +2588,7 @@
|
||||
"prefs.design_tools.imported_files": "導入的檔案",
|
||||
"prefs.design_tools.note": "提示",
|
||||
"prefs.design_tools.off": "關閉",
|
||||
"prefs.design_tools.placement_group": "放置的特徵",
|
||||
"prefs.design_tools.placement_group": "支座、凸台、卡扣",
|
||||
"prefs.design_tools.rows": "行數",
|
||||
"prefs.devices": "3D 游標",
|
||||
"prefs.dialog_buttons": "對話框按鈕",
|
||||
@@ -5929,7 +5929,7 @@
|
||||
"ui.pitch": "螺距",
|
||||
"ui.placement": "放置",
|
||||
"ui.placement_handles": "放置手柄",
|
||||
"ui.placement_handles_tip": "用於在模型上拖動特徵的標記,以及在特徵可旋轉時用於旋轉的圓環。",
|
||||
"ui.placement_handles_tip": "用於在模型上拖動特徵的標記和用於旋轉的圓環。",
|
||||
"ui.plane": "平面",
|
||||
"ui.plane_at_angle": "角度平面",
|
||||
"ui.plugins": "插件",
|
||||
|
||||
@@ -2653,7 +2653,7 @@
|
||||
"prefs.design_tools.imported_files": "导入的文件",
|
||||
"prefs.design_tools.note": "提示",
|
||||
"prefs.design_tools.off": "关闭",
|
||||
"prefs.design_tools.placement_group": "放置的特征",
|
||||
"prefs.design_tools.placement_group": "支座、凸台、卡扣",
|
||||
"prefs.design_tools.rows": "行数",
|
||||
"prefs.devices": "3D 鼠标",
|
||||
"prefs.dialog_buttons": "对话框按钮",
|
||||
@@ -6070,7 +6070,7 @@
|
||||
"ui.pitch": "螺距",
|
||||
"ui.placement": "放置",
|
||||
"ui.placement_handles": "放置手柄",
|
||||
"ui.placement_handles_tip": "用于在模型上拖动特征的标记,以及在特征可旋转时用于旋转的圆环。",
|
||||
"ui.placement_handles_tip": "用于在模型上拖动特征的标记和用于旋转的圆环。",
|
||||
"ui.plane": "平面",
|
||||
"ui.plane_at_angle": "角度平面",
|
||||
"ui.plugins": "插件",
|
||||
|
||||
@@ -20,5 +20,4 @@ from an `App` value the app defines. No app types and no engine.
|
||||
| `auth::tokens_match`, `auth::remove_token_file`, `auth::TokenFile`, `auth::pid_path`, `auth::MIN_TOKEN_LEN` | Constant-time compare and token file housekeeping |
|
||||
| `Effect` | What a request still running does when it finishes: reads only, writes a file, or changes; `when_done(document)` says it |
|
||||
| `server::Hub`, `server::Protocol`, `server::Summary`, `server::Waits`, `server::new_token` | The loopback JSON-lines server behind the token (desktop only); the app says what its requests mean |
|
||||
| `client::Connection`, `client::Request`, `client::CallError`, `client::Waits` | The client (desktop only): the app's token on every request, one bounded round trip, a fresh connection after a lost one, the wait for a request still running (`control.pending`); "not running" told from "lost during the call" |
|
||||
| `ControlRequest`, `ControlResponse`, `DocumentWatch`, `DOCUMENT_WATCH` | A request as the window gets it; the document id and revision subscribed connections wait on |
|
||||
|
||||
@@ -1,616 +0,0 @@
|
||||
//! The control channel's client (docs/control-protocol.md): a connection to an app's loopback
|
||||
//! control port, with the app's token ([`App::client_token`]) on every request, one bounded
|
||||
//! round trip per request, a fresh connection after one is lost, and the wait for a request the
|
||||
//! app says is still running (#1618), through `control.pending`. Its errors tell an app that is
|
||||
//! not running from one that went away during a call ([`CallError`]). What a request means, and
|
||||
//! anything an app adds to its parameters, is the caller's.
|
||||
|
||||
use std::io::{BufRead, BufReader, Read, Write};
|
||||
use std::net::{TcpStream, ToSocketAddrs};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::Effect;
|
||||
use crate::auth::App;
|
||||
|
||||
/// Longest reply line accepted from the app.
|
||||
pub const MAX_REPLY: u64 = 64 << 20;
|
||||
|
||||
/// How long a reply may take: kernel operations (booleans, fillets) on big parts can take a while.
|
||||
pub const READ_TIMEOUT: Duration = Duration::from_secs(300);
|
||||
|
||||
/// How long the app may take to answer a request sent [`Request::once`]: a little longer than its
|
||||
/// control server waits for a long request (3600 s), so its "still running" reply arrives first.
|
||||
pub const ONCE_TIMEOUT: Duration = Duration::from_secs(3720);
|
||||
|
||||
/// How long connecting to the app may take.
|
||||
const CONNECT_TIMEOUT: Duration = Duration::from_millis(800);
|
||||
|
||||
/// How often a long wait is reported ([`Connection::with_progress`]).
|
||||
const PROGRESS_EVERY: Duration = Duration::from_secs(30);
|
||||
|
||||
/// How an error starts when a request is still running in the app after the wait: the request
|
||||
/// was not cancelled and its reply waits on the connection; [`Connection::pending`] gets it.
|
||||
pub const STILL_RUNNING: &str = "still running:";
|
||||
|
||||
/// How long to wait for a request the app says is still running.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Waits {
|
||||
/// How long a call goes on waiting, asking `control.pending`, after the app said its request
|
||||
/// is still running (the app says so after 60 s), before it says so itself.
|
||||
pub pending: Duration,
|
||||
/// How long a later call, or [`Connection::pending`], waits for a request still running from
|
||||
/// an earlier call.
|
||||
pub settle: Duration,
|
||||
/// How often `control.pending` is asked while waiting.
|
||||
pub poll: Duration,
|
||||
}
|
||||
|
||||
impl Default for Waits {
|
||||
fn default() -> Self {
|
||||
Waits { pending: Duration::from_secs(240), settle: Duration::from_secs(60), poll: Duration::from_millis(500) }
|
||||
}
|
||||
}
|
||||
|
||||
/// Why a call has no result. Each holds the message for the caller (an agent reads it).
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum CallError {
|
||||
/// Nothing listens at the address: the app is not running, or it quit before the call was
|
||||
/// sent. The message says how to start it.
|
||||
NotRunning(String),
|
||||
/// The app went away or did not answer during the call, after running it or not.
|
||||
Lost(String),
|
||||
/// The request is still running in the app after the wait (it starts with [`STILL_RUNNING`]),
|
||||
/// or an earlier one is and this one was not sent.
|
||||
StillRunning(String),
|
||||
/// The app answered with an error (a refused token says which token file it came from).
|
||||
Refused(String),
|
||||
/// The reply was not a control reply.
|
||||
BadReply(String),
|
||||
}
|
||||
|
||||
impl CallError {
|
||||
/// The message.
|
||||
pub fn message(&self) -> &str {
|
||||
match self {
|
||||
CallError::NotRunning(m) | CallError::Lost(m) | CallError::StillRunning(m) | CallError::Refused(m) | CallError::BadReply(m) => m,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for CallError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(self.message())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for CallError {}
|
||||
|
||||
impl From<CallError> for String {
|
||||
fn from(e: CallError) -> String {
|
||||
e.message().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// A request: its method and parameters, how messages name it and what it does if it is still
|
||||
/// running when its call returns.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Request {
|
||||
pub method: String,
|
||||
pub params: Value,
|
||||
/// The request in messages ("`engine.execute` (solid.fillet)").
|
||||
pub what: String,
|
||||
/// What it still does when it finishes after its wait.
|
||||
pub effect: Effect,
|
||||
/// Never sent twice: on a fresh connection, waiting up to [`ONCE_TIMEOUT`], and once it is
|
||||
/// being written any error is final, as the app may have run it. Otherwise a request that
|
||||
/// finds its connection lost is sent once more on a fresh one (the app may have restarted).
|
||||
pub once: bool,
|
||||
}
|
||||
|
||||
impl Request {
|
||||
/// `method` with `params`, named "`method`" in messages, taken to change the document, sent
|
||||
/// again once on a fresh connection when its connection was lost.
|
||||
pub fn new(method: &str, params: Value) -> Self {
|
||||
Request { method: method.to_string(), params, what: format!("`{method}`"), effect: Effect::Changes, once: false }
|
||||
}
|
||||
}
|
||||
|
||||
/// A control reply as a call's result: its `result`, or its `error`.
|
||||
pub fn outcome(v: &Value) -> Result<Value, String> {
|
||||
if v.get("ok").and_then(Value::as_bool) == Some(true) {
|
||||
Ok(v.get("result").cloned().unwrap_or(Value::Null))
|
||||
} else {
|
||||
Err(v.get("error").and_then(Value::as_str).unwrap_or("error").to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// A request the app said is still running. Its reply will come on this connection, through
|
||||
/// `control.pending`, so the connection is kept for it.
|
||||
struct Outstanding {
|
||||
what: String,
|
||||
/// What it does when it finishes; `None`: not known (a request this client did not send).
|
||||
effect: Option<Effect>,
|
||||
since: Instant,
|
||||
}
|
||||
|
||||
impl Outstanding {
|
||||
fn secs(&self) -> u64 {
|
||||
self.since.elapsed().as_secs()
|
||||
}
|
||||
}
|
||||
|
||||
/// What became of a request still running when its call returned, once a later call waited it
|
||||
/// out: kept for [`Connection::pending`].
|
||||
enum Settled {
|
||||
Finished { what: String, reply: Value },
|
||||
Lost { what: String },
|
||||
}
|
||||
|
||||
/// The end of a wait for an outstanding request.
|
||||
enum Waited {
|
||||
/// It finished: its own reply.
|
||||
Done(Value),
|
||||
/// The app holds nothing (any more) for this connection.
|
||||
Nothing,
|
||||
/// Still running when the wait ran out.
|
||||
Running,
|
||||
}
|
||||
|
||||
/// A connection to the app listening at an address. It connects when a call needs it, so a
|
||||
/// client started before the app reaches it once it runs.
|
||||
pub struct Connection {
|
||||
app: App,
|
||||
/// What the app calls its document in messages ("design").
|
||||
document: &'static str,
|
||||
addr: String,
|
||||
port: u16,
|
||||
conn: Option<(BufReader<TcpStream>, TcpStream)>,
|
||||
/// Counts the connections made, so a caller can tell a fresh one from the one it asked.
|
||||
serial: u64,
|
||||
next_id: u64,
|
||||
/// The token given, when one was. Otherwise each request looks the token up again, since
|
||||
/// every launch of the app makes a new one.
|
||||
explicit_token: Option<String>,
|
||||
/// A request still running in the app; its reply waits on `conn`.
|
||||
outstanding: Option<Outstanding>,
|
||||
/// An outstanding request's end, once a later call waited for it (for `pending`).
|
||||
settled: Option<Settled>,
|
||||
waits: Waits,
|
||||
/// Where a long wait is reported.
|
||||
progress: fn(&str),
|
||||
}
|
||||
|
||||
impl Connection {
|
||||
/// A connection to `app` at `addr` (`host:port`), with `token` when given, else the app's
|
||||
/// token found the usual way ([`App::client_token`]); not connected yet.
|
||||
pub fn new(app: App, addr: &str, token: Option<&str>) -> Self {
|
||||
let port = addr.rsplit(':').next().and_then(|p| p.parse::<u16>().ok()).unwrap_or(0);
|
||||
Self {
|
||||
app,
|
||||
document: "design",
|
||||
addr: addr.to_string(),
|
||||
port,
|
||||
conn: None,
|
||||
serial: 0,
|
||||
next_id: 1,
|
||||
explicit_token: token.map(str::to_string),
|
||||
outstanding: None,
|
||||
settled: None,
|
||||
waits: Waits::default(),
|
||||
progress: |_| {},
|
||||
}
|
||||
}
|
||||
|
||||
/// What the app calls its document in messages ("design", the default).
|
||||
#[must_use]
|
||||
pub fn with_document(mut self, document: &'static str) -> Self {
|
||||
self.document = document;
|
||||
self
|
||||
}
|
||||
|
||||
/// Report a long wait for a request still running to `progress`, every 30 s.
|
||||
#[must_use]
|
||||
pub fn with_progress(mut self, progress: fn(&str)) -> Self {
|
||||
self.progress = progress;
|
||||
self
|
||||
}
|
||||
|
||||
/// How long to wait for a request still running.
|
||||
pub fn set_waits(&mut self, waits: Waits) {
|
||||
self.waits = waits;
|
||||
}
|
||||
|
||||
/// The address it connects to.
|
||||
pub fn addr(&self) -> &str {
|
||||
&self.addr
|
||||
}
|
||||
|
||||
/// The port of the address (0 when it names none).
|
||||
pub fn port(&self) -> u16 {
|
||||
self.port
|
||||
}
|
||||
|
||||
/// The token given, if one was.
|
||||
pub fn explicit_token(&self) -> Option<&str> {
|
||||
self.explicit_token.as_deref()
|
||||
}
|
||||
|
||||
/// Whether a connection is open now.
|
||||
pub fn is_connected(&self) -> bool {
|
||||
self.conn.is_some()
|
||||
}
|
||||
|
||||
/// The number of connections made so far: the same number while the same connection stays
|
||||
/// open.
|
||||
pub fn serial(&self) -> u64 {
|
||||
self.serial
|
||||
}
|
||||
|
||||
/// Whether a request of this connection is still running in the app.
|
||||
pub fn has_outstanding(&self) -> bool {
|
||||
self.outstanding.is_some()
|
||||
}
|
||||
|
||||
/// Close the connection. A request still running loses its reply with it.
|
||||
pub fn disconnect(&mut self) {
|
||||
self.conn = None;
|
||||
if let Some(o) = self.outstanding.take() {
|
||||
self.settled = Some(Settled::Lost { what: o.what });
|
||||
}
|
||||
}
|
||||
|
||||
/// Connect afresh. Failing to reach the app at all is `NotConnected`, so [`Connection::explain`]
|
||||
/// can tell it from a connection lost during a call.
|
||||
///
|
||||
/// # Errors
|
||||
/// `NotConnected` when nothing listens at the address (or it does not resolve).
|
||||
pub fn connect(&mut self) -> std::io::Result<()> {
|
||||
self.disconnect();
|
||||
let not_connected = |e: std::io::Error| std::io::Error::new(std::io::ErrorKind::NotConnected, e);
|
||||
let mut last = std::io::Error::new(std::io::ErrorKind::NotFound, format!("cannot resolve {}", self.addr));
|
||||
for sa in self.addr.to_socket_addrs().map_err(not_connected)? {
|
||||
match TcpStream::connect_timeout(&sa, CONNECT_TIMEOUT) {
|
||||
Ok(s) => {
|
||||
s.set_nodelay(true).ok();
|
||||
s.set_read_timeout(Some(READ_TIMEOUT)).ok();
|
||||
let read = s.try_clone()?;
|
||||
self.conn = Some((BufReader::new(read), s));
|
||||
self.serial += 1;
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => last = e,
|
||||
}
|
||||
}
|
||||
Err(not_connected(last))
|
||||
}
|
||||
|
||||
/// See whether the app is there now; the error says why not. The connection doesn't stay
|
||||
/// open: the app closes one that shows no token within a few seconds, and the first call may
|
||||
/// be long away. That call connects again.
|
||||
///
|
||||
/// # Errors
|
||||
/// As [`Connection::connect`].
|
||||
pub fn try_connect(&mut self) -> std::io::Result<()> {
|
||||
self.connect()?;
|
||||
self.disconnect();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The error for a failed exchange: whether the app is not running (and how to start it) or
|
||||
/// went away during the call.
|
||||
pub fn explain(&self, e: &std::io::Error) -> CallError {
|
||||
let (addr, port, name, document) = (&self.addr, self.port, self.app.name, self.document);
|
||||
let start = format!("{} --control {port}", name.to_lowercase());
|
||||
if e.kind() == std::io::ErrorKind::NotConnected {
|
||||
CallError::NotRunning(format!(
|
||||
"no {name} app is listening at {addr} ({e}): it is not running, or it quit. Start it with `{start}` (or ask the user to); the \
|
||||
next call connects to it"
|
||||
))
|
||||
} else {
|
||||
CallError::Lost(format!(
|
||||
"the {name} app at {addr} did not answer ({e}): it may have quit or crashed during this call, after running it or not. Inspect \
|
||||
the {document} before sending a change again; if the app quit, start it with `{start}`"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// The request line for `method`: a fresh id and the app's token (looked up again for every
|
||||
/// request: an app restarted mid-session has a new one).
|
||||
pub fn request_line(&mut self, method: &str, params: Value) -> String {
|
||||
let id = self.next_id;
|
||||
self.next_id += 1;
|
||||
let mut msg = json!({"id": id, "method": method, "params": params});
|
||||
if let Some(t) = self.app.client_token(self.port, self.explicit_token.as_deref()) {
|
||||
msg["token"] = json!(t.token);
|
||||
}
|
||||
msg.to_string()
|
||||
}
|
||||
|
||||
/// One request line out and one reply line back, at most [`MAX_REPLY`] long, connecting
|
||||
/// first when not connected. Any failure closes the connection (a timed-out read leaves a
|
||||
/// late reply on the stream).
|
||||
fn roundtrip(&mut self, line: &str) -> std::io::Result<String> {
|
||||
if self.conn.is_none() {
|
||||
self.connect()?;
|
||||
}
|
||||
let Some((reader, writer)) = self.conn.as_mut() else { return Err(std::io::Error::other("not connected")) };
|
||||
// One write: a request that loses its newline still reaches the app as a request.
|
||||
let sent = writer.write_all(format!("{line}\n").as_bytes()).and_then(|()| writer.flush());
|
||||
if let Err(e) = sent {
|
||||
self.disconnect();
|
||||
return Err(e);
|
||||
}
|
||||
let mut reply = String::new();
|
||||
let n = match reader.by_ref().take(MAX_REPLY).read_line(&mut reply) {
|
||||
Ok(n) => n,
|
||||
Err(e) => {
|
||||
self.disconnect();
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
if n == 0 || !reply.ends_with('\n') {
|
||||
self.disconnect();
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "connection closed"));
|
||||
}
|
||||
Ok(reply)
|
||||
}
|
||||
|
||||
fn set_read_timeout(&self, t: Duration) {
|
||||
if let Some((_, writer)) = &self.conn {
|
||||
writer.set_read_timeout(Some(t)).ok();
|
||||
}
|
||||
}
|
||||
|
||||
/// A reply line as JSON.
|
||||
fn parse(reply: &str) -> Result<Value, CallError> {
|
||||
serde_json::from_str(reply).map_err(|e| CallError::BadReply(format!("bad reply from the app: {e}")))
|
||||
}
|
||||
|
||||
/// A control reply as a call's result; a refused token says which token file it came from
|
||||
/// (and whether it is stale).
|
||||
///
|
||||
/// # Errors
|
||||
/// [`CallError::Refused`] with the app's error.
|
||||
pub fn outcome(&self, v: &Value) -> Result<Value, CallError> {
|
||||
outcome(v).map_err(|error| {
|
||||
CallError::Refused(
|
||||
self.app.client_token(self.port, self.explicit_token.as_deref()).map_or_else(|| error.clone(), |t| t.explain_error(&error)),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// `method` with `params`, at once and once: not behind a request still running on this
|
||||
/// connection, no second try. The app's whole reply (`ok`, `result` or `error`, and anything
|
||||
/// it adds), for a request the app answers at once on any connection
|
||||
/// (`control.capabilities`, `control.running`).
|
||||
///
|
||||
/// # Errors
|
||||
/// As [`Connection::explain`]; [`CallError::BadReply`].
|
||||
pub fn ask(&mut self, method: &str, params: Value) -> Result<Value, CallError> {
|
||||
let line = self.request_line(method, params);
|
||||
let reply = self.roundtrip(&line).map_err(|e| self.explain(&e))?;
|
||||
Self::parse(&reply)
|
||||
}
|
||||
|
||||
/// `method` with `params`, waiting at most `timeout` for the reply and never behind a request
|
||||
/// still running: for a request the app answers within a frame, asked again later when the
|
||||
/// app is busy. One try.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`CallError::StillRunning`] when a request of this connection is still running; as
|
||||
/// [`Connection::ask`] and [`Connection::outcome`].
|
||||
pub fn quick(&mut self, method: &str, params: Value, timeout: Duration) -> Result<Value, CallError> {
|
||||
if self.outstanding.is_some() {
|
||||
return Err(CallError::StillRunning("a request is still running in the app".into()));
|
||||
}
|
||||
if self.conn.is_none() {
|
||||
self.connect().map_err(|e| self.explain(&e))?;
|
||||
}
|
||||
let line = self.request_line(method, params);
|
||||
self.set_read_timeout(timeout);
|
||||
let reply = self.roundtrip(&line);
|
||||
self.set_read_timeout(READ_TIMEOUT);
|
||||
let reply = reply.map_err(|e| self.explain(&e))?;
|
||||
let v = Self::parse(&reply)?;
|
||||
self.outcome(&v)
|
||||
}
|
||||
|
||||
/// Ask the app, on this connection only, about the request it still runs for it.
|
||||
fn ask_pending(&mut self) -> Result<Value, CallError> {
|
||||
if self.conn.is_none() {
|
||||
self.disconnect();
|
||||
return Err(CallError::Lost("the connection to the app was lost".into()));
|
||||
}
|
||||
let v = self.ask("control.pending", json!({}))?;
|
||||
outcome(&v).map_err(CallError::Refused).inspect_err(|_| self.disconnect())
|
||||
}
|
||||
|
||||
/// `control.cancel` for the request still running on this connection. Sent at once, without
|
||||
/// waiting for that request first, and never on a fresh connection, which has nothing to
|
||||
/// cancel. Once the app no longer holds it (removed, stopped or finished), later calls don't
|
||||
/// wait for it.
|
||||
///
|
||||
/// # Errors
|
||||
/// As [`Connection::ask`]; [`CallError::Refused`].
|
||||
pub fn cancel(&mut self, params: Value) -> Result<Value, CallError> {
|
||||
if self.conn.is_none() {
|
||||
return Ok(json!({"cancelled": null, "note": "nothing to cancel: no request of this client is running in the app"}));
|
||||
}
|
||||
let v = self.ask("control.cancel", params)?;
|
||||
let r = outcome(&v).map_err(CallError::Refused);
|
||||
if r.is_ok() {
|
||||
self.outstanding = None;
|
||||
}
|
||||
r
|
||||
}
|
||||
|
||||
/// Wait up to `limit` for the outstanding request, asking `control.pending` now and then and
|
||||
/// reporting how long it takes. An error: the connection, and so the reply, was lost.
|
||||
fn wait_outstanding(&mut self, limit: Duration) -> Result<Waited, CallError> {
|
||||
let start = Instant::now();
|
||||
let mut told = start;
|
||||
loop {
|
||||
let Some(o) = &self.outstanding else { return Ok(Waited::Nothing) };
|
||||
let what = o.what.clone();
|
||||
let r = self.ask_pending()?;
|
||||
if r.get("pending").is_none_or(Value::is_null) {
|
||||
self.outstanding = None;
|
||||
return Ok(Waited::Nothing);
|
||||
}
|
||||
if r.get("done").and_then(Value::as_bool) == Some(true) {
|
||||
self.outstanding = None;
|
||||
return Ok(Waited::Done(r.get("reply").cloned().unwrap_or(Value::Null)));
|
||||
}
|
||||
let waited = start.elapsed();
|
||||
if waited >= limit {
|
||||
return Ok(Waited::Running);
|
||||
}
|
||||
if told.elapsed() >= PROGRESS_EVERY {
|
||||
let secs = self.outstanding.as_ref().map_or(0, Outstanding::secs);
|
||||
(self.progress)(&format!("{what} is still running in the app ({secs} s); waiting for it"));
|
||||
told = Instant::now();
|
||||
}
|
||||
std::thread::sleep(self.waits.poll.min(limit.saturating_sub(waited)));
|
||||
}
|
||||
}
|
||||
|
||||
/// The error for the outstanding request still running after its wait.
|
||||
fn still_running(&self, earlier: bool) -> CallError {
|
||||
let Some(o) = &self.outstanding else { return CallError::StillRunning(format!("{STILL_RUNNING} a request is still running in the app")) };
|
||||
let document = self.document;
|
||||
let effect =
|
||||
o.effect.map_or_else(|| format!("it was not cancelled and may still change the {document} when it finishes"), |e| e.when_done(document));
|
||||
let settle = self.waits.settle.as_secs();
|
||||
CallError::StillRunning(if earlier {
|
||||
format!(
|
||||
"{STILL_RUNNING} an earlier request, {}, is still running in the app ({} s; {effect}), so this call was not sent. Call the \
|
||||
`pending` tool to wait for its result, then send this call again",
|
||||
o.what,
|
||||
o.secs()
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{STILL_RUNNING} {} is still running in the app after {} s ({effect}); don't send it again. Call the `pending` tool to \
|
||||
wait for its result (up to {settle} s per call); other calls wait for it first",
|
||||
o.what,
|
||||
o.secs()
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Before a new request: wait for one still running from an earlier call, keeping its end for
|
||||
/// `pending`. Still running after the wait: the new request is not sent.
|
||||
fn settle_earlier(&mut self) -> Result<(), CallError> {
|
||||
let Some(what) = self.outstanding.as_ref().map(|o| o.what.clone()) else { return Ok(()) };
|
||||
match self.wait_outstanding(self.waits.settle) {
|
||||
Ok(Waited::Done(reply)) => self.settled = Some(Settled::Finished { what, reply }),
|
||||
Ok(Waited::Nothing) => {}
|
||||
Ok(Waited::Running) => return Err(self.still_running(true)),
|
||||
// The connection was lost, and the earlier reply with it (`pending` says so): the new
|
||||
// request goes on a fresh connection.
|
||||
Err(_) => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `control.pending` for the client: the end of the request still running from an earlier
|
||||
/// call, waiting for it up to the settle wait: `{pending: what | null, done?, reply?, lost?,
|
||||
/// running_s?}`. Never sent on a fresh connection, which has nothing pending.
|
||||
pub fn pending(&mut self) -> Value {
|
||||
if let Some(s) = self.settled.take() {
|
||||
return match s {
|
||||
Settled::Finished { what, reply } => json!({"pending": what, "done": true, "reply": reply}),
|
||||
Settled::Lost { what } => json!({"pending": what, "lost": true}),
|
||||
};
|
||||
}
|
||||
let Some(what) = self.outstanding.as_ref().map(|o| o.what.clone()) else { return json!({"pending": null}) };
|
||||
match self.wait_outstanding(self.waits.settle) {
|
||||
Ok(Waited::Done(reply)) => json!({"pending": what, "done": true, "reply": reply}),
|
||||
Ok(Waited::Nothing) => json!({"pending": null}),
|
||||
Ok(Waited::Running) => json!({"pending": what, "done": false, "running_s": self.outstanding.as_ref().map_or(0, Outstanding::secs)}),
|
||||
Err(_) => {
|
||||
self.settled = None;
|
||||
json!({"pending": what, "lost": true})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Send `request` and wait for its reply: the app's whole reply (`ok`, `result` or `error`,
|
||||
/// and anything it adds). First a request still running from an earlier call is waited for
|
||||
/// (except for `control.running`, which the app answers at once); a request the app says is
|
||||
/// still running is waited for through `control.pending`; a request refused unrun as busy
|
||||
/// behind one this client did not know of is sent once more after that one, when
|
||||
/// `retry_busy`.
|
||||
///
|
||||
/// # Errors
|
||||
/// As [`Connection::explain`]: a request whose connection is lost is sent once more on a fresh
|
||||
/// one (unless [`Request::once`]), and when nothing listens any more the app went away, maybe
|
||||
/// while running it ([`CallError::Lost`]); [`CallError::StillRunning`]; [`CallError::BadReply`].
|
||||
pub fn exchange(&mut self, request: Request, retry_busy: bool) -> Result<Value, CallError> {
|
||||
let Request { method, params, what, effect, once } = request;
|
||||
if method != "control.running" {
|
||||
self.settle_earlier()?;
|
||||
}
|
||||
let again = retry_busy.then(|| params.clone());
|
||||
let sent = Instant::now();
|
||||
let line = self.request_line(&method, params);
|
||||
let reply = if once {
|
||||
// A fresh connection (an idle one may have been closed by an app restart; connecting
|
||||
// again sends nothing).
|
||||
self.connect().or_else(|_| self.connect()).and_then(|()| {
|
||||
self.set_read_timeout(ONCE_TIMEOUT);
|
||||
let r = self.roundtrip(&line);
|
||||
self.set_read_timeout(READ_TIMEOUT);
|
||||
r
|
||||
})
|
||||
} else {
|
||||
self.roundtrip(&line).or_else(|lost| {
|
||||
self.roundtrip(&line).map_err(|e| if e.kind() == std::io::ErrorKind::NotConnected && lost.kind() != e.kind() { lost } else { e })
|
||||
})
|
||||
}
|
||||
.map_err(|e| self.explain(&e))?;
|
||||
let v = Self::parse(&reply)?;
|
||||
// Timed out in the app, still running: wait for its result on this connection.
|
||||
if v.get("running").and_then(Value::as_bool) == Some(true) {
|
||||
self.outstanding = Some(Outstanding { what: what.clone(), effect: Some(effect), since: sent });
|
||||
(self.progress)(&format!(
|
||||
"{what} is still running in the app after {} s; waiting up to {} s more for it",
|
||||
sent.elapsed().as_secs(),
|
||||
self.waits.pending.as_secs()
|
||||
));
|
||||
let document = self.document;
|
||||
return match self.wait_outstanding(self.waits.pending)? {
|
||||
Waited::Done(reply) => Ok(reply),
|
||||
Waited::Nothing => Err(CallError::Lost(format!(
|
||||
"{what} timed out in the app, which kept no result for it: inspect the {document} before sending it again"
|
||||
))),
|
||||
Waited::Running => Err(self.still_running(false)),
|
||||
};
|
||||
}
|
||||
// Refused unrun behind a request this client did not know of: wait for that one, then
|
||||
// send this one once more.
|
||||
if v.get("busy").and_then(Value::as_bool) == Some(true)
|
||||
&& let Some(params) = again
|
||||
{
|
||||
self.outstanding = Some(Outstanding { what: "an earlier request".into(), effect: None, since: Instant::now() });
|
||||
self.settle_earlier()?;
|
||||
return self.exchange(Request { method, params, what, effect, once }, false);
|
||||
}
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
/// [`Connection::exchange`] (retrying a busy refusal once) and the reply's result.
|
||||
///
|
||||
/// # Errors
|
||||
/// As [`Connection::exchange`] and [`Connection::outcome`].
|
||||
pub fn call(&mut self, request: Request) -> Result<Value, CallError> {
|
||||
let v = self.exchange(request, true)?;
|
||||
self.outcome(&v)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "client_tests.rs"]
|
||||
mod tests;
|
||||
@@ -1,190 +0,0 @@
|
||||
//! The client against the shared server ([`crate::server::Hub`]) on an ephemeral loopback port.
|
||||
|
||||
use super::*;
|
||||
use crate::ControlRequest;
|
||||
use crate::server::{Hub, Protocol, Stop, Summary, Waits as ServerWaits};
|
||||
use std::ffi::OsString;
|
||||
use std::net::TcpListener;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::mpsc::channel;
|
||||
|
||||
const TOKEN: &str = "0123456789abcdef0123456789abcdef";
|
||||
|
||||
fn no_dir() -> Option<PathBuf> {
|
||||
None
|
||||
}
|
||||
|
||||
fn no_env(_: &str) -> Option<OsString> {
|
||||
None
|
||||
}
|
||||
|
||||
fn utc(secs: u64) -> String {
|
||||
secs.to_string()
|
||||
}
|
||||
|
||||
/// An app with no token files and no environment: a client sends only the token it is given.
|
||||
const APP: App = App { name: "Probe", data_dir: no_dir, env: no_env, utc, default_port: 0 };
|
||||
|
||||
/// An app whose requests all may change its document.
|
||||
struct Probe;
|
||||
|
||||
impl Protocol for Probe {
|
||||
fn summary(&self, method: &str, _: &Value) -> Summary {
|
||||
Summary { what: format!("`{method}`"), effect: Effect::Changes, command: None, steps: None, long: false }
|
||||
}
|
||||
fn document(&self) -> &'static str {
|
||||
"design"
|
||||
}
|
||||
fn change_event(&self) -> &'static str {
|
||||
"design.changed"
|
||||
}
|
||||
fn revision_method(&self) -> &'static str {
|
||||
"document.revision"
|
||||
}
|
||||
fn docs(&self) -> &'static str {
|
||||
"docs/control-protocol.md"
|
||||
}
|
||||
fn capabilities(&self) -> Value {
|
||||
json!({})
|
||||
}
|
||||
}
|
||||
|
||||
/// The app on `port` (0: an ephemeral one) behind [`TOKEN`]: its window answers each request after
|
||||
/// `delay` with the request's method and parameters, and the server says a request is still
|
||||
/// running after `wait`.
|
||||
fn serve(port: u16, delay: Duration, wait: Duration) -> Stop {
|
||||
// A port just closed may take a moment to be free again on a loaded machine.
|
||||
let started = Instant::now();
|
||||
let mut bound = TcpListener::bind(("127.0.0.1", port));
|
||||
while bound.is_err() && started.elapsed() < Duration::from_secs(2) {
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
bound = TcpListener::bind(("127.0.0.1", port));
|
||||
}
|
||||
let listener = bound.unwrap();
|
||||
let (tx, rx) = channel::<ControlRequest>();
|
||||
std::thread::spawn(move || {
|
||||
for req in rx {
|
||||
std::thread::sleep(delay);
|
||||
let _ = req.reply.send(json!({"ok": true, "result": {"method": req.method, "params": req.params}}));
|
||||
}
|
||||
});
|
||||
let waits = ServerWaits { short: wait, long: wait, token: Duration::from_secs(5) };
|
||||
let hub = Arc::new(Hub::new(Probe, || {}, 1 << 20).with_waits(waits));
|
||||
hub.listen_stoppable(listener, TOKEN.into(), tx, false).unwrap()
|
||||
}
|
||||
|
||||
fn addr(port: u16) -> String {
|
||||
format!("127.0.0.1:{port}")
|
||||
}
|
||||
|
||||
/// A port nothing listens on.
|
||||
fn free_port() -> u16 {
|
||||
TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap().port()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_call_is_one_round_trip_with_the_token() {
|
||||
let app = serve(0, Duration::ZERO, Duration::from_secs(10));
|
||||
let mut c = Connection::new(APP, &addr(app.port()), Some(TOKEN));
|
||||
assert!(!c.is_connected());
|
||||
let r = c.call(Request::new("document.inspect", json!({"a": 1}))).unwrap();
|
||||
assert_eq!(r, json!({"method": "document.inspect", "params": {"a": 1}}));
|
||||
assert!(c.is_connected());
|
||||
let serial = c.serial();
|
||||
// the next call goes on the same connection
|
||||
c.call(Request::new("document.inspect", json!({}))).unwrap();
|
||||
assert_eq!(c.serial(), serial);
|
||||
// `try_connect` sees the app there and leaves no connection open
|
||||
let mut fresh = Connection::new(APP, &addr(app.port()), Some(TOKEN));
|
||||
fresh.try_connect().unwrap();
|
||||
assert!(!fresh.is_connected());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_wrong_token_or_none_is_refused() {
|
||||
let app = serve(0, Duration::ZERO, Duration::from_secs(10));
|
||||
for token in [Some("fedcba9876543210fedcba9876543210"), None] {
|
||||
let mut c = Connection::new(APP, &addr(app.port()), token);
|
||||
match c.call(Request::new("document.inspect", json!({}))) {
|
||||
Err(CallError::Refused(e)) => assert!(e.starts_with("unauthorized"), "{token:?}: {e}"),
|
||||
other => panic!("{token:?}: {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_app_not_running_says_how_to_start_it() {
|
||||
let port = free_port();
|
||||
let mut c = Connection::new(APP, &addr(port), Some(TOKEN));
|
||||
let e = c.call(Request::new("document.inspect", json!({}))).unwrap_err();
|
||||
let CallError::NotRunning(m) = &e else { panic!("{e:?}") };
|
||||
assert!(m.starts_with(&format!("no Probe app is listening at 127.0.0.1:{port}")), "{m}");
|
||||
assert!(m.contains(&format!("`probe --control {port}`")), "{m}");
|
||||
assert_eq!(c.try_connect().unwrap_err().kind(), std::io::ErrorKind::NotConnected);
|
||||
}
|
||||
|
||||
/// An app that goes away during a call is lost, not "not running"; the next call says it is not
|
||||
/// running, and once it runs again the next call reaches it.
|
||||
#[test]
|
||||
fn an_app_gone_during_a_call_is_lost_and_reached_again_once_back() {
|
||||
let app = serve(0, Duration::ZERO, Duration::from_secs(10));
|
||||
let port = app.port();
|
||||
let mut c = Connection::new(APP, &addr(port), Some(TOKEN));
|
||||
c.call(Request::new("document.inspect", json!({}))).unwrap();
|
||||
app.stop();
|
||||
let e = c.call(Request::new("document.inspect", json!({}))).unwrap_err();
|
||||
let CallError::Lost(m) = &e else { panic!("{e:?}") };
|
||||
assert!(m.contains("may have quit") && m.contains("Inspect the design") && m.contains(&format!("`probe --control {port}`")), "{m}");
|
||||
assert!(matches!(c.call(Request::new("document.inspect", json!({}))), Err(CallError::NotRunning(_))));
|
||||
let _back = serve(port, Duration::ZERO, Duration::from_secs(10));
|
||||
assert_eq!(c.call(Request::new("ui.view", json!({}))).unwrap()["method"], json!("ui.view"));
|
||||
}
|
||||
|
||||
/// A server that reads a request and closes the connection without a reply: the call is lost
|
||||
/// (after one more try on a fresh connection), and the reply is never taken for "not running".
|
||||
#[test]
|
||||
fn a_connection_closed_without_a_reply_is_lost() {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let port = listener.local_addr().unwrap().port();
|
||||
std::thread::spawn(move || {
|
||||
for stream in listener.incoming().flatten() {
|
||||
let mut line = String::new();
|
||||
let _ = BufReader::new(stream).read_line(&mut line);
|
||||
}
|
||||
});
|
||||
let mut c = Connection::new(APP, &addr(port), Some(TOKEN));
|
||||
let e = c.call(Request::new("document.inspect", json!({}))).unwrap_err();
|
||||
assert!(matches!(e, CallError::Lost(_)), "{e:?}");
|
||||
assert!(!c.is_connected());
|
||||
}
|
||||
|
||||
/// A request the app says is still running is waited for through `control.pending`, on the same
|
||||
/// connection, and its own reply is the call's result.
|
||||
#[test]
|
||||
fn a_request_still_running_is_waited_for_through_pending() {
|
||||
let app = serve(0, Duration::from_millis(400), Duration::from_millis(50));
|
||||
let mut c = Connection::new(APP, &addr(app.port()), Some(TOKEN));
|
||||
c.set_waits(Waits { pending: Duration::from_secs(10), settle: Duration::from_secs(10), poll: Duration::from_millis(5) });
|
||||
let r = c.call(Request::new("solid.slow", json!({"n": 2}))).unwrap();
|
||||
assert_eq!(r, json!({"method": "solid.slow", "params": {"n": 2}}));
|
||||
assert!(!c.has_outstanding());
|
||||
assert_eq!(c.pending(), json!({"pending": null}));
|
||||
}
|
||||
|
||||
/// Past the wait, the call says the request is still running, and `pending` later has its end.
|
||||
#[test]
|
||||
fn a_request_still_running_past_the_wait_is_reported_and_its_end_kept() {
|
||||
let app = serve(0, Duration::from_millis(400), Duration::from_millis(20));
|
||||
let mut c = Connection::new(APP, &addr(app.port()), Some(TOKEN));
|
||||
c.set_waits(Waits { pending: Duration::from_millis(30), settle: Duration::from_secs(10), poll: Duration::from_millis(5) });
|
||||
let mut slow = Request::new("solid.slow", json!({}));
|
||||
slow.what = "`solid.slow` (now)".into();
|
||||
let e = c.call(slow).unwrap_err();
|
||||
let CallError::StillRunning(m) = &e else { panic!("{e:?}") };
|
||||
assert!(m.starts_with(STILL_RUNNING) && m.contains("`solid.slow` (now)") && m.contains("will still apply"), "{m}");
|
||||
assert!(c.has_outstanding());
|
||||
let end = c.pending();
|
||||
assert_eq!(end["done"], json!(true), "{end}");
|
||||
assert_eq!(end["reply"]["result"]["method"], json!("solid.slow"), "{end}");
|
||||
}
|
||||
@@ -1,17 +1,14 @@
|
||||
//! The control channel any app may share (docs/control-protocol.md): the per-launch token and its
|
||||
//! files ([`auth`]), the loopback server that admits a connection by the token and hands each
|
||||
//! request to the window ([`server`]), the client that talks to it ([`client`]), the request the
|
||||
//! window answers ([`ControlRequest`]), the document watch subscribed connections wait on
|
||||
//! ([`DocumentWatch`]), and what a request still running does when it finishes ([`Effect`]). It
|
||||
//! needs no UI toolkit; the window's own requests over egui (`ui.screenshot`, `ui.resize`) are
|
||||
//! `solidcraft-control-egui`.
|
||||
//! request to the window ([`server`]), the request the window answers ([`ControlRequest`]), the
|
||||
//! document watch subscribed connections wait on ([`DocumentWatch`]), and what a request still running
|
||||
//! does when it finishes ([`Effect`]). It needs no UI toolkit; the window's own requests over egui
|
||||
//! (`ui.screenshot`, `ui.resize`) are `solidcraft-control-egui`.
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
pub mod auth;
|
||||
mod cancel;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod client;
|
||||
mod effect;
|
||||
mod request;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
|
||||
@@ -1197,10 +1197,6 @@ pub enum FeatureKind {
|
||||
tool: Option<FaceAt>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
sketch: Option<SketchId>,
|
||||
/// With `sketch`: its curves joined end to end first, so a closed loop cuts along itself
|
||||
/// only. Off (each curve on its own, an open one extended past its ends) in older designs.
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
loops: bool,
|
||||
},
|
||||
/// Scale bodies about a point (uniform, or per axis).
|
||||
Scale {
|
||||
|
||||
+7
-13
@@ -9715,7 +9715,7 @@ fn eval_feature(
|
||||
}
|
||||
apply_traced(doc, st, f, made, *operation, &[])
|
||||
}
|
||||
FeatureKind::SplitFace { faces, body, plane, tool, sketch, loops } => {
|
||||
FeatureKind::SplitFace { faces, body, plane, tool, sketch } => {
|
||||
let i = body_at(st, body, &crate::plain(faces))?;
|
||||
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
|
||||
let mut picked: Vec<usize> = Vec::new();
|
||||
@@ -9735,18 +9735,12 @@ fn eval_feature(
|
||||
}
|
||||
(None, Some(sid)) => {
|
||||
let ss = st.sketch(sid.0).ok_or_else(|| DocError::Unknown(format!("sketch {sid}")))?;
|
||||
// Each curve's exact segments (lines and arcs split by their own plane and cylinder),
|
||||
// or with `loops` the curves joined end to end: a closed chain is not extended.
|
||||
let curves: Vec<Vec<solidcore_record::Seg2>> = if *loops {
|
||||
let ids: Vec<String> = ss.sketch.curves.iter().filter(|c| !c.construction).map(|c| c.id.clone()).collect();
|
||||
surface::curve_chains(ss, &ids)?
|
||||
} else {
|
||||
(0..ss.sketch.curves.len())
|
||||
.filter(|ci| ss.sketch.curves.get(*ci).is_some_and(|c| !c.construction))
|
||||
.map(|ci| ss.sketch.segs(ci))
|
||||
.filter(|segs| !segs.is_empty())
|
||||
.collect()
|
||||
};
|
||||
// Each curve's exact segments (lines and arcs split by their own plane and cylinder).
|
||||
let curves: Vec<Vec<solidcore_record::Seg2>> = (0..ss.sketch.curves.len())
|
||||
.filter(|ci| ss.sketch.curves.get(*ci).is_some_and(|c| !c.construction))
|
||||
.map(|ci| ss.sketch.segs(ci))
|
||||
.filter(|segs| !segs.is_empty())
|
||||
.collect();
|
||||
if curves.is_empty() {
|
||||
return Err(DocError::Invalid(format!("sketch `{}` has no curves to split with", ss.name)));
|
||||
}
|
||||
|
||||
@@ -824,7 +824,7 @@ const BLEND_STATIONS: usize = 41;
|
||||
/// The sketch curves `ids` (free-form ones exactly where they can be) grouped into chains of segments that meet end to end, each oriented
|
||||
/// to run one way. Curves are joined greedily at their free ends: where three meet, one of them
|
||||
/// starts a chain of its own.
|
||||
pub(super) fn curve_chains(ss: &SolvedSketch, ids: &[String]) -> Result<Vec<Vec<Seg2>>> {
|
||||
fn curve_chains(ss: &SolvedSketch, ids: &[String]) -> Result<Vec<Vec<Seg2>>> {
|
||||
if ids.len() > MAX_CHAIN_CURVES {
|
||||
return Err(DocError::Invalid(format!("at most {MAX_CHAIN_CURVES} curves")));
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
//! A Split Face by sketch curves may save `loops: true`: the sketch's curves are joined end to end
|
||||
//! before the split, so a closed loop of lines and arcs cuts the face along the loop only. An older
|
||||
//! design has none and splits by each curve on its own, an open one extended past its ends, as
|
||||
//! before. The last release warns about the newer format number and opens the design, splitting
|
||||
//! that feature curve by curve (its sides extended across the face).
|
||||
@@ -191,7 +191,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
.contract(&SPLIT_FACE_CONTRACT)
|
||||
.output(output_schema_for::<FeatureResult>)
|
||||
.params(
|
||||
"faces: [[x,y,z] points on the faces to split, or face names from model.faces]; body?; and the splitting tool: plane: XY|XZ|YZ|plane name|{origin, normal} — or tool: {body, point} (a face of a body, its surface extended) — or sketch: id or name (its curves, along the sketch's normal; each curve on its own, an open one extended past its ends) and loops?: bool (default false: true joins the curves end to end first, so a closed loop cuts along itself only)",
|
||||
"faces: [[x,y,z] points on the faces to split, or face names from model.faces]; body?; and the splitting tool: plane: XY|XZ|YZ|plane name|{origin, normal} — or tool: {body, point} (a face of a body, its surface extended) — or sketch: id or name (its curves, along the sketch's normal)",
|
||||
),
|
||||
CommandSpec::new("solid.split_body", "Split Body", crate::checked!("solid.split_body", split_body))
|
||||
.at("SOLID", "MODIFY")
|
||||
@@ -2946,7 +2946,6 @@ fn split_face(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
plane: xy(),
|
||||
tool: Some(solidcraft_doc::FaceAt { body: BodyName(tb), point: Position(point) }),
|
||||
sketch: None,
|
||||
loops: false,
|
||||
}
|
||||
} else if p.get("sketch").is_some_and(|v| !v.is_null()) {
|
||||
let sketch = sketch_id(s, p.get("sketch"), cmd, "sketch")?;
|
||||
@@ -2956,7 +2955,6 @@ fn split_face(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
plane: xy(),
|
||||
tool: None,
|
||||
sketch: Some(SketchId(sketch)),
|
||||
loops: bool_(p, "loops").unwrap_or(false),
|
||||
}
|
||||
} else {
|
||||
let plane = plane_param(s, p.get("plane"), cmd)?;
|
||||
@@ -2966,7 +2964,6 @@ fn split_face(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
plane,
|
||||
tool: None,
|
||||
sketch: None,
|
||||
loops: false,
|
||||
}
|
||||
};
|
||||
add_face_feature(&picked, s, p, kind)
|
||||
|
||||
@@ -64,11 +64,6 @@ pub struct SplitFaceParams {
|
||||
/// `tool` is given.
|
||||
#[serde(default)]
|
||||
pub sketch: Option<SketchRef>,
|
||||
/// With `sketch`: true joins its curves end to end first, so a closed loop of lines and arcs
|
||||
/// splits the faces along the loop only. Default: false, each curve on its own, an open one
|
||||
/// extended past its ends.
|
||||
#[serde(default)]
|
||||
pub loops: Option<bool>,
|
||||
/// The plane to split along. Read when neither `tool` nor `sketch` is given; then it is
|
||||
/// required.
|
||||
#[serde(default)]
|
||||
|
||||
@@ -16,7 +16,7 @@ fn schemas_and_the_key_check_agree() {
|
||||
);
|
||||
assert_keys(
|
||||
input_schema_for::<SplitFaceParams>(),
|
||||
&["body", "body_name", "body_names", "faces", "loops", "name", "participants", "plane", "sketch", "tool"],
|
||||
&["body", "body_name", "body_names", "faces", "name", "participants", "plane", "sketch", "tool"],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -155,8 +155,6 @@ fn the_check_takes_what_the_schema_allows() {
|
||||
("solid.split_face", &face, json!({"faces": [[1, 2, 3]], "tool": {"body": "T", "point": [1, 2, 3]}}), true),
|
||||
("solid.split_face", &face, json!({"faces": [[1, 2, 3]], "sketch": "Cuts"}), true),
|
||||
("solid.split_face", &face, json!({"faces": [[1, 2, 3]], "sketch": 7, "name": "Split", "participants": ["*"]}), true),
|
||||
("solid.split_face", &face, json!({"faces": [[1, 2, 3]], "sketch": "Cuts", "loops": true}), true),
|
||||
("solid.split_face", &face, json!({"faces": [[1, 2, 3]], "sketch": "Cuts", "loops": null}), true),
|
||||
("solid.split_face", &face, json!({"faces": [[1, 2, 3]], "tool": null, "sketch": null, "plane": null, "body": null}), true),
|
||||
("solid.split_face", &face, json!({"faces": [[1, 2, 3]]}), true),
|
||||
("solid.split_face", &face, json!({"plane": "XY"}), false),
|
||||
@@ -164,7 +162,6 @@ fn the_check_takes_what_the_schema_allows() {
|
||||
("solid.split_face", &face, json!({"faces": [[1, 2]], "plane": "XY"}), false),
|
||||
("solid.split_face", &face, json!({"faces": [[1, 2, 3]], "body": 5, "plane": "XY"}), false),
|
||||
("solid.split_face", &face, json!({"faces": [[1, 2, 3]], "sketch": true}), false),
|
||||
("solid.split_face", &face, json!({"faces": [[1, 2, 3]], "sketch": "Cuts", "loops": "yes"}), false),
|
||||
("solid.split_face", &face, json!({"faces": [[1, 2, 3]], "tool": {"point": [1, 2, 3]}}), false),
|
||||
("solid.split_face", &face, json!({"faces": [[1, 2, 3]], "plane": "XY", "line": "cut"}), false),
|
||||
] {
|
||||
|
||||
@@ -55,33 +55,6 @@ fn split_a_face_by_sketch_curves() {
|
||||
assert!((v - 24000.0).abs() < 1e-6 * 24000.0, "{v}");
|
||||
}
|
||||
|
||||
/// A triangle of three lines on the top: by default each side is extended across the face; with
|
||||
/// `loops` the sides are joined first and the closed loop cuts out the triangle only. Open curves
|
||||
/// are extended either way.
|
||||
#[test]
|
||||
fn split_a_face_by_a_closed_loop_of_lines() {
|
||||
let split = |loops: bool| {
|
||||
let mut s = boxed();
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Loop"}));
|
||||
run(&mut s, "sketch.line", json!({"points": [[10, 5], [30, 5], [20, 25]], "closed": true}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.split_face", json!({"faces": [[20, 12, 20]], "body": "B", "sketch": "Loop", "loops": loops}));
|
||||
faces_volume(&mut s, "B")
|
||||
};
|
||||
let (f, v) = split(true);
|
||||
assert_eq!(f, 7, "the triangle and the rest of the top");
|
||||
assert!((v - 24000.0).abs() < 1e-6 * 24000.0, "{v}");
|
||||
assert!(split(false).0 > 7, "each side extended across the top");
|
||||
|
||||
let mut s = boxed();
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Cuts"}));
|
||||
run(&mut s, "sketch.line", json!({"points": [[10, 10], [10, 20]]}));
|
||||
run(&mut s, "sketch.line", json!({"points": [[30, 10], [30, 20]]}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.split_face", json!({"faces": [[20, 15, 20]], "body": "B", "sketch": "Cuts", "loops": true}));
|
||||
assert_eq!(faces_volume(&mut s, "B").0, 8, "two open lines still cut across the top");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_tool_that_misses_is_refused() {
|
||||
let mut s = boxed();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+423
-54
@@ -1,9 +1,10 @@
|
||||
//! Where MCP tool calls end up: a control-channel method call (`docs/control-protocol.md`).
|
||||
|
||||
use std::time::Duration;
|
||||
use std::io::{BufRead, BufReader, Read, Write};
|
||||
use std::net::{TcpStream, ToSocketAddrs};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_control::client::{CallError, Connection, Request, outcome};
|
||||
use solidcraft_engine::Session;
|
||||
use solidcraft_engine::control_pending;
|
||||
use solidcraft_engine::panels::ActionSink;
|
||||
@@ -56,35 +57,116 @@ pub trait Backend {
|
||||
}
|
||||
}
|
||||
|
||||
/// Longest reply line accepted from the app.
|
||||
const MAX_REPLY: u64 = 64 << 20;
|
||||
|
||||
/// How long a reply may take: kernel operations (booleans, fillets) on big parts can take a while.
|
||||
const READ_TIMEOUT: Duration = Duration::from_secs(300);
|
||||
|
||||
/// How long the app may take to answer `mcp.options`: the window answers it within a frame, so a
|
||||
/// longer wait means the app is busy and the server asks again on a later call.
|
||||
const OPTIONS_TIMEOUT: Duration = Duration::from_secs(2);
|
||||
|
||||
/// How long the app may take to run an `engine.batch`: a little longer than its control server
|
||||
/// waits (3600 s), so its "still running" reply arrives first.
|
||||
const BATCH_TIMEOUT: Duration = Duration::from_secs(3720);
|
||||
|
||||
/// How long a call goes on waiting, asking `control.pending`, after the app said its request is
|
||||
/// still running (the app says so after 60 s; #1618), before it says so itself.
|
||||
const PENDING_WAIT: Duration = Duration::from_secs(240);
|
||||
|
||||
/// How long a later call, or the `pending` tool, waits for a request still running from an
|
||||
/// earlier call.
|
||||
const SETTLE_WAIT: Duration = Duration::from_secs(60);
|
||||
|
||||
/// How often `control.pending` is asked while waiting.
|
||||
const POLL_EVERY: Duration = Duration::from_millis(500);
|
||||
|
||||
/// How often a long wait is reported on stderr.
|
||||
const PROGRESS_EVERY: Duration = Duration::from_secs(30);
|
||||
|
||||
/// How a tool error starts when a request is still running in the app after the wait: the
|
||||
/// request was not cancelled and its reply waits on the connection; the `pending` tool gets it.
|
||||
/// `batch` uses it to tell "still running" from "failed".
|
||||
pub(crate) use solidcraft_control::client::STILL_RUNNING;
|
||||
pub(crate) const STILL_RUNNING: &str = "still running:";
|
||||
|
||||
/// A request the app said is still running (#1618). Its reply will come on this connection,
|
||||
/// through `control.pending`, so the connection is kept for it.
|
||||
struct Outstanding {
|
||||
what: String,
|
||||
/// What it does when it finishes; `None`: not known (a request this bridge did not send).
|
||||
effect: Option<solidcraft_control::Effect>,
|
||||
since: Instant,
|
||||
}
|
||||
|
||||
impl Outstanding {
|
||||
fn secs(&self) -> u64 {
|
||||
self.since.elapsed().as_secs()
|
||||
}
|
||||
}
|
||||
|
||||
/// What became of a request still running when its call returned, once a later call waited it
|
||||
/// out: kept for the `pending` tool.
|
||||
enum Settled {
|
||||
Finished { what: String, reply: Value },
|
||||
Lost { what: String },
|
||||
}
|
||||
|
||||
/// The end of a wait for an outstanding request.
|
||||
enum Waited {
|
||||
/// It finished: its own reply.
|
||||
Done(Value),
|
||||
/// The app holds nothing (any more) for this connection.
|
||||
Nothing,
|
||||
/// Still running when the wait ran out.
|
||||
Running,
|
||||
}
|
||||
|
||||
/// How long to wait for a request still running (tests shorten them).
|
||||
#[cfg(test)]
|
||||
pub(crate) use solidcraft_control::client::Waits;
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) struct Waits {
|
||||
pub pending: Duration,
|
||||
pub settle: Duration,
|
||||
pub poll: Duration,
|
||||
}
|
||||
|
||||
const WAITS: Waits = Waits { pending: PENDING_WAIT, settle: SETTLE_WAIT, poll: POLL_EVERY };
|
||||
|
||||
/// The level a control reply says its request was checked at (#9184), if it says one.
|
||||
fn echoed(v: &Value) -> Option<ParamCheck> {
|
||||
v.get("param_check").and_then(Value::as_str).and_then(ParamCheck::parse)
|
||||
}
|
||||
|
||||
/// A control reply as a call's result.
|
||||
fn outcome(v: &Value) -> Result<Value, String> {
|
||||
if v.get("ok").and_then(Value::as_bool) == Some(true) {
|
||||
Ok(v.get("result").cloned().unwrap_or(Value::Null))
|
||||
} else {
|
||||
Err(v.get("error").and_then(Value::as_str).unwrap_or("error").to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// A running SolidCraft app reached through its loopback control port
|
||||
/// (`solidcraft --control PORT`): the shared control client ([`Connection`]), with the agent's
|
||||
/// folders on every request and the calls checked against their commands' contracts.
|
||||
/// (`solidcraft --control PORT`).
|
||||
pub struct Remote {
|
||||
conn: Connection,
|
||||
addr: String,
|
||||
port: u16,
|
||||
conn: Option<(BufReader<TcpStream>, TcpStream)>,
|
||||
next_id: u64,
|
||||
/// `--token`, when given. Otherwise each request looks the token up again, since every
|
||||
/// launch of the app makes a new one (docs/control-protocol.md).
|
||||
explicit_token: Option<String>,
|
||||
/// A request still running in the app; its reply waits on `conn`.
|
||||
outstanding: Option<Outstanding>,
|
||||
/// An outstanding request's end, once a later call waited for it (for `pending`).
|
||||
settled: Option<Settled>,
|
||||
waits: Waits,
|
||||
/// How calls are checked (`--param-check`): by the app, when it checks a request's own
|
||||
/// level, else here before they are sent.
|
||||
param_check: ParamCheck,
|
||||
/// Whether the connected app checks a request's `param_check` itself (#9184), asked once per
|
||||
/// connection (the connection's serial number beside it); `None` until asked.
|
||||
app_checks: Option<(u64, bool)>,
|
||||
/// connection; `None` until asked.
|
||||
app_checks: Option<bool>,
|
||||
/// The level the app said it checked the last reply at (`param_check` beside `ok`).
|
||||
echoed: Option<ParamCheck>,
|
||||
}
|
||||
@@ -93,11 +175,6 @@ pub struct Remote {
|
||||
/// `batch` uses it to tell "never sent" from "sent, reply lost".
|
||||
pub(crate) const NOT_RUNNING: &str = "no SolidCraft app is listening at";
|
||||
|
||||
/// A long wait for a request still running, on stderr.
|
||||
fn note(message: &str) {
|
||||
solidcraft_engine::note!("solidcraft-mcp: {message}");
|
||||
}
|
||||
|
||||
impl Remote {
|
||||
/// Connect to the app at `addr`, with its control token found the usual way
|
||||
/// (`SOLIDCRAFT_CONTROL_TOKEN`, else the app's token file for that port).
|
||||
@@ -108,15 +185,24 @@ impl Remote {
|
||||
/// [`Remote::connect`] with the token given (`--token`); `None` finds it the usual way.
|
||||
pub fn connect_with_token(addr: &str, token: Option<&str>) -> std::io::Result<Self> {
|
||||
let mut r = Self::new(addr, token);
|
||||
r.conn.connect()?;
|
||||
r.reconnect()?;
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
/// A bridge to the app at `addr` that connects when a call needs it: an MCP server started
|
||||
/// before the app keeps its tools, and they reach the app once it runs.
|
||||
pub fn new(addr: &str, token: Option<&str>) -> Self {
|
||||
let port = addr.rsplit(':').next().and_then(|p| p.parse::<u16>().ok()).unwrap_or(0);
|
||||
let explicit_token = token.map(str::to_string);
|
||||
Self {
|
||||
conn: Connection::new(solidcraft_engine::control_auth::APP, addr, token).with_progress(note),
|
||||
addr: addr.to_string(),
|
||||
port,
|
||||
conn: None,
|
||||
next_id: 1,
|
||||
explicit_token,
|
||||
outstanding: None,
|
||||
settled: None,
|
||||
waits: WAITS,
|
||||
param_check: ParamCheck::Off,
|
||||
app_checks: None,
|
||||
echoed: None,
|
||||
@@ -133,45 +219,312 @@ impl Remote {
|
||||
/// Shorter waits for requests still running (tests).
|
||||
#[cfg(test)]
|
||||
pub(crate) fn set_waits(&mut self, waits: Waits) {
|
||||
self.conn.set_waits(waits);
|
||||
self.waits = waits;
|
||||
}
|
||||
|
||||
/// Close the connection. A request still running loses its reply with it.
|
||||
fn drop_conn(&mut self) {
|
||||
self.conn = None;
|
||||
// The next connection may reach another app (restarted, another version).
|
||||
self.app_checks = None;
|
||||
if let Some(o) = self.outstanding.take() {
|
||||
self.settled = Some(Settled::Lost { what: o.what });
|
||||
}
|
||||
}
|
||||
|
||||
/// See whether the app is there now (`new` does not); the error says why not. The connection
|
||||
/// doesn't stay open: the app closes one that shows no token within a few seconds, and the
|
||||
/// first call may be long away. That call connects again.
|
||||
pub fn try_connect(&mut self) -> std::io::Result<()> {
|
||||
self.conn.try_connect()
|
||||
self.reconnect()?;
|
||||
self.drop_conn();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `params` with the agent's folders: the app confines the files a request reaches to them
|
||||
/// (control.rs), and to the folders it opened or saved a design in, which count as its open
|
||||
/// design's.
|
||||
fn scoped(params: Value) -> Value {
|
||||
/// Failing to reach the app at all is `NotConnected`, so [`Remote::explain`] can tell it from
|
||||
/// a connection lost during a call.
|
||||
fn reconnect(&mut self) -> std::io::Result<()> {
|
||||
self.drop_conn();
|
||||
let not_connected = |e: std::io::Error| std::io::Error::new(std::io::ErrorKind::NotConnected, e);
|
||||
let mut last = std::io::Error::new(std::io::ErrorKind::NotFound, format!("cannot resolve {}", self.addr));
|
||||
for sa in self.addr.to_socket_addrs().map_err(not_connected)? {
|
||||
match TcpStream::connect_timeout(&sa, Duration::from_millis(800)) {
|
||||
Ok(s) => {
|
||||
s.set_nodelay(true).ok();
|
||||
s.set_read_timeout(Some(READ_TIMEOUT)).ok();
|
||||
let read = s.try_clone()?;
|
||||
self.conn = Some((BufReader::new(read), s));
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => last = e,
|
||||
}
|
||||
}
|
||||
Err(not_connected(last))
|
||||
}
|
||||
|
||||
/// The tool error for a failed call: whether the app is not running (and how to start it) or
|
||||
/// went away during the call.
|
||||
fn explain(&self, e: &std::io::Error) -> String {
|
||||
let (addr, port) = (&self.addr, self.port);
|
||||
if e.kind() == std::io::ErrorKind::NotConnected {
|
||||
format!(
|
||||
"{NOT_RUNNING} {addr} ({e}): it is not running, or it quit. Start it with `solidcraft --control {port}` \
|
||||
(or ask the user to); the next call connects to it"
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"the SolidCraft app at {addr} did not answer ({e}): it may have quit or crashed during this call, after running it or not. Inspect the design before sending a change again; if the app quit, start it with `solidcraft --control {port}`"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn roundtrip(&mut self, line: &str) -> std::io::Result<String> {
|
||||
if self.conn.is_none() {
|
||||
self.reconnect()?;
|
||||
}
|
||||
let Some((reader, writer)) = self.conn.as_mut() else { return Err(std::io::Error::other("not connected")) };
|
||||
// One write: a request that loses its newline still reaches the app as a request.
|
||||
let sent = writer.write_all(format!("{line}\n").as_bytes()).and_then(|()| writer.flush());
|
||||
if let Err(e) = sent {
|
||||
self.drop_conn();
|
||||
return Err(e);
|
||||
}
|
||||
let mut reply = String::new();
|
||||
let n = match reader.by_ref().take(MAX_REPLY).read_line(&mut reply) {
|
||||
Ok(n) => n,
|
||||
Err(e) => {
|
||||
// A timed-out read leaves a late reply on the stream: start afresh next time.
|
||||
self.drop_conn();
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
if n == 0 || !reply.ends_with('\n') {
|
||||
self.drop_conn();
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "connection closed"));
|
||||
}
|
||||
Ok(reply)
|
||||
}
|
||||
|
||||
fn set_read_timeout(&self, t: Duration) {
|
||||
if let Some((_, writer)) = &self.conn {
|
||||
writer.set_read_timeout(Some(t)).ok();
|
||||
}
|
||||
}
|
||||
|
||||
/// `engine.batch` is never sent twice (#743): it goes on a fresh connection (an idle one may
|
||||
/// have been closed by an app restart; connecting again sends nothing), and once it is being
|
||||
/// written any error is final, as the app may have run it.
|
||||
fn send_batch(&mut self, line: &str) -> std::io::Result<String> {
|
||||
self.reconnect().or_else(|_| self.reconnect())?;
|
||||
self.set_read_timeout(BATCH_TIMEOUT);
|
||||
let r = self.roundtrip(line);
|
||||
self.set_read_timeout(READ_TIMEOUT);
|
||||
r
|
||||
}
|
||||
}
|
||||
|
||||
impl Remote {
|
||||
/// The request line for `method`: a fresh id, the agent's folders and the app's token.
|
||||
fn request_line(&mut self, method: &str, params: Value) -> String {
|
||||
let id = self.next_id;
|
||||
self.next_id += 1;
|
||||
let mut params = params;
|
||||
// The app confines the files this request reaches to the agent's folders (control.rs).
|
||||
if let (Some(scope), Some(o)) = (solidcraft_engine::io::vfs::current_scope(), params.as_object_mut()) {
|
||||
o.insert("allow_paths".into(), json!(scope.roots().iter().map(|r| r.to_string_lossy()).collect::<Vec<_>>()));
|
||||
// And the folders it opened or saved a design in, which count as its open design's.
|
||||
if !scope.design_roots().is_empty() {
|
||||
o.insert("design_folders".into(), json!(scope.design_roots().iter().map(|r| r.to_string_lossy()).collect::<Vec<_>>()));
|
||||
}
|
||||
}
|
||||
params
|
||||
let mut msg = json!({"id": id, "method": method, "params": params});
|
||||
// The token goes with every request (an app restarted mid-session has a new one).
|
||||
let token = solidcraft_engine::control_auth::APP.client_token(self.port, self.explicit_token.as_deref());
|
||||
if let Some(t) = &token {
|
||||
msg["token"] = json!(t.token);
|
||||
}
|
||||
msg.to_string()
|
||||
}
|
||||
|
||||
/// Ask the app, on this connection only, about the request it still runs for it.
|
||||
fn ask_pending(&mut self) -> Result<Value, String> {
|
||||
if self.conn.is_none() {
|
||||
self.drop_conn();
|
||||
return Err("the connection to the app was lost".into());
|
||||
}
|
||||
let line = self.request_line("control.pending", json!({}));
|
||||
let reply = self.roundtrip(&line).map_err(|e| self.explain(&e))?;
|
||||
let v: Value = serde_json::from_str(&reply).map_err(|e| format!("bad reply from the app: {e}"))?;
|
||||
outcome(&v).inspect_err(|_| self.drop_conn())
|
||||
}
|
||||
|
||||
/// `control.cancel` for the request still running on this connection (a timed-out call's,
|
||||
/// docs/control-protocol.md). Sent at once, without waiting for that request first, and never
|
||||
/// on a fresh connection, which has nothing to cancel. Once the app no longer holds it
|
||||
/// (removed, stopped or finished), later calls don't wait for it.
|
||||
fn cancel_outstanding(&mut self, params: Value) -> Result<Value, String> {
|
||||
if self.conn.is_none() {
|
||||
return Ok(json!({"cancelled": null, "note": "nothing to cancel: no request of this bridge is running in the app"}));
|
||||
}
|
||||
let line = self.request_line("control.cancel", params);
|
||||
let reply = self.roundtrip(&line).map_err(|e| self.explain(&e))?;
|
||||
let v: Value = serde_json::from_str(&reply).map_err(|e| format!("bad reply from the app: {e}"))?;
|
||||
let r = outcome(&v);
|
||||
if r.is_ok() {
|
||||
self.outstanding = None;
|
||||
}
|
||||
r
|
||||
}
|
||||
|
||||
/// Wait up to `limit` for the outstanding request, asking `control.pending` now and then and
|
||||
/// saying on stderr how long it takes. An error: the connection, and so the reply, was lost.
|
||||
fn wait_outstanding(&mut self, limit: Duration) -> Result<Waited, String> {
|
||||
let start = Instant::now();
|
||||
let mut told = start;
|
||||
loop {
|
||||
let Some(o) = &self.outstanding else { return Ok(Waited::Nothing) };
|
||||
let what = o.what.clone();
|
||||
let r = self.ask_pending()?;
|
||||
if r.get("pending").is_none_or(Value::is_null) {
|
||||
self.outstanding = None;
|
||||
return Ok(Waited::Nothing);
|
||||
}
|
||||
if r.get("done").and_then(Value::as_bool) == Some(true) {
|
||||
self.outstanding = None;
|
||||
return Ok(Waited::Done(r.get("reply").cloned().unwrap_or(Value::Null)));
|
||||
}
|
||||
let waited = start.elapsed();
|
||||
if waited >= limit {
|
||||
return Ok(Waited::Running);
|
||||
}
|
||||
if told.elapsed() >= PROGRESS_EVERY {
|
||||
let secs = self.outstanding.as_ref().map_or(0, Outstanding::secs);
|
||||
solidcraft_engine::note!("solidcraft-mcp: {what} is still running in the app ({secs} s); waiting for it");
|
||||
told = Instant::now();
|
||||
}
|
||||
std::thread::sleep(self.waits.poll.min(limit.saturating_sub(waited)));
|
||||
}
|
||||
}
|
||||
|
||||
/// The tool error for the outstanding request still running after its wait.
|
||||
fn still_running(&self, earlier: bool) -> String {
|
||||
let Some(o) = &self.outstanding else { return format!("{STILL_RUNNING} a request is still running in the app") };
|
||||
let effect =
|
||||
o.effect.map_or_else(|| "it was not cancelled and may still change the design when it finishes".into(), |e| e.when_done("design"));
|
||||
let settle = self.waits.settle.as_secs();
|
||||
if earlier {
|
||||
format!(
|
||||
"{STILL_RUNNING} an earlier request, {}, is still running in the app ({} s; {effect}), so this call was not sent. Call the \
|
||||
`pending` tool to wait for its result, then send this call again",
|
||||
o.what,
|
||||
o.secs()
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{STILL_RUNNING} {} is still running in the app after {} s ({effect}); don't send it again. Call the `pending` tool to \
|
||||
wait for its result (up to {settle} s per call); other calls wait for it first",
|
||||
o.what,
|
||||
o.secs()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Before a new request: wait for one still running from an earlier call, keeping its end for
|
||||
/// `pending`. Still running after the wait: the new request is not sent.
|
||||
fn settle_earlier(&mut self) -> Result<(), String> {
|
||||
let Some(what) = self.outstanding.as_ref().map(|o| o.what.clone()) else { return Ok(()) };
|
||||
match self.wait_outstanding(self.waits.settle) {
|
||||
Ok(Waited::Done(reply)) => self.settled = Some(Settled::Finished { what, reply }),
|
||||
Ok(Waited::Nothing) => {}
|
||||
Ok(Waited::Running) => return Err(self.still_running(true)),
|
||||
// The connection was lost, and the earlier reply with it (`pending` says so): the new
|
||||
// request goes on a fresh connection.
|
||||
Err(_) => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `control.pending` for the bridge: the end of the request still running from an earlier
|
||||
/// call, waiting for it up to the settle wait. Never sent on a fresh connection, which has
|
||||
/// nothing pending.
|
||||
fn pending_status(&mut self) -> Result<Value, String> {
|
||||
if let Some(s) = self.settled.take() {
|
||||
return Ok(match s {
|
||||
Settled::Finished { what, reply } => json!({"pending": what, "done": true, "reply": reply}),
|
||||
Settled::Lost { what } => json!({"pending": what, "lost": true}),
|
||||
});
|
||||
}
|
||||
let Some(what) = self.outstanding.as_ref().map(|o| o.what.clone()) else { return Ok(json!({"pending": null})) };
|
||||
match self.wait_outstanding(self.waits.settle) {
|
||||
Ok(Waited::Done(reply)) => Ok(json!({"pending": what, "done": true, "reply": reply})),
|
||||
Ok(Waited::Nothing) => Ok(json!({"pending": null})),
|
||||
Ok(Waited::Running) => Ok(json!({"pending": what, "done": false, "running_s": self.outstanding.as_ref().map_or(0, Outstanding::secs)})),
|
||||
Err(_) => {
|
||||
self.settled = None;
|
||||
Ok(json!({"pending": what, "lost": true}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `method` sent to the app as the shared client sends a request: named and with its effect
|
||||
/// for messages, `engine.batch` never sent twice (#743), and the level the app checked the
|
||||
/// reply at kept.
|
||||
fn call_once(&mut self, method: &str, params: Value, retry_busy: bool) -> Result<Value, String> {
|
||||
// The app answers `control.running` at once, even behind a request still running: it is
|
||||
// not held back for that one as other calls are.
|
||||
if method != "control.running" {
|
||||
self.settle_earlier()?;
|
||||
}
|
||||
self.echoed = None;
|
||||
let request = Request {
|
||||
method: method.to_string(),
|
||||
what: control_pending::describe(method, ¶ms),
|
||||
effect: control_pending::effect(method, ¶ms),
|
||||
once: method == "engine.batch",
|
||||
params: Self::scoped(params),
|
||||
};
|
||||
let reply = self.conn.exchange(request, retry_busy)?;
|
||||
self.echoed = echoed(&reply);
|
||||
Ok(self.conn.outcome(&reply)?)
|
||||
let what = control_pending::describe(method, ¶ms);
|
||||
let effect = control_pending::effect(method, ¶ms);
|
||||
let again = retry_busy.then(|| params.clone());
|
||||
let sent = Instant::now();
|
||||
let line = self.request_line(method, params);
|
||||
let reply = if method == "engine.batch" {
|
||||
self.send_batch(&line)
|
||||
} else {
|
||||
// One retry on a fresh connection (the app may have restarted). When nothing listens
|
||||
// any more, the app went away, maybe while running this request: say that, not
|
||||
// just "not running".
|
||||
self.roundtrip(&line).or_else(|lost| {
|
||||
self.roundtrip(&line).map_err(|e| if e.kind() == std::io::ErrorKind::NotConnected && lost.kind() != e.kind() { lost } else { e })
|
||||
})
|
||||
}
|
||||
.map_err(|e| self.explain(&e))?;
|
||||
let v: Value = serde_json::from_str(&reply).map_err(|e| format!("bad reply from the app: {e}"))?;
|
||||
self.echoed = echoed(&v);
|
||||
// Timed out in the app, still running (#1618): wait for its result on this connection.
|
||||
if v.get("running").and_then(Value::as_bool) == Some(true) {
|
||||
self.outstanding = Some(Outstanding { what: what.clone(), effect: Some(effect), since: sent });
|
||||
solidcraft_engine::note!(
|
||||
"solidcraft-mcp: {what} is still running in the app after {} s; waiting up to {} s more for it",
|
||||
sent.elapsed().as_secs(),
|
||||
self.waits.pending.as_secs()
|
||||
);
|
||||
return match self.wait_outstanding(self.waits.pending)? {
|
||||
Waited::Done(reply) => {
|
||||
self.echoed = echoed(&reply);
|
||||
outcome(&reply)
|
||||
}
|
||||
Waited::Nothing => {
|
||||
Err(format!("{what} timed out in the app, which kept no result for it: inspect the design before sending it again"))
|
||||
}
|
||||
Waited::Running => Err(self.still_running(false)),
|
||||
};
|
||||
}
|
||||
// Refused unrun behind a request this bridge did not know of: wait for that one, then
|
||||
// send this one once more.
|
||||
if v.get("busy").and_then(Value::as_bool) == Some(true)
|
||||
&& let Some(params) = again
|
||||
{
|
||||
self.outstanding = Some(Outstanding { what: "an earlier request".into(), effect: None, since: Instant::now() });
|
||||
self.settle_earlier()?;
|
||||
return self.call_once(method, params, false);
|
||||
}
|
||||
// A refused token: say which token file it came from (and whether it is stale).
|
||||
outcome(&v).map_err(|error| {
|
||||
solidcraft_engine::control_auth::APP
|
||||
.client_token(self.port, self.explicit_token.as_deref())
|
||||
.map_or_else(|| error.clone(), |t| t.explain_error(&error))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,21 +549,21 @@ impl Remote {
|
||||
/// connection with `control.capabilities`, which lists the levels it takes; an older app
|
||||
/// answers "unknown method". Any other answer (no app, a busy one) is asked again next time.
|
||||
fn app_checks(&mut self) -> bool {
|
||||
if let Some((serial, known)) = self.app_checks
|
||||
&& self.conn.is_connected()
|
||||
&& serial == self.conn.serial()
|
||||
{
|
||||
if let Some(known) = self.app_checks {
|
||||
return known;
|
||||
}
|
||||
// Sent ahead of settling a request still running on this connection: the app answers
|
||||
// `control.capabilities` at once, as `control.running`, without taking that one's reply.
|
||||
let reply = self.conn.ask("control.capabilities", Self::scoped(json!({}))).ok();
|
||||
let line = self.request_line("control.capabilities", json!({}));
|
||||
let reply = self.roundtrip(&line).ok().and_then(|r| serde_json::from_str::<Value>(&r).ok());
|
||||
let known = match reply.as_ref().map(outcome) {
|
||||
Some(Ok(c)) => Some(c["param_check"].as_array().is_some_and(|levels| levels.iter().any(|l| l == self.param_check.name()))),
|
||||
Some(Err(e)) if e.starts_with("unknown method") => Some(false),
|
||||
_ => None,
|
||||
};
|
||||
self.app_checks = if self.conn.is_connected() { known.map(|k| (self.conn.serial(), k)) } else { None };
|
||||
if self.conn.is_some() {
|
||||
self.app_checks = known;
|
||||
}
|
||||
known.unwrap_or(false)
|
||||
}
|
||||
|
||||
@@ -220,7 +573,7 @@ impl Remote {
|
||||
fn confirmed(&mut self, reply: Result<Value, String>) -> Result<Value, String> {
|
||||
let unconfirmed = reply.is_ok() && self.echoed != Some(self.param_check);
|
||||
if unconfirmed {
|
||||
self.app_checks = Some((self.conn.serial(), false));
|
||||
self.app_checks = Some(false);
|
||||
}
|
||||
match reply {
|
||||
Ok(_) if unconfirmed && self.param_check == ParamCheck::Strict => {
|
||||
@@ -305,12 +658,12 @@ impl Remote {
|
||||
impl Backend for Remote {
|
||||
fn call(&mut self, method: &str, params: Value) -> Result<Value, String> {
|
||||
match method {
|
||||
"control.pending" => Ok(self.conn.pending()),
|
||||
"control.pending" => self.pending_status(),
|
||||
"engine.execute" if self.param_check != ParamCheck::Off => self.execute_checked(params),
|
||||
"engine.batch" if self.param_check != ParamCheck::Off => self.batch_checked(params),
|
||||
"engine.script" if self.param_check != ParamCheck::Off => self.call_once(method, self.at_level(params), true),
|
||||
// Not behind the request it cancels, as other calls are.
|
||||
"control.cancel" => Ok(self.conn.cancel(Self::scoped(params))?),
|
||||
"control.cancel" => self.cancel_outstanding(params),
|
||||
_ => self.call_once(method, params, true),
|
||||
}
|
||||
}
|
||||
@@ -322,30 +675,46 @@ impl Backend for Remote {
|
||||
}
|
||||
/// The same app with this bridge's token, not yet connected: it connects when asked.
|
||||
fn side_channel(&self) -> Option<Remote> {
|
||||
Some(Remote::new(self.conn.addr(), self.conn.explicit_token()).with_param_check(self.param_check))
|
||||
Some(Remote::new(&self.addr, self.explicit_token.as_deref()).with_param_check(self.param_check))
|
||||
}
|
||||
fn app_options(&mut self) -> Result<Option<Value>, String> {
|
||||
// This runs on the serve loop before a tool call, so it never waits: not behind a request
|
||||
// still running (ask again on a later call), and one try with no retry, because a retry
|
||||
// reconnects with READ_TIMEOUT and could block for minutes.
|
||||
match self.conn.quick("mcp.options", Self::scoped(json!({})), OPTIONS_TIMEOUT) {
|
||||
if self.outstanding.is_some() {
|
||||
return Err("a request is still running in the app".into());
|
||||
}
|
||||
if self.conn.is_none() {
|
||||
self.reconnect().map_err(|e| self.explain(&e))?;
|
||||
}
|
||||
let line = self.request_line("mcp.options", json!({}));
|
||||
self.set_read_timeout(OPTIONS_TIMEOUT);
|
||||
let reply = self.roundtrip(&line);
|
||||
self.set_read_timeout(READ_TIMEOUT);
|
||||
let reply = reply.map_err(|e| self.explain(&e))?;
|
||||
let v: Value = serde_json::from_str(&reply).map_err(|e| format!("bad reply from the app: {e}"))?;
|
||||
let r = outcome(&v).map_err(|error| {
|
||||
solidcraft_engine::control_auth::APP
|
||||
.client_token(self.port, self.explicit_token.as_deref())
|
||||
.map_or_else(|| error.clone(), |t| t.explain_error(&error))
|
||||
});
|
||||
match r {
|
||||
Ok(v) => Ok(Some(v)),
|
||||
// An app from before the setting (its agent chat refuses methods it doesn't know):
|
||||
// the server's own options decide.
|
||||
Err(CallError::Refused(e)) if e.starts_with("unknown method") || e.contains("is not available in the agent chat") => Ok(None),
|
||||
Err(e) => Err(e.into()),
|
||||
Err(e) if e.starts_with("unknown method") || e.contains("is not available in the agent chat") => Ok(None),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
fn describe(&self) -> String {
|
||||
if self.conn.is_connected() {
|
||||
format!("connected to the SolidCraft app at {} (the user watches the viewport)", self.conn.addr())
|
||||
if self.conn.is_some() {
|
||||
format!("connected to the SolidCraft app at {} (the user watches the viewport)", self.addr)
|
||||
} else {
|
||||
// Still "connected to the SolidCraft app": the skills tell bridged from headless by it.
|
||||
format!(
|
||||
"connected to the SolidCraft app at {} once it runs (the user watches the viewport). It was not running \
|
||||
when this server started; until it is (`solidcraft --control {}`), tool calls say so",
|
||||
self.conn.addr(),
|
||||
self.conn.port()
|
||||
self.addr, self.port
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,12 +71,14 @@ impl Body {
|
||||
pub(crate) fn fixes() -> UpstreamFixes {
|
||||
let mut f = UpstreamFixes::default();
|
||||
f.fillet_retry_radius_as_first = true;
|
||||
f.fillet_rail_rim_on_face = true;
|
||||
f.fillet_refuse_rails_leaving_faces = true;
|
||||
f.fillet_refuse_face_consuming_blends = true;
|
||||
f.fillet_refuse_non_manifold_result = true;
|
||||
f.fillet_vertex_error_diagnostics = true;
|
||||
f.fillet_corner_ball_patch = true;
|
||||
f.fillet_chamfer_cone_on_arc = true;
|
||||
f.fillet_torus_center_on_axis = true;
|
||||
f.offset_excess_faces_after_merge = true;
|
||||
f.offset_clip_right_side_out = true;
|
||||
f.revolution_offset_axis_both_sides = true;
|
||||
@@ -170,6 +172,7 @@ pub(crate) fn fixes() -> UpstreamFixes {
|
||||
f.local_operation_step_walls = true;
|
||||
f.edge_check_orders_tangent_faces = true;
|
||||
f.thicken_planar_side_walls = true;
|
||||
f.fillet_torus_span_from_start_section = true;
|
||||
f.fillet_knife_edge_rounds = true;
|
||||
f.revolution_round_offset_collapses = true;
|
||||
f.fillet_chamfer_cone_stored_as_cone = true;
|
||||
@@ -184,9 +187,12 @@ pub(crate) fn fixes() -> UpstreamFixes {
|
||||
f.cylinder_plane_tangent_within_tolerance = true;
|
||||
f.boolean_wire_edge_keeps_faceuse_shell = true;
|
||||
f.fillet_nx1_tangent_corner_edge = true;
|
||||
f.fillet_torus_rail_on_axis = true;
|
||||
f.loops_go_on_round_the_vertex = true;
|
||||
f.revolve_splits_curves_on_axis = true;
|
||||
f.cover_wraps_across_closed_seam = true;
|
||||
f.fillet_trace_from_tangent_point_toward_next = true;
|
||||
f.fillet_tangent_point_one_sided_step = true;
|
||||
f.fillet_rails_meet_at_one_end_point = true;
|
||||
f.fillet_trace_reaches_rails = true;
|
||||
f.delete_vertex_joins_seam_iso_curve = true;
|
||||
|
||||
@@ -13080,28 +13080,6 @@ mod settings_search_tests {
|
||||
assert!(!drawn(&h, "Status bar"), "a miss shows nothing");
|
||||
}
|
||||
|
||||
/// The Placed features row is found by the name of every feature that has the marker, not
|
||||
/// only by its own label.
|
||||
#[test]
|
||||
fn preferences_search_finds_placement_handles_by_each_placed_feature() {
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
h.app.home.open = false;
|
||||
crate::prefs::open_at(&mut h.app, crate::prefs::PAGE_TOOLS);
|
||||
h.frames(2);
|
||||
let drawn = |h: &crate::scenario::Harness, label: &str| {
|
||||
let body = crate::scenario::rect_of("dialog:page_body");
|
||||
h.widget(label).is_some_and(|[x, y]| body.is_none_or(|body| body.contains(egui::pos2(x as f32, y as f32))))
|
||||
};
|
||||
for query in ["Placed features", "Rest", "Boss", "Snap Fit", "Box", "Cylinder", "Cone", "Sphere", "Torus", "Coil"] {
|
||||
h.app.prefs_window.shell.filter = query.into();
|
||||
h.frames(4);
|
||||
assert!(drawn(&h, "Placement handles"), "searching '{query}' should show Placement handles");
|
||||
}
|
||||
h.app.prefs_window.shell.filter = "zzzz-no-setting".into();
|
||||
h.frames(4);
|
||||
assert!(!drawn(&h, "Placement handles"), "a miss shows nothing");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn design_and_tools_short_labels_keep_legacy_search_and_reveal_the_real_control() {
|
||||
for language in ["en", "ja"] {
|
||||
|
||||
@@ -7,7 +7,7 @@ pub(super) fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
ui_kit::settings_heading(ui, app.tr("prefs.structure.tools"), app.tr("prefs.structure.tools_note"));
|
||||
ui_kit::settings_section(ui, "create", app.tr("ui.create"), |ui| {
|
||||
create_card(ui, |ui| {
|
||||
create_row(ui, app.tr("ui.hole"), None, &["hole"], true, |ui| {
|
||||
create_row(ui, app.tr("ui.hole"), &["hole"], true, |ui| {
|
||||
for (setting, help, key, legacy) in [
|
||||
(
|
||||
setting::hole_snaps as fn(&mut SolveApp, &mut egui::Ui) -> egui::Response,
|
||||
@@ -29,7 +29,7 @@ pub(super) fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
});
|
||||
}
|
||||
});
|
||||
create_row(ui, app.tr_label("Extrude"), None, &["extrude"], false, |ui| {
|
||||
create_row(ui, app.tr_label("Extrude"), &["extrude"], false, |ui| {
|
||||
for (setting, help, key) in [
|
||||
(
|
||||
setting::extrude_auto_to_object as fn(&mut SolveApp, &mut egui::Ui) -> egui::Response,
|
||||
@@ -56,9 +56,7 @@ pub(super) fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
crate::scenario::publish_target("prefs:extrude_view_return", response.rect);
|
||||
});
|
||||
});
|
||||
// Found by the name of every feature that has the marker, as well as by the label.
|
||||
let placed = PLACED_FEATURES.iter().map(|key| app.tr(key)).collect::<Vec<_>>().join(" ");
|
||||
create_row(ui, app.tr("prefs.design_tools.placement_group"), Some(&placed), &["rest", "boss", "snap_fit"], false, |ui| {
|
||||
create_row(ui, app.tr("prefs.design_tools.placement_group"), &["rest", "boss", "snap_fit"], false, |ui| {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
let response = setting::placement_handles(app, ui).backing_on_hover_text(app.tr("ui.placement_handles_tip"));
|
||||
crate::scenario::publish_target("prefs:placement_handles", response.rect);
|
||||
@@ -217,14 +215,8 @@ fn create_card(ui: &mut egui::Ui, rows: impl FnOnce(&mut egui::Ui)) {
|
||||
|
||||
/// One tool in the Create card: its icons in tiles and its name on the left, its settings on the
|
||||
/// right, a rule above every row but the first. Search uses the same block gate as `tool_group`.
|
||||
/// The features with a placement marker, by their name keys: Preferences search finds the
|
||||
/// Placed features row by any of them.
|
||||
const PLACED_FEATURES: [&str; 9] =
|
||||
["cmd.plastic_rest", "cmd.plastic_boss", "cmd.plastic_snap_fit", "ui.box", "ui.cylinder", "ui.cone", "ui.sphere", "ui.torus", "ui.coil"];
|
||||
|
||||
/// A tool's row; `hint` is more text the search finds the row by (it is not shown).
|
||||
fn create_row(ui: &mut egui::Ui, label: &str, hint: Option<&str>, icons: &[&str], first: bool, add: impl FnOnce(&mut egui::Ui)) {
|
||||
let gate = crate::ui_kit::settings_search::block_gate(ui, label, hint);
|
||||
fn create_row(ui: &mut egui::Ui, label: &str, icons: &[&str], first: bool, add: impl FnOnce(&mut egui::Ui)) {
|
||||
let gate = crate::ui_kit::settings_search::block_gate(ui, label, None);
|
||||
let before = crate::ui_kit::settings_search::hits(ui);
|
||||
if gate.as_ref().is_some_and(|gate| !gate.show) {
|
||||
crate::ui_kit::settings_search::run_hidden(ui, label, add);
|
||||
|
||||
@@ -18,7 +18,7 @@ Each group is a 120x64 figure at the left of its checkboxes, with a group name u
|
||||
| Extrude: Choose the operation automatically | Checkbox | `Prefs.auto_operation` | `true` | "Join, New Body or Cut, from what the material touches." | `tool-extrude` | |
|
||||
| Extrude: Turn the view to isometric when Extrude opens | Checkbox | `Prefs.extrude_view_isometric` | `false` | none | `tool-extrude` | Retained newer control, directly after automatic operation in the same group. |
|
||||
| Extrude: Go back to the earlier view when Extrude closes | Checkbox | `Prefs.extrude_view_return` | `false` | none | `tool-extrude` | Directly after isometric; disabled unless isometric is on. |
|
||||
| Placed features: Placement handles | Checkbox | `Prefs.placement_handles` | `false` | "A marker to drag the feature across the model and, where the feature can turn, a ring to turn it." | `tool-placement` | |
|
||||
| Rest, Boss, Snap Fit: Placement handles | Checkbox | `Prefs.placement_handles` | `false` | "A marker to drag the feature across the model and a ring to turn it." | `tool-placement` | |
|
||||
|
||||
## Assembly Move
|
||||
|
||||
|
||||
+2
-3
@@ -347,9 +347,8 @@ that every file came back signed. Without the secret (forks) the jobs build unsi
|
||||
- **Test-sign without publishing:** `gh workflow run release.yml -R tangency-labs/solidcraft --ref
|
||||
<branch> -f draft=false`, then check the Windows jobs' summaries and the `release-assets`
|
||||
artifact.
|
||||
- SignPath only signs requests whose jobs leading up to them all ran on GitHub-hosted runners, so
|
||||
the `version` and `windows` jobs stay on GitHub's runners while every other release job runs on
|
||||
Blacksmith.
|
||||
- SignPath only signs runs whose jobs all ran on GitHub-hosted runners, so keep the `version`,
|
||||
`icons` and `windows` jobs off self-hosted runners.
|
||||
|
||||
## Homebrew tap
|
||||
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
{"case":"adjacent-two-bodies-small","deferred":null,"error":null,"ints":{"api_status_result_vertex_edge_face_counts":[0,1,4,4,1],"body0_topology":{"blocks":[[128403,5122478]],"n":91},"body0_vertex_edge_face_counts":[6,7,2],"body1_topology":{"blocks":[[128403,5122478]],"n":91},"body1_vertex_edge_face_counts":[6,7,2],"result_topology":{"blocks":[[71196,1456742]],"n":49}},"topology":null,"values":{"body0_geometry":{"blocks":[[437.16011999999995,437.16011999999995,41736.076799999995]],"n":186},"body1_geometry":{"blocks":[[654.3001199999998,654.3001199999998,60852.5788]],"n":186},"fillet_point_step":[0.00001],"result_geometry":{"blocks":[[370.2274884099221,373.42748840992215,19893.411576968836]],"n":106}}}
|
||||
{"case":"attributed-box-all-edges-r1","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,24,48,26]},"topology":null,"values":{"attributes":{"blocks":[[120977.06194288167,120977.06194288167,7270559.503909666]],"n":174},"fillet_point_step":[0.00001]}}
|
||||
{"case":"attributed-box-cylinder-arc-round-seam-mid-r2","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,15,23,10]},"topology":null,"values":{"attributes":{"blocks":[[501488.75762421096,501488.75762421096,44364677.586285785]],"n":164},"fillet_point_step":[0.00001]}}
|
||||
{"case":"attributed-box-cylinder-arc-round-seam-opposite-r2","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,12,18,9]},"topology":null,"values":{"attributes":{"blocks":[[459392.6159228618,459392.6159228618,34982982.892283924]],"n":144},"fillet_point_step":[0.00001]}}
|
||||
{"case":"attributed-box-cylinder-arc-round-seam-opposite-r2","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[1001,10,15,8]},"topology":null,"values":{"attributes":{"blocks":[[480413.1912678502,480413.1912678502,34610352.53320036]],"n":140},"fillet_point_step":[0.00001]}}
|
||||
{"case":"attributed-box-cylinder-arc-round-seam-quarter-r2","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,12,18,9]},"topology":null,"values":{"attributes":{"blocks":[[459392.61597430886,459392.61597430886,34980837.82187664]],"n":144},"fillet_point_step":[0.00001]}}
|
||||
{"case":"attributed-box-cylinder-arc-round-seam-three-quarter-r2","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,12,18,9]},"topology":null,"values":{"attributes":{"blocks":[[459392.61603265547,459392.61603265547,34979788.966652505]],"n":144},"fillet_point_step":[0.00001]}}
|
||||
{"case":"attributed-box-cylinder-arc-round-seam-three-quarter-r2","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[1001,10,15,8]},"topology":null,"values":{"attributes":{"blocks":[[480413.19126785017,480413.19126785017,34607083.53077263]],"n":140},"fillet_point_step":[0.00001]}}
|
||||
{"case":"attributed-box-edge0-behavior0","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,10,15,7]},"topology":null,"values":{"attributes":{"blocks":[[352173.9822852112,352173.9822852112,21632188.503961973]],"n":114},"fillet_point_step":[0.00001]}}
|
||||
{"case":"attributed-box-edge0-behavior1","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,10,15,7]},"topology":null,"values":{"attributes":{"blocks":[[352190.9822852112,352190.9822852112,21633222.503961973]],"n":114},"fillet_point_step":[0.00001]}}
|
||||
{"case":"attributed-box-edge0-behavior2","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,10,15,7]},"topology":null,"values":{"attributes":{"blocks":[[352197.9822852112,352197.9822852112,21633766.503961973]],"n":114},"fillet_point_step":[0.00001]}}
|
||||
@@ -56,7 +56,7 @@
|
||||
{"case":"attributed-step-edge9-r1","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,14,21,9]},"topology":null,"values":{"attributes":{"blocks":[[518859.2787837579,518859.2787837579,44576274.79263492]],"n":162},"fillet_point_step":[0.00001]}}
|
||||
{"case":"attributed-variable-box-edge0-1-to-2","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,10,15,7]},"topology":null,"values":{"attributes":{"blocks":[[352177.32784037106,352177.32784037106,21632479.256082594]],"n":114},"fillet_point_step":[0.00001]}}
|
||||
{"case":"attributed-variable-box-edge6-2-to-1","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,10,15,7]},"topology":null,"values":{"attributes":{"blocks":[[352165.32784038194,352165.32784038194,21631500.77399957]],"n":114},"fillet_point_step":[0.00001]}}
|
||||
{"case":"big-rad-filleted_box-r1.0-top-edges","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,20,36,18],"mass_status":[0],"result_topology":{"blocks":[[281283,35317521],[285988,37334239],[211429,16888283]],"n":668}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[564.2389452228085,983.5015547299157,983.5015547299157,5.038410057395571,5.000000000000004,4.999999925784341,65212.870373066624,65652.9430384423,65652.94978056214,24587.53852411162,24776.42022262525,24776.420624076964],"result_geometry":{"blocks":[[1043.909543499877,1043.909543499877,132801.34831244644],[786.6137295003689,786.6137295003689,92148.64887494135],[548.8554048175439,548.8554048175439,83452.93188722912],[707.0839096995005,708.6839096995004,71698.26219582556],[75.48697566657812,75.48697566657812,960.1950347119977]],"n":1052},"selected_edge_before":{"blocks":[[112569.0,112569.0,7262465.274761933]],"n":136}}}
|
||||
{"case":"big-rad-filleted_box-r1.0-top-edges","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,20,36,18],"mass_status":[0],"result_topology":{"blocks":[[281291,35318792],[285980,37333166],[211429,16888283]],"n":668}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[564.238945222809,983.5015547299164,983.5015547299164,5.038410057395567,5.000000000000008,4.999999925784339,65212.8703730667,65652.94303844232,65652.94978056221,24587.53852411164,24776.42022262525,24776.420624076974],"result_geometry":{"blocks":[[1043.909543499877,1043.909543499877,132577.34831244644],[786.6137295003684,786.6137295003684,92148.64887494127],[548.8554048175436,548.8554048175436,83452.93188722905],[707.0839096995004,708.6839096995003,71698.26219582555],[75.48697566657789,75.48697566657789,960.1950347119924]],"n":1052},"selected_edge_before":{"blocks":[[112569.0,112569.0,7262465.274761933]],"n":136}}}
|
||||
{"case":"big-rad-filleted_box-r2.0-top-edges","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[1001,16,24,10],"mass_status":[0],"result_topology":{"blocks":[[281192,35301210],[207150,18894363]],"n":440}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[581.1150528699196,991.4160344161737,991.4160344161737,5.000000000000002,4.999999999999986,4.999999975615006,65845.15814429456,65969.77702349689,65969.78339230167,24785.400739525885,24785.40073952599,24785.400860155278],"result_geometry":{"blocks":[[1040.9539390913508,1040.9539390913508,137529.47261491715],[755.3864709086489,755.3864709086489,107054.86402295402],[580.7960876026176,593.5960876026177,48777.44680852571]],"n":676},"selected_edge_before":{"blocks":[[112569.0,112569.0,7262465.274761933]],"n":136}}}
|
||||
{"case":"big-rad-filleted_box-r3.0-top-edges","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[1001,16,24,10],"mass_status":[0],"result_topology":{"blocks":[[281192,35301210],[207150,18894363]],"n":440}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[581.1150528699196,991.4160344161737,991.4160344161737,5.000000000000002,4.999999999999986,4.999999975615006,65845.15814429456,65969.77702349689,65969.78339230167,24785.400739525885,24785.40073952599,24785.400860155278],"result_geometry":{"blocks":[[1040.9539390913508,1040.9539390913508,137529.47261491715],[755.3864709086489,755.3864709086489,107054.86402295402],[580.7960876026176,593.5960876026177,48777.44680852571]],"n":676},"selected_edge_before":{"blocks":[[112569.0,112569.0,7262465.274761933]],"n":136}}}
|
||||
{"case":"big-radius-box_diff_cyl_big_rad-r28-edge12","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,5,7,3],"mass_status":[1007],"result_topology":{"blocks":[[143392,7604201]],"n":110}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[],"result_geometry":{"blocks":[[3031.0901543149744,4114.4964121177445,280772.74380950886]],"n":200},"selected_edge_before":{"blocks":[[14306.057758366,14306.057758366,72955.84024483882]],"n":17}}}
|
||||
@@ -365,13 +365,13 @@
|
||||
{"case":"issue919-cylinder-rim-edge2-blend-curve-g1-r9.99","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[1007,2,3,3],"mass_status":[0],"result_topology":{"blocks":[[87159,2825806]],"n":60}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[1884.9556065113175,6283.18500796372,6283.18500796372,-1.4726672827265266e-8,1.4726670363343151e-8,9.999999999999998,994837.7456252411,994837.7456252411,314159.4891268237,0.011249309873292779,0.009398701749560834,-0.07608134049405635],"result_geometry":{"blocks":[[527.7040579753087,702.5531233622659,26658.127877841434]],"n":116},"selected_edge_before":{"blocks":[[14156.70132599177,14188.020009198175,71124.74995626012]],"n":17}}}
|
||||
{"case":"issue919-cylinder-rim-edge2-blend-curve-g2-r10.0","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[1007,2,3,3],"mass_status":[0],"result_topology":{"blocks":[[87159,2825806]],"n":60}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[1884.9556065113175,6283.18500796372,6283.18500796372,-1.4726672827265266e-8,1.4726670363343151e-8,9.999999999999998,994837.7456252411,994837.7456252411,314159.4891268237,0.011249309873292779,0.009398701749560834,-0.07608134049405635],"result_geometry":{"blocks":[[527.7040579753087,702.5531233622659,26658.127877841434]],"n":116},"selected_edge_before":{"blocks":[[14156.70132599177,14188.020009198175,71124.74995626012]],"n":17}}}
|
||||
{"case":"issue919-cylinder-rim-edge2-blend-curve-g2-r9.99","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[1007,2,3,3],"mass_status":[0],"result_topology":{"blocks":[[87159,2825806]],"n":60}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[1884.9556065113175,6283.18500796372,6283.18500796372,-1.4726672827265266e-8,1.4726670363343151e-8,9.999999999999998,994837.7456252411,994837.7456252411,314159.4891268237,0.011249309873292779,0.009398701749560834,-0.07608134049405635],"result_geometry":{"blocks":[[527.7040579753087,702.5531233622659,26658.127877841434]],"n":116},"selected_edge_before":{"blocks":[[14156.70132599177,14188.020009198175,71124.74995626012]],"n":17}}}
|
||||
{"case":"issue920-cylinder-rim-edge1-constant-r10.0","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,3,4,3],"mass_status":[0],"result_topology":{"blocks":[[101236,3961388]],"n":77}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[1570.796376097398,5235.988291000227,5235.988291000227,1.883310151187847e-7,2.0599482613885566e-7,11.500000235372115,947714.0853077865,947713.8663011974,240855.69263077818,0.016178781256665505,0.01479147006091632,-0.038040272280341014],"result_geometry":{"blocks":[[590.9590566994827,737.60927109562,34353.41686048178]],"n":138},"selected_edge_before":{"blocks":[[14095.70132599177,14127.020009198175,70343.74995626012]],"n":17}}}
|
||||
{"case":"issue920-cylinder-rim-edge1-constant-r10.0","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,3,4,3],"mass_status":[0],"result_topology":{"blocks":[[101236,3961388]],"n":77}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[1570.7963760973983,5235.988291000227,5235.988291000227,1.8833101490165886e-7,2.059948257914543e-7,11.500000235372113,947714.0853077865,947713.8663011974,240855.69263077818,0.01617878124484213,0.014791470053185648,-0.03804027228670748],"result_geometry":{"blocks":[[590.9590566994827,737.60927109562,34353.41686048178]],"n":138},"selected_edge_before":{"blocks":[[14095.70132599177,14127.020009198175,70343.74995626012]],"n":17}}}
|
||||
{"case":"issue920-cylinder-rim-edge1-constant-r9.99","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,3,5,4],"mass_status":[0],"result_topology":{"blocks":[[130313,6525256]],"n":96}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[1571.1549739407485,5237.781041311214,5237.781041311214,1.882665543057572e-7,2.0592431928935601e-7,11.497487105531828,947809.066019011,947808.8470124219,240963.9868810795,0.016178781243650686,0.014791470064092968,-0.03804027240533144],"result_geometry":{"blocks":[[596.9287052557129,789.048870953054,40056.385280241884]],"n":174},"selected_edge_before":{"blocks":[[14095.70132599177,14127.020009198175,70343.74995626012]],"n":17}}}
|
||||
{"case":"issue920-cylinder-rim-edge1-variable-constant-law-r10.0","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,3,4,3],"mass_status":[0],"result_topology":{"blocks":[[101236,3961388]],"n":77}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[1570.796376097398,5235.988291000227,5235.988291000227,1.883310151187847e-7,2.0599482613885566e-7,11.500000235372115,947714.0853077865,947713.8663011974,240855.69263077818,0.016178781256665505,0.01479147006091632,-0.038040272280341014],"result_geometry":{"blocks":[[590.9590566994827,737.60927109562,34353.41686048178]],"n":138},"selected_edge_before":{"blocks":[[14095.70132599177,14127.020009198175,70343.74995626012]],"n":17}}}
|
||||
{"case":"issue920-cylinder-rim-edge1-variable-constant-law-r10.0","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,3,4,3],"mass_status":[0],"result_topology":{"blocks":[[101236,3961388]],"n":77}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[1570.7963760973983,5235.988291000227,5235.988291000227,1.8833101490165886e-7,2.059948257914543e-7,11.500000235372113,947714.0853077865,947713.8663011974,240855.69263077818,0.01617878124484213,0.014791470053185648,-0.03804027228670748],"result_geometry":{"blocks":[[590.9590566994827,737.60927109562,34353.41686048178]],"n":138},"selected_edge_before":{"blocks":[[14095.70132599177,14127.020009198175,70343.74995626012]],"n":17}}}
|
||||
{"case":"issue920-cylinder-rim-edge1-variable-constant-law-r9.99","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,3,5,4],"mass_status":[0],"result_topology":{"blocks":[[130313,6525256]],"n":96}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[1571.1549739407485,5237.781041311214,5237.781041311214,1.882665543057572e-7,2.0592431928935601e-7,11.497487105531828,947809.066019011,947808.8470124219,240963.9868810795,0.016178781243650686,0.014791470064092968,-0.03804027240533144],"result_geometry":{"blocks":[[596.9287052557129,789.048870953054,40056.385280241884]],"n":174},"selected_edge_before":{"blocks":[[14095.70132599177,14127.020009198175,70343.74995626012]],"n":17}}}
|
||||
{"case":"issue920-cylinder-rim-edge2-constant-r10.0","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,3,4,3],"mass_status":[0],"result_topology":{"blocks":[[101242,3857334]],"n":77}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[1570.7963806678379,5235.989024424654,5235.989024424654,-2.0599479760804667e-7,-1.8833099051491674e-7,8.500002269459227,633554.821223871,633555.0402304601,240855.47414735783,-0.004930509781645134,-0.005392948677594708,-0.03804027233582019],"result_geometry":{"blocks":[[488.14921457115923,651.8980941809427,32888.688473480026]],"n":138},"selected_edge_before":{"blocks":[[14156.70132599177,14188.020009198175,71124.74995626012]],"n":17}}}
|
||||
{"case":"issue920-cylinder-rim-edge2-constant-r10.0","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,3,4,3],"mass_status":[0],"result_topology":{"blocks":[[101242,3857334]],"n":77}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[1570.7963806678383,5235.989024424656,5235.989024424656,-2.059947972606453e-7,-1.8833099001756472e-7,8.500002269459227,633554.8212238712,633555.0402304602,240855.4741473579,-0.004930509755303675,-0.005392948670318698,-0.038040272243961226],"result_geometry":{"blocks":[[488.1492145711593,651.8980941809427,32888.688473480026]],"n":138},"selected_edge_before":{"blocks":[[14156.70132599177,14188.020009198175,71124.74995626012]],"n":17}}}
|
||||
{"case":"issue920-cylinder-rim-edge2-constant-r9.99","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,3,5,4],"mass_status":[0],"result_topology":{"blocks":[[130319,6421375]],"n":96}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[1571.1549777992927,5237.781801224955,5237.781801224955,-2.0592429249187742e-7,-1.882665288112231e-7,8.502515462564519,634068.7746551691,634068.9936617584,240963.76368978285,-0.004930509781670225,-0.0053929487658412765,-0.038040272398055484],"result_geometry":{"blocks":[[624.2477803851453,823.5034052152915,53997.852758887406]],"n":174},"selected_edge_before":{"blocks":[[14156.70132599177,14188.020009198175,71124.74995626012]],"n":17}}}
|
||||
{"case":"issue920-cylinder-rim-edge2-variable-constant-law-r10.0","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,3,4,3],"mass_status":[0],"result_topology":{"blocks":[[101242,3857334]],"n":77}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[1570.7963806678379,5235.989024424654,5235.989024424654,-2.0599479760804667e-7,-1.8833099051491674e-7,8.500002269459227,633554.821223871,633555.0402304601,240855.47414735783,-0.004930509781645134,-0.005392948677594708,-0.03804027233582019],"result_geometry":{"blocks":[[488.14921457115923,651.8980941809427,32888.688473480026]],"n":138},"selected_edge_before":{"blocks":[[14156.70132599177,14188.020009198175,71124.74995626012]],"n":17}}}
|
||||
{"case":"issue920-cylinder-rim-edge2-variable-constant-law-r10.0","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,3,4,3],"mass_status":[0],"result_topology":{"blocks":[[101242,3857334]],"n":77}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[1570.7963806678383,5235.989024424656,5235.989024424656,-2.059947972606453e-7,-1.8833099001756472e-7,8.500002269459227,633554.8212238712,633555.0402304602,240855.4741473579,-0.004930509755303675,-0.005392948670318698,-0.038040272243961226],"result_geometry":{"blocks":[[488.1492145711593,651.8980941809427,32888.688473480026]],"n":138},"selected_edge_before":{"blocks":[[14156.70132599177,14188.020009198175,71124.74995626012]],"n":17}}}
|
||||
{"case":"issue920-cylinder-rim-edge2-variable-constant-law-r9.99","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,3,5,4],"mass_status":[0],"result_topology":{"blocks":[[130319,6421375]],"n":96}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[1571.1549777992927,5237.781801224955,5237.781801224955,-2.0592429249187742e-7,-1.882665288112231e-7,8.502515462564519,634068.7746551691,634068.9936617584,240963.76368978285,-0.004930509781670225,-0.0053929487658412765,-0.038040272398055484],"result_geometry":{"blocks":[[624.2477803851453,823.5034052152915,53997.852758887406]],"n":174},"selected_edge_before":{"blocks":[[14156.70132599177,14188.020009198175,71124.74995626012]],"n":17}}}
|
||||
{"case":"issue_2020_mitred_put_back_d9_adjusted_topology","deferred":null,"error":null,"ints":{"all_corner_vertex_state":{"blocks":[[52,908]],"n":32},"before_direct_topology":{"blocks":[[78,1076]],"n":41},"corner_adjustment_flags":[0,0,0,1,0,0,2,0,0,3,0,0],"corner_calculation_flags":[0,0,0,0,1,0,0,0,2,0,0,0,3,0,0,0],"offsets_after_state":{"blocks":[[486632,29434852]],"n":108},"offsets_before_state":{"blocks":[[486632,29434852]],"n":108},"prefix_state":{"blocks":[[129520,6117777]],"n":245},"prefix_statuses":[0,0,0,0,0]},"topology":null,"values":{"actual_solver_corner_parameters":{"blocks":[[26.792846803190923,26.792846803190923,453.0330947014537]],"n":28},"all_corner_vertices":{"blocks":[[144.00000000000006,144.00000000000006,952.000000000001]],"n":20},"fillet_point_step":[0.00001],"offsets_after_distances":[-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0],"offsets_before_distances":[-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0],"selected_edge_before":{"blocks":[[56502.00000000001,56502.00000000001,1722373.9091997326]],"n":68}}}
|
||||
{"case":"issue_2020_mitred_put_back_d9_corner_edges","deferred":null,"error":null,"ints":{"all_corner_vertex_state":{"blocks":[[32,612]],"n":32},"before_direct_topology":{"blocks":[[78,1076]],"n":41},"corner_adjustment_flags":[0,0,0,1,0,0,2,0,0,3,0,0],"corner_calculation_flags":[0,0,0,0,1,0,0,0,2,0,0,0,3,0,0,0],"corner_edge_geometry_state":{"blocks":[[56154,1403810]],"n":48},"generated_geometry_state":{"blocks":[[228206,15463960]],"n":132},"offsets_after_state":{"blocks":[[486632,29434852]],"n":108},"offsets_before_state":{"blocks":[[486632,29434852]],"n":108},"prefix_state":{"blocks":[[129520,6117777]],"n":245},"prefix_statuses":[0,0,0,0,0,0,0]},"topology":null,"values":{"actual_solver_corner_parameters":{"blocks":[[26.792846803190923,26.792846803190923,453.0330947014537]],"n":28},"all_corner_vertices":{"blocks":[[276.00000000000034,291.9999999999999,3768.00000000001]],"n":40},"corner_edge_geometry_samples":{"blocks":[[701.094308273315,730.1495971314017,20855.868192437465]],"n":76},"fillet_point_step":[0.00001],"generated_geometry_samples":{"blocks":[[-0.7999999999997469,1.4e+21,-7.000000000000052e+20],[61.60000000000035,2e+20,-1e+20]],"n":272},"offsets_after_distances":[-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0],"offsets_before_distances":[-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0],"selected_edge_before":{"blocks":[[56502.00000000001,56502.00000000001,1722373.9091997326]],"n":68}}}
|
||||
@@ -706,9 +706,9 @@
|
||||
{"case":"stubs4-degenerate-bottle-e10-r0.5","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[1001,26,40,16],"mass_status":[0],"result_topology":{"blocks":[[281457,35341216],[282271,36580789],[241977,24565779]],"n":730}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[33.11819079597675,11.066834056610833,11.066834056610833,-1.7609358703810353e-6,-3.0259935914096315e-6,2.0788625909315117,64.79258549927602,66.6733025969641,5.277123502477886,-0.00010373323274112306,-0.00006130966599405097,0.000020823413685507833],"result_geometry":{"blocks":[[912.7406198383134,996.1574227558623,139865.57295882053],[1710.5690534181747,1900.7696584750158,195545.28934387196],[536.122128285832,587.7649824357395,53296.94403726827],[1116.695548284241,1296.7760762038654,136279.5389856879],[81.3031298885769,99.10219410214427,4292.377944642321]],"n":1112},"selected_edge_before":{"blocks":[[14124.027919677248,14129.830196394449,70404.16195467954]],"n":17}}}
|
||||
{"case":"stubs4-degenerate-bottle-e13-r0.5","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[1001,26,40,16],"mass_status":[0],"result_topology":{"blocks":[[281457,35341216],[282271,36580789],[241977,24565779]],"n":730}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[33.11819079597675,11.066834056610833,11.066834056610833,-1.7609358703810353e-6,-3.0259935914096315e-6,2.0788625909315117,64.79258549927602,66.6733025969641,5.277123502477886,-0.00010373323274112306,-0.00006130966599405097,0.000020823413685507833],"result_geometry":{"blocks":[[912.7406198383134,996.1574227558623,139865.57295882053],[1710.5690534181747,1900.7696584750158,195545.28934387196],[536.122128285832,587.7649824357395,53296.94403726827],[1116.695548284241,1296.7760762038654,136279.5389856879],[81.3031298885769,99.10219410214427,4292.377944642321]],"n":1112},"selected_edge_before":{"blocks":[[14127.137890774764,14132.830196425228,70408.49505041902]],"n":17}}}
|
||||
{"case":"stubs4-degenerate-bottle-e2-r0.5","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[1001,26,40,16],"mass_status":[0],"result_topology":{"blocks":[[281457,35341216],[282271,36580789],[241977,24565779]],"n":730}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[33.11819079597675,11.066834056610833,11.066834056610833,-1.7609358703810353e-6,-3.0259935914096315e-6,2.0788625909315117,64.79258549927602,66.6733025969641,5.277123502477886,-0.00010373323274112306,-0.00006130966599405097,0.000020823413685507833],"result_geometry":{"blocks":[[912.7406198383134,996.1574227558623,139865.57295882053],[1710.5690534181747,1900.7696584750158,195545.28934387196],[536.122128285832,587.7649824357395,53296.94403726827],[1116.695548284241,1296.7760762038654,136279.5389856879],[81.3031298885769,99.10219410214427,4292.377944642321]],"n":1112},"selected_edge_before":{"blocks":[[14121.830195910168,14121.830195910168,70458.47793958586]],"n":17}}}
|
||||
{"case":"stubs4-degenerate-brepForFillet2-e5-r0.5","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,11,17,8],"mass_status":[0],"result_topology":{"blocks":[[284319,36121624],[75249,2183309]],"n":315}},"topology":null,"values":{"fillet_point_step":[0.001],"mass_properties":[1440.2512042078263,3616.228017328615,3616.228017328615,3.4572526876318976,6.96874544266061,11.462739678939666,855608.6103133827,741301.6535002191,324387.4213297927,303890.9378205091,98588.82988258151,79222.6133688387],"result_geometry":{"blocks":[[977.961010051866,1132.158704471907,112679.82359158955],[1036.8292124050079,1132.679254497851,128907.22375615075]],"n":494},"selected_edge_before":{"blocks":[[14104.237249363965,14104.237249363965,70989.17788508131]],"n":17}}}
|
||||
{"case":"stubs4-degenerate-brepForFillet2-e7-r0.5","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,11,17,8],"mass_status":[0],"result_topology":{"blocks":[[284315,36028144],[75276,1943882]],"n":315}},"topology":null,"values":{"fillet_point_step":[0.001],"mass_properties":[1434.446719545761,3614.981574290599,3614.981574290599,3.458487141241509,6.968101849333976,11.456808441115292,854576.7487812358,740358.908198703,324305.38297009957,303605.7870037983,98562.20396720385,79209.84335560785],"result_geometry":{"blocks":[[914.982144292844,1052.8700587476103,105507.89657827417],[1124.5314332536234,1268.0947969440074,136640.56046809343]],"n":494},"selected_edge_before":{"blocks":[[14135.395411755704,14135.395411755704,71369.33404886544]],"n":17}}}
|
||||
{"case":"stubs4-degenerate-brepForFillet2-e9-r0.5","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,11,17,8],"mass_status":[0],"result_topology":{"blocks":[[284315,36122025],[75252,2183684]],"n":315}},"topology":null,"values":{"fillet_point_step":[0.001],"mass_properties":[1440.2673071459665,3616.232846225047,3616.232846225047,3.4572613565128156,6.9681461275206615,11.462734211720338,855582.4493567429,741302.5155547927,324361.5489719664,303878.46230592194,98589.12346911957,79198.82032237474],"result_geometry":{"blocks":[[941.846900626913,1095.734815081679,106272.87800830806],[1191.5145200824738,1287.0778837728578,145508.12565509713]],"n":494},"selected_edge_before":{"blocks":[[14144.237249363965,14144.237249363965,71425.17788508131]],"n":17}}}
|
||||
{"case":"stubs4-degenerate-brepForFillet2-e5-r0.5","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,11,17,8],"mass_status":[0],"result_topology":{"blocks":[[284319,36121624],[75249,2183309]],"n":315}},"topology":null,"values":{"fillet_point_step":[0.001],"mass_properties":[1440.2514087704533,3616.228750038445,3616.228750038445,3.457251172602004,6.96874443703026,11.462741006165695,855608.8549164346,741301.9063565227,324387.4354611304,303890.9642818426,98588.77684098834,79222.60746546565],"result_geometry":{"blocks":[[977.9593770006319,1132.1570714206728,112679.78113225622],[1036.828160581436,1132.6782038029176,128907.1646927588]],"n":494},"selected_edge_before":{"blocks":[[14104.237249363965,14104.237249363965,70989.17788508131]],"n":17}}}
|
||||
{"case":"stubs4-degenerate-brepForFillet2-e7-r0.5","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,11,17,8],"mass_status":[0],"result_topology":{"blocks":[[284315,36028144],[75276,1943882]],"n":315}},"topology":null,"values":{"fillet_point_step":[0.001],"mass_properties":[1434.4468924323423,3614.9815411983363,3614.9815411983363,3.4584870631799705,6.9681018581584295,11.45680847593723,854576.7452027344,740358.9010593686,324305.3770227939,303605.7854831291,98562.20093185853,79209.84097411254],"result_geometry":{"blocks":[[914.9820844486802,1052.8699989034462,105507.89478294915],[1124.5314068643559,1268.0947705547399,136640.55870717773]],"n":494},"selected_edge_before":{"blocks":[[14135.395411755704,14135.395411755704,71369.33404886544]],"n":17}}}
|
||||
{"case":"stubs4-degenerate-brepForFillet2-e9-r0.5","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,11,17,8],"mass_status":[0],"result_topology":{"blocks":[[284315,36122025],[75252,2183684]],"n":315}},"topology":null,"values":{"fillet_point_step":[0.001],"mass_properties":[1440.2673124917385,3616.232865303477,3616.232865303477,3.4572613193719812,6.968146164613963,11.462734246179187,855582.4593741557,741302.5220788062,324361.5529561333,303878.4671120746,98589.1222390095,79198.81936566049],"result_geometry":{"blocks":[[941.8468663093593,1095.7347807641254,106272.87711605163],[1191.5144945435786,1287.0778582339628,145508.12373045052]],"n":494},"selected_edge_before":{"blocks":[[14144.237249363965,14144.237249363965,71425.17788508131]],"n":17}}}
|
||||
{"case":"stubs4-degenerate-caps_coons-e5-r0.5","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[1001,5,6,2],"mass_status":[1007],"result_topology":{"blocks":[[114369,5170962]],"n":103}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[],"result_geometry":{"blocks":[[33.20006041930303,362.35023081673893,6668.477882377478]],"n":164},"selected_edge_before":{"blocks":[[14037.1094889089,14070.282001512544,70183.14505475634]],"n":17}}}
|
||||
{"case":"stubs4-degenerate-nurb_cone-e1-r0.5","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[1001,16,24,10],"mass_status":[0],"result_topology":{"blocks":[[281552,35353171],[207130,17929604]],"n":440}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[314.8325246121948,213.7213448689033,213.7213448689033,-25.256791575512928,-20.641446371057185,-48.78070940849276,602531.7041465972,647001.3562174569,230178.1359116786,215876.09028929376,263548.4756783311,111869.03168332015],"result_geometry":{"blocks":[[-4492.250713961271,4649.51618583184,-535943.3213095476],[-4052.741456601031,4286.165134468015,-516235.4603248068],[-2714.7015550832602,2771.3320313018985,-230085.89935783745]],"n":676},"selected_edge_before":{"blocks":[[13749.737417808685,14322.262582191315,66614.77086913426]],"n":17}}}
|
||||
{"case":"stubs4-degenerate-nurb_cone-e11-r0.5","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[1001,16,24,10],"mass_status":[0],"result_topology":{"blocks":[[281552,35353171],[207130,17929604]],"n":440}},"topology":null,"values":{"fillet_point_step":[0.001],"mass_properties":[314.8325246121948,213.7213448689033,213.7213448689033,-25.256791575512928,-20.641446371057185,-48.78070940849276,602531.7041465972,647001.3562174569,230178.1359116786,215876.09028929376,263548.4756783311,111869.03168332015],"result_geometry":{"blocks":[[-4492.250713961271,4649.51618583184,-535943.3213095476],[-4052.741456601031,4286.165134468015,-516235.4603248068],[-2714.7015550832602,2771.3320313018985,-230085.89935783745]],"n":676},"selected_edge_before":{"blocks":[[13738.84427117878,14353.15572882122,66458.77782534131]],"n":17}}}
|
||||
@@ -716,10 +716,10 @@
|
||||
{"case":"stubs4-degenerate-nurb_cone-e3-r0.5","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[1001,16,24,10],"mass_status":[0],"result_topology":{"blocks":[[281552,35353171],[207130,17929604]],"n":440}},"topology":null,"values":{"fillet_point_step":[0.001],"mass_properties":[314.8325246121948,213.7213448689033,213.7213448689033,-25.256791575512928,-20.641446371057185,-48.78070940849276,602531.7041465972,647001.3562174569,230178.1359116786,215876.09028929376,263548.4756783311,111869.03168332015],"result_geometry":{"blocks":[[-4492.250713961271,4649.51618583184,-535943.3213095476],[-4052.741456601031,4286.165134468015,-516235.4603248068],[-2714.7015550832602,2771.3320313018985,-230085.89935783745]],"n":676},"selected_edge_before":{"blocks":[[13744.700724671131,14331.299275328869,66625.11707259771]],"n":17}}}
|
||||
{"case":"stubs4-degenerate-nurb_cone-e5-r0.5","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[1001,16,24,10],"mass_status":[0],"result_topology":{"blocks":[[281552,35353171],[207130,17929604]],"n":440}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[314.8325246121948,213.7213448689033,213.7213448689033,-25.256791575512928,-20.641446371057185,-48.78070940849276,602531.7041465972,647001.3562174569,230178.1359116786,215876.09028929376,263548.4756783311,111869.03168332015],"result_geometry":{"blocks":[[-4492.250713961271,4649.51618583184,-535943.3213095476],[-4052.741456601031,4286.165134468015,-516235.4603248068],[-2714.7015550832602,2771.3320313018985,-230085.89935783745]],"n":676},"selected_edge_before":{"blocks":[[13746.484192903254,14333.515807096746,66632.79245387115]],"n":17}}}
|
||||
{"case":"stubs4-degenerate-nurb_cone-e8-r0.5","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[1001,16,24,10],"mass_status":[0],"result_topology":{"blocks":[[281552,35353171],[207130,17929604]],"n":440}},"topology":null,"values":{"fillet_point_step":[0.001],"mass_properties":[314.8325246121948,213.7213448689033,213.7213448689033,-25.256791575512928,-20.641446371057185,-48.78070940849276,602531.7041465972,647001.3562174569,230178.1359116786,215876.09028929376,263548.4756783311,111869.03168332015],"result_geometry":{"blocks":[[-4492.250713961271,4649.51618583184,-535943.3213095476],[-4052.741456601031,4286.165134468015,-516235.4603248068],[-2714.7015550832602,2771.3320313018985,-230085.89935783745]],"n":676},"selected_edge_before":{"blocks":[[13736.67714277342,14349.32285722658,66464.01751703939]],"n":17}}}
|
||||
{"case":"stubs4-degenerate-test5-e2-r0.5","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,11,15,5],"mass_status":[1007],"result_topology":{"blocks":[[286149,29279394]],"n":224}},"topology":null,"values":{"fillet_point_step":[0.001],"mass_properties":[],"result_geometry":{"blocks":[[85.68068056302971,378.2896320462491,18376.1619712122],[88.63423681712015,173.94735705977516,4988.654837848823]],"n":404},"selected_edge_before":{"blocks":[[14033.1235,14049.423499999999,70158.1555]],"n":17}}}
|
||||
{"case":"stubs4-degenerate-test5-e3-r0.5","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,11,15,5],"mass_status":[1007],"result_topology":{"blocks":[[286141,29613358]],"n":224}},"topology":null,"values":{"fillet_point_step":[0.001],"mass_properties":[],"result_geometry":{"blocks":[[92.59197973664538,371.60165077974125,21582.08948024115],[81.40325384112059,180.63533832628286,3655.954013313813]],"n":404},"selected_edge_before":{"blocks":[[14034.1235,14050.423499999999,70167.3055]],"n":17}}}
|
||||
{"case":"stubs4-degenerate-test5-e5-r0.5","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,11,15,5],"mass_status":[1007],"result_topology":{"blocks":[[286142,29613638]],"n":224}},"topology":null,"values":{"fillet_point_step":[0.001],"mass_properties":[],"result_geometry":{"blocks":[[102.47681984275921,382.38943984881865,23475.246803095986],[126.08764664308549,197.3307525314586,7072.332913603795]],"n":404},"selected_edge_before":{"blocks":[[14052.4235,14052.4235,70331.6555]],"n":17}}}
|
||||
{"case":"stubs4-degenerate-test5-e9-r0.5","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,11,15,5],"mass_status":[1007],"result_topology":{"blocks":[[286145,29532836]],"n":224}},"topology":null,"values":{"fillet_point_step":[0.001],"mass_properties":[],"result_geometry":{"blocks":[[136.43077066828778,400.93935387465575,31560.32044426053],[92.02990150619084,178.78083850562157,7363.80759763486]],"n":404},"selected_edge_before":{"blocks":[[14056.4235,14056.4235,70328.8055]],"n":17}}}
|
||||
{"case":"stubs4-degenerate-test5-e2-r0.5","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,11,15,5],"mass_status":[1007],"result_topology":{"blocks":[[286149,29279394]],"n":224}},"topology":null,"values":{"fillet_point_step":[0.001],"mass_properties":[],"result_geometry":{"blocks":[[85.68068516522655,378.28962485244244,18376.163061145715],[88.63424007630162,173.94735361807966,4988.6552368631865]],"n":404},"selected_edge_before":{"blocks":[[14033.1235,14049.423499999999,70158.1555]],"n":17}}}
|
||||
{"case":"stubs4-degenerate-test5-e3-r0.5","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,11,15,5],"mass_status":[1007],"result_topology":{"blocks":[[286141,29613358]],"n":224}},"topology":null,"values":{"fillet_point_step":[0.001],"mass_properties":[],"result_geometry":{"blocks":[[92.5919820900713,371.60164723693,21582.09004605816],[81.40325931621032,180.63533123359207,3655.954458180061]],"n":404},"selected_edge_before":{"blocks":[[14034.1235,14050.423499999999,70167.3055]],"n":17}}}
|
||||
{"case":"stubs4-degenerate-test5-e5-r0.5","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,11,15,5],"mass_status":[1007],"result_topology":{"blocks":[[286142,29613638]],"n":224}},"topology":null,"values":{"fillet_point_step":[0.001],"mass_properties":[],"result_geometry":{"blocks":[[102.47681659976956,382.38943660577735,23475.24605930542],[126.08763981341828,197.33074608584062,7072.332436838247]],"n":404},"selected_edge_before":{"blocks":[[14052.4235,14052.4235,70331.6555]],"n":17}}}
|
||||
{"case":"stubs4-degenerate-test5-e9-r0.5","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[0,11,15,5],"mass_status":[1007],"result_topology":{"blocks":[[286145,29532836]],"n":224}},"topology":null,"values":{"fillet_point_step":[0.001],"mass_properties":[],"result_geometry":{"blocks":[[136.43076299695784,400.9393465658624,31560.31867032784],[92.02989898141172,178.78083612575566,7363.807143274321]],"n":404},"selected_edge_before":{"blocks":[[14056.4235,14056.4235,70328.8055]],"n":17}}}
|
||||
{"case":"stubs4-delete-edge-bottle-e16-r0.5","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[1001,26,40,16],"mass_status":[0],"result_topology":{"blocks":[[281457,35341216],[282271,36580789],[241977,24565779]],"n":730}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[33.11819079597675,11.066834056610833,11.066834056610833,-1.7609358703810353e-6,-3.0259935914096315e-6,2.0788625909315117,64.79258549927602,66.6733025969641,5.277123502477886,-0.00010373323274112306,-0.00006130966599405097,0.000020823413685507833],"result_geometry":{"blocks":[[912.7406198383134,996.1574227558623,139865.57295882053],[1710.5690534181747,1900.7696584750158,195545.28934387196],[536.122128285832,587.7649824357395,53296.94403726827],[1116.695548284241,1296.7760762038654,136279.5389856879],[81.3031298885769,99.10219410214427,4292.377944642321]],"n":1112},"selected_edge_before":{"blocks":[[14131.330290252932,14137.869747404748,70425.4044659795]],"n":17}}}
|
||||
{"case":"stubs4-delete-edge-bottle-e19-r0.5","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[1001,26,40,16],"mass_status":[0],"result_topology":{"blocks":[[281457,35341216],[282271,36580789],[241977,24565779]],"n":730}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[33.11819079597675,11.066834056610833,11.066834056610833,-1.7609358703810353e-6,-3.0259935914096315e-6,2.0788625909315117,64.79258549927602,66.6733025969641,5.277123502477886,-0.00010373323274112306,-0.00006130966599405097,0.000020823413685507833],"result_geometry":{"blocks":[[912.7406198383134,996.1574227558623,139865.57295882053],[1710.5690534181747,1900.7696584750158,195545.28934387196],[536.122128285832,587.7649824357395,53296.94403726827],[1116.695548284241,1296.7760762038654,136279.5389856879],[81.3031298885769,99.10219410214427,4292.377944642321]],"n":1112},"selected_edge_before":{"blocks":[[14140.869745985467,14140.869745985467,70500.33847700804]],"n":17}}}
|
||||
{"case":"stubs4-delete-edge-bottle-e27-r0.5","deferred":null,"error":null,"ints":{"api_status_vertex_edge_face_counts":[1001,26,40,16],"mass_status":[0],"result_topology":{"blocks":[[281457,35341216],[282271,36580789],[241977,24565779]],"n":730}},"topology":null,"values":{"fillet_point_step":[0.00001],"mass_properties":[33.11819079597675,11.066834056610833,11.066834056610833,-1.7609358703810353e-6,-3.0259935914096315e-6,2.0788625909315117,64.79258549927602,66.6733025969641,5.277123502477886,-0.00010373323274112306,-0.00006130966599405097,0.000020823413685507833],"result_geometry":{"blocks":[[912.7406198383134,996.1574227558623,139865.57295882053],[1710.5690534181747,1900.7696584750158,195545.28934387196],[536.122128285832,587.7649824357395,53296.94403726827],[1116.695548284241,1296.7760762038654,136279.5389856879],[81.3031298885769,99.10219410214427,4292.377944642321]],"n":1112},"selected_edge_before":{"blocks":[[14051.213183992433,14054.532782269893,70249.37439179729]],"n":17}}}
|
||||
|
||||
+11
-11
@@ -75,7 +75,7 @@
|
||||
{"case":"gen0024","deferred":null,"error":null,"ints":{"status":[1001]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[1.3725441272977728,0.6862720636488864],"point":{"blocks":[[15.294333106129692,74.24695926186625,277.26166903632435]],"n":80}}}
|
||||
{"case":"gen0025","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[0.07497759728747988,0.07497759728747988],"point":{"blocks":[[20.200632377815086,45.69462178879397,583.6012086259886]],"n":80}}}
|
||||
{"case":"gen0026","deferred":null,"error":null,"ints":{"status":[1001]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[-351325.87341182795,351325.87341182795],"point":{"blocks":[[-116004.64807679922,1232437.5830538757,-6675641.576840369]],"n":80}}}
|
||||
{"case":"gen0027","deferred":null,"error":null,"ints":{"status":[1001]},"topology":null,"values":{"fillet_point_step":[0.001],"offsets":[0.27063687841080664,0.27063687841080664],"point":{"blocks":[[19.619520279904034,35.31714247266496,447.5149876339198]],"n":80}}}
|
||||
{"case":"gen0027","deferred":null,"error":null,"ints":{"status":[1001]},"topology":null,"values":{"fillet_point_step":[0.001],"offsets":[0.27063687841080664,0.27063687841080664],"point":{"blocks":[[19.619452599694057,35.31708324180408,447.5100656516453]],"n":80}}}
|
||||
{"case":"gen0028","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[-0.9020857020487547,-0.9020857020487547],"point":{"blocks":[[19.032946418962048,40.723307502640814,475.973364731492]],"n":80}}}
|
||||
{"case":"gen0029","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[0.8626803266400759,-0.8626803266400759],"point":{"blocks":[[45.73091678001725,67.7182218583911,1482.7043233607176]],"n":80}}}
|
||||
{"case":"gen0030","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[0.14397316956260245,-0.14397316956260245],"point":{"blocks":[[25.5536390461404,33.613732647770306,732.1313875262564]],"n":80}}}
|
||||
@@ -97,7 +97,7 @@
|
||||
{"case":"gen0046","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[0.32184618316829305,0.32184618316829305],"point":{"blocks":[[19.763840541773273,39.03486785606795,551.1708791831503]],"n":80}}}
|
||||
{"case":"gen0047","deferred":null,"error":null,"ints":{"status":[1001]},"topology":null,"values":{"fillet_point_step":[0.001],"offsets":[0.33312092349014705,-0.33312092349014705],"point":{"blocks":[[7.040340626471737,26.13161034528739,144.9432934426138]],"n":80}}}
|
||||
{"case":"gen0048","deferred":null,"error":null,"ints":{"extend":[0,0],"solver_status":[0],"status":[1006]},"topology":null,"values":{"fillet_point_step":[0.00001],"g1":[0.5,0.8626636515623405],"knots_in":[0.5,0.8626636515623405]}}
|
||||
{"case":"gen0049","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.001],"offsets":[-1.5245077199873365,-1.5245077199873365],"point":{"blocks":[[-14.301965863707672,48.973090396590756,-788.1026734875113]],"n":80}}}
|
||||
{"case":"gen0049","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.001],"offsets":[-1.5245077199873365,-1.5245077199873365],"point":{"blocks":[[-15.073318613688702,48.202897057202925,-770.0017196509854]],"n":80}}}
|
||||
{"case":"gen0050","deferred":null,"error":null,"ints":{"status":[1001]},"topology":null,"values":{"fillet_point_step":[0.001],"offsets":[-0.7310627321796721,0.36553136608983605],"point":{"blocks":[[30.468690740854942,56.973252644511575,855.5218606973722]],"n":80}}}
|
||||
{"case":"gen0051","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[0.19181242535671425,0.19181242535671425],"point":{"blocks":[[167138.68739382955,167166.5633069897,1337537.0199421444]],"n":80}}}
|
||||
{"case":"gen0052","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[0.00008705041260730905,-0.00008705041260730905],"point":{"blocks":[[373.3856616499212,1329.6637292290825,4075.836177353789]],"n":80}}}
|
||||
@@ -185,7 +185,7 @@
|
||||
{"case":"gen0134","deferred":null,"error":null,"ints":{"status":[1001]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[-0.15418256711648276,-0.07709128355824138],"point":{"blocks":[[-0.26881742962136523,29.37482073855683,-131.35008253638503]],"n":80}}}
|
||||
{"case":"gen0135","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[0.9760961831605279,-0.9760961831605279],"point":{"blocks":[[21.025297812363757,101.96423972501205,407.6903925943848]],"n":80}}}
|
||||
{"case":"gen0136","deferred":null,"error":null,"ints":{"extend":[1,0],"solver_status":[0],"status":[1001]},"topology":null,"values":{"fillet_point_step":[0.00001],"g1":[0.3021049424762976,0.31372290219633897,0.5],"knots_in":[0.3021049424762976,0.31372290219633897,0.5]}}
|
||||
{"case":"gen0137","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.001],"offsets":[-1.0576790822895008,-1.0576790822895008],"point":{"blocks":[[13.566495429311722,46.98956097137515,253.36975225263933]],"n":80}}}
|
||||
{"case":"gen0137","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.001],"offsets":[-1.0576790822895008,-1.0576790822895008],"point":{"blocks":[[9.335519627322816,46.872479150055376,169.28060844591442]],"n":80}}}
|
||||
{"case":"gen0138","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[-0.6683590933820529,0.6683590933820529],"point":{"blocks":[[19.01514505736307,42.42894026927875,341.8749123784208]],"n":80}}}
|
||||
{"case":"gen0139","deferred":null,"error":null,"ints":{"status":[1001]},"topology":null,"values":{"fillet_point_step":[0.001],"offsets":[0.0,0.0],"point":{"blocks":[[8.569487284056807,25.3198495572189,4.416521455062805]],"n":80}}}
|
||||
{"case":"gen0140","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[-0.4624016120819063,-0.4624016120819063],"point":{"blocks":[[17.173329837472025,41.53570566654,441.65602257301765]],"n":80}}}
|
||||
@@ -211,7 +211,7 @@
|
||||
{"case":"gen0160","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[0.5496454922726274,0.5496454922726274],"point":{"blocks":[[16.564768931704776,37.759077703189824,298.1480286446076]],"n":80}}}
|
||||
{"case":"gen0161","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[287214.5259962959,287214.5259962959],"point":{"blocks":[[3816288.9089792436,10671278.914155519,38669663.12654833]],"n":80}}}
|
||||
{"case":"gen0162","deferred":null,"error":null,"ints":{"status":[1001]},"topology":null,"values":{"fillet_point_step":[0.001],"offsets":[0.5747792377896995,-0.28738961889484976],"point":{"blocks":[[58.91781798645779,82.23215696733745,1759.4838832143903]],"n":80}}}
|
||||
{"case":"gen0163","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.001],"offsets":[0.22976384063762864,-0.22976384063762864],"point":{"blocks":[[9.434962951306456,55.19956767728689,-52.78807811852818]],"n":80}}}
|
||||
{"case":"gen0163","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.001],"offsets":[0.22976384063762864,-0.22976384063762864],"point":{"blocks":[[10.720789707704943,55.541616265798496,62.68940729085022]],"n":80}}}
|
||||
{"case":"gen0164","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[0.2642518931171112,-0.1321259465585556],"point":{"blocks":[[4.149969516948741,26.208704086903612,-16.358239053020796]],"n":80}}}
|
||||
{"case":"gen0165","deferred":null,"error":null,"ints":{"status":[1001]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[4.737045353992686e-7,2.368522676996343e-7],"point":{"blocks":[[-24857875498.02827,24859818313.83341,-223721042039.0927]],"n":80}}}
|
||||
{"case":"gen0166","deferred":null,"error":null,"ints":{"status":[1001]},"topology":null,"values":{"fillet_point_step":[0.001],"offsets":[2.702330611485978e-7,-2.702330611485978e-7],"point":{"blocks":[[89463506600.02216,89463984496.77466,805170923729.6243]],"n":80}}}
|
||||
@@ -219,7 +219,7 @@
|
||||
{"case":"gen0168","deferred":null,"error":null,"ints":{"status":[1001]},"topology":null,"values":{"fillet_point_step":[0.001],"offsets":[0.06236256656271657,0.06236256656271657],"point":{"blocks":[[9.197902206312381,36.806480941533124,24.63894952216701]],"n":80}}}
|
||||
{"case":"gen0169","deferred":null,"error":null,"ints":{"status":[1001]},"topology":null,"values":{"fillet_point_step":[0.001],"offsets":[76.04306912264374,-76.04306912264374],"point":{"blocks":[[4425.043245579697,7622.315960265144,150273.22079801888]],"n":80}}}
|
||||
{"case":"gen0170","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[0.39756283642594914,-0.39756283642594914],"point":{"blocks":[[7.381401640225262,45.80037607571984,-51.355411134648094]],"n":80}}}
|
||||
{"case":"gen0171","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.001],"offsets":[0.0,0.0],"point":{"blocks":[[10.99999113096873,39.22679472432958,179.70172715799077]],"n":80}}}
|
||||
{"case":"gen0171","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.001],"offsets":[0.0,0.0],"point":{"blocks":[[13.182162664588459,39.48730185470428,338.8025711586965]],"n":80}}}
|
||||
{"case":"gen0172","deferred":null,"error":null,"ints":{"status":[1001]},"topology":null,"values":{"fillet_point_step":[0.001],"offsets":[1.6198336950826169,1.6198336950826169],"point":{"blocks":[[26.676211927653018,164.40394865910753,1700.676462754016]],"n":80}}}
|
||||
{"case":"gen0173","deferred":null,"error":null,"ints":{"status":[1001]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[0.5376285763084768,0.5376285763084768],"point":{"blocks":[[25024.96152479502,25041.65633388397,301599.58464651764]],"n":80}}}
|
||||
{"case":"gen0174","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[412.1007407754986,-206.0503703877493],"point":{"blocks":[[9594.848434170177,21991.54292662045,376251.5556356339]],"n":80}}}
|
||||
@@ -269,7 +269,7 @@
|
||||
{"case":"gen0218","deferred":null,"error":null,"ints":{"status":[1001]},"topology":null,"values":{"fillet_point_step":[0.001],"offsets":[0.292234297866413,-0.292234297866413],"point":{"blocks":[[-7.816330082841141,77.69700078693428,-348.2666303314022]],"n":80}}}
|
||||
{"case":"gen0219","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[-1.1523844362351285,1.1523844362351285],"point":{"blocks":[[20.44696232695649,35.15812098497622,542.6715434809263]],"n":80}}}
|
||||
{"case":"gen0220","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[251641.27250837215,125820.63625418607],"point":{"blocks":[[9928383.970224954,29913211.533604838,70400437.53197363]],"n":80}}}
|
||||
{"case":"gen0221","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.001],"offsets":[-0.0,-0.0],"point":{"blocks":[[3.775737832024795,28.024394813310053,-29.33682967692149]],"n":80}}}
|
||||
{"case":"gen0221","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.001],"offsets":[-0.0,-0.0],"point":{"blocks":[[1.6290541023966307,27.93836802816698,-182.4340526090423]],"n":80}}}
|
||||
{"case":"gen0222","deferred":null,"error":null,"ints":{"status":[1001]},"topology":null,"values":{"fillet_point_step":[0.001],"offsets":[1.4198678936498121,-1.4198678936498121],"point":{"blocks":[[-42.9066648626919,142.76489644611658,-1120.2077599750735]],"n":80}}}
|
||||
{"case":"gen0223","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[-1.1713949832846184,-1.1713949832846184],"point":{"blocks":[[-51.5928380467218,135.56818813521377,-2456.689711846445]],"n":80}}}
|
||||
{"case":"gen0224","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[-55.74678722526025,-55.74678722526025],"point":{"blocks":[[-3033.32130164265,6222.781383579647,-114675.7577482022]],"n":80}}}
|
||||
@@ -397,7 +397,7 @@
|
||||
{"case":"gen0346","deferred":null,"error":null,"ints":{"status":[1001]},"topology":null,"values":{"fillet_point_step":[0.001],"offsets":[0.27845437433400994,0.27845437433400994],"point":{"blocks":[[31.77212522902905,38.411249671779615,657.6375855996513]],"n":80}}}
|
||||
{"case":"gen0347","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[0.0,0.0],"point":{"blocks":[[14.42643481624605,38.898545652461245,419.41384411033823]],"n":80}}}
|
||||
{"case":"gen0348","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[0.024946036492406145,-0.024946036492406145],"point":{"blocks":[[12.269876550986305,28.630828865536774,93.46571111584737]],"n":80}}}
|
||||
{"case":"gen0349","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.001],"offsets":[-0.8084930105557193,0.8084930105557193],"point":{"blocks":[[2.381289125567308,39.99897684511395,-279.3346691456838]],"n":80}}}
|
||||
{"case":"gen0349","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.001],"offsets":[-0.8084930105557193,0.8084930105557193],"point":{"blocks":[[5.155159530096939,39.89145114600119,-205.5706761366323]],"n":80}}}
|
||||
{"case":"gen0350","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[-0.2897156024848271,-0.2897156024848271],"point":{"blocks":[[13.993282952100863,36.242777939961584,208.59173608238274]],"n":80}}}
|
||||
{"case":"gen0351","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[1.0141310825330543,-0.5070655412665271],"point":{"blocks":[[-25.16359507230496,55.04546714619831,-1290.7954915809735]],"n":80}}}
|
||||
{"case":"gen0352","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[-24149.909384874732,24149.909384874732],"point":{"blocks":[[10403206.057448285,20812931.965070143,445181663.4602736]],"n":80}}}
|
||||
@@ -425,7 +425,7 @@
|
||||
{"case":"gen0374","deferred":null,"error":null,"ints":{"status":[1001]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[-146.52461555673372,-146.52461555673372],"point":{"blocks":[[-922.2370076909385,10139.270345109735,-48637.117917711366]],"n":80}}}
|
||||
{"case":"gen0375","deferred":null,"error":null,"ints":{"extend":[0,0],"solver_status":[0],"status":[1001]},"topology":null,"values":{"fillet_point_step":[0.00001],"g1":[0.059485057875261475],"knots_in":[0.059485057875261475]}}
|
||||
{"case":"gen0376","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[-0.0,-0.0],"point":{"blocks":[[4270251.596985117,7626411.224863285,132446896.57202563]],"n":80}}}
|
||||
{"case":"gen0377","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.001],"offsets":[-498.51383362867233,498.51383362867233],"point":{"blocks":[[-6591.732743439909,13882.56835003608,-141749.00948763467]],"n":80}}}
|
||||
{"case":"gen0377","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.001],"offsets":[-498.51383362867233,498.51383362867233],"point":{"blocks":[[-6592.907111109868,13883.698749003837,-141796.8511512976]],"n":80}}}
|
||||
{"case":"gen0378","deferred":null,"error":null,"ints":{"status":[1001]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[78.67183682576177,78.67183682576177],"point":{"blocks":[[-1744.8622968961572,1764.273044129071,-122520.53088124155]],"n":80}}}
|
||||
{"case":"gen0379","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[-0.0,0.0],"point":{"blocks":[[-34916167718.30228,34916167763.03917,-279329341603.0519]],"n":80}}}
|
||||
{"case":"gen0380","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[0.7541805448141334,0.7541805448141334],"point":{"blocks":[[21.84350428542847,47.43949785555593,404.9848034985693]],"n":80}}}
|
||||
@@ -580,7 +580,7 @@
|
||||
{"case":"gen0529","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[1.1508522372415169,-1.1508522372415169],"point":{"blocks":[[1.8325186878033715,56.29865648181925,-340.3186580489356]],"n":80}}}
|
||||
{"case":"gen0530","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[0.49054485491942723,-0.49054485491942723],"point":{"blocks":[[7.236692566166987,54.34778536135473,-141.78096024015886]],"n":80}}}
|
||||
{"case":"gen0531","deferred":null,"error":null,"ints":{"extend":[1,0],"solver_status":[0],"status":[1006]},"topology":null,"values":{"fillet_point_step":[0.00001],"g1":[0.6278786548543553,0.31650252466469564,0.5],"knots_in":[0.6278786548543553,0.31650252466469564,0.5]}}
|
||||
{"case":"gen0532","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.001],"offsets":[0.00018572213244843157,-0.00018572213244843157],"point":{"blocks":[[-6100.370291092273,7799.90204176117,-55775.02201245272]],"n":80}}}
|
||||
{"case":"gen0532","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.001],"offsets":[0.00018572213244843157,-0.00018572213244843157],"point":{"blocks":[[-1120.667466564267,3006.97625017855,-11024.48464648686]],"n":80}}}
|
||||
{"case":"gen0533","deferred":null,"error":null,"ints":{"status":[1001]},"topology":null,"values":{"fillet_point_step":[0.001],"offsets":[0.0,-0.0],"point":{"blocks":[[5.606656790097681,107.03761455793614,-340.0649040651823]],"n":80}}}
|
||||
{"case":"gen0534","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[1.5790767629736289,1.5790767629736289],"point":{"blocks":[[2.418010021071451,44.65034770788736,-292.965942919616]],"n":80}}}
|
||||
{"case":"gen0535","deferred":null,"error":null,"ints":{"extend":[0,0],"solver_status":[0],"status":[1001]},"topology":null,"values":{"fillet_point_step":[0.00001],"g1":[0.9234478012231815],"knots_in":[0.9234478012231815]}}
|
||||
@@ -628,7 +628,7 @@
|
||||
{"case":"gen0577","deferred":null,"error":null,"ints":{"status":[1001]},"topology":null,"values":{"fillet_point_step":[0.001],"offsets":[1.4696712182972984,1.4696712182972984],"point":{"blocks":[[59.1250603695906,76.60130498539112,2085.92718898999]],"n":80}}}
|
||||
{"case":"gen0578","deferred":null,"error":null,"ints":{"status":[1001]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[0.590642470336763,0.590642470336763],"point":{"blocks":[[-21.46094174519216,71.6406986203895,-770.8928341376198]],"n":80}}}
|
||||
{"case":"gen0579","deferred":null,"error":null,"ints":{"status":[1001]},"topology":null,"values":{"fillet_point_step":[0.001],"offsets":[-0.042121849952598524,0.042121849952598524],"point":{"blocks":[[15.443716508467887,71.02447900078211,212.31784072107814]],"n":80}}}
|
||||
{"case":"gen0580","deferred":null,"error":null,"ints":{"status":[1001]},"topology":null,"values":{"fillet_point_step":[0.001],"offsets":[-0.0,-0.0],"point":{"blocks":[[5502762.817410249,10005453.970621428,89687798.79781598]],"n":80}}}
|
||||
{"case":"gen0580","deferred":null,"error":null,"ints":{"status":[1001]},"topology":null,"values":{"fillet_point_step":[0.001],"offsets":[-0.0,-0.0],"point":{"blocks":[[5502761.207644772,10005453.890265154,89687681.6219422]],"n":80}}}
|
||||
{"case":"gen0581","deferred":null,"error":null,"ints":{"status":[1001]},"topology":null,"values":{"fillet_point_step":[0.001],"offsets":[0.00044238756102644956,-0.00022119378051322478],"point":{"blocks":[[683.8457615253996,690.7548713571667,7601.961915957611]],"n":80}}}
|
||||
{"case":"gen0582","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[-308021.79493079573,-308021.79493079573],"point":{"blocks":[[8356020.378702474,12825252.847665178,289398215.1605343]],"n":80}}}
|
||||
{"case":"gen0583","deferred":null,"error":null,"ints":{"status":[1001]},"topology":null,"values":{"fillet_point_step":[0.001],"offsets":[1.319349557027487e-7,6.596747785137436e-8],"point":{"blocks":[[-719890.7600532864,719916.447547392,-7130325.34823085]],"n":80}}}
|
||||
@@ -642,7 +642,7 @@
|
||||
{"case":"gen0591","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[0.8033147828536416,0.8033147828536416],"point":{"blocks":[[20.885335582722558,58.32648437443985,708.3418753107223]],"n":80}}}
|
||||
{"case":"gen0592","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[-1.1624619995951506,-0.5812309997975753],"point":{"blocks":[[-11.35263106406933,74.76900844910355,-1083.98000945865]],"n":80}}}
|
||||
{"case":"gen0593","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[397.32274097089766,-397.32274097089766],"point":{"blocks":[[-3419.3921225439144,8523.359196756446,-166828.8704449418]],"n":80}}}
|
||||
{"case":"gen0594","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.001],"offsets":[2.0804487010843495e-7,-2.0804487010843495e-7],"point":{"blocks":[[520571.5702657828,520580.4131364739,5108767.744788313]],"n":80}}}
|
||||
{"case":"gen0594","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.001],"offsets":[2.0804487010843495e-7,-2.0804487010843495e-7],"point":{"blocks":[[630551.4862896164,630560.5280039047,6648048.921130304]],"n":80}}}
|
||||
{"case":"gen0595","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[-0.9428581744297906,0.9428581744297906],"point":{"blocks":[[22.87616290722158,38.14000613960552,596.9756363945345]],"n":80}}}
|
||||
{"case":"gen0596","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[-329.5127908634342,329.5127908634342],"point":{"blocks":[[-82.45538469442229,10097.70561180192,-54109.53663840378]],"n":80}}}
|
||||
{"case":"gen0597","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[1.5520477637791908e-7,1.5520477637791908e-7],"point":{"blocks":[[53.13636678092718,55.91301238753362,543.9950381533218]],"n":80}}}
|
||||
|
||||
+5
@@ -1760,6 +1760,10 @@ fn enable_fix(k: &mut Kernel, name: &str) -> Result<(), String> {
|
||||
"cover_refuses_folded_continuation" => &mut k.fixes.cover_refuses_folded_continuation,
|
||||
"cover_wraps_across_closed_seam" => &mut k.fixes.cover_wraps_across_closed_seam,
|
||||
"fillet_refuse_non_manifold_result" => &mut k.fixes.fillet_refuse_non_manifold_result,
|
||||
"fillet_torus_rail_on_axis" => &mut k.fixes.fillet_torus_rail_on_axis,
|
||||
"fillet_trace_from_tangent_point_toward_next" => {
|
||||
&mut k.fixes.fillet_trace_from_tangent_point_toward_next
|
||||
}
|
||||
"analytic_copy_keeps_parameterization" => &mut k.fixes.analytic_copy_keeps_parameterization,
|
||||
"precise_properties_body_length_scale" => &mut k.fixes.precise_properties_body_length_scale,
|
||||
"boolean_wire_edge_keeps_faceuse_shell" => {
|
||||
@@ -1772,6 +1776,7 @@ fn enable_fix(k: &mut Kernel, name: &str) -> Result<(), String> {
|
||||
"stitch_solid_nests_cavities" => &mut k.fixes.stitch_solid_nests_cavities,
|
||||
"ssi_low_angle_start_on_probation" => &mut k.fixes.ssi_low_angle_start_on_probation,
|
||||
"curve_tight_box_interval_knots" => &mut k.fixes.curve_tight_box_interval_knots,
|
||||
"fillet_tangent_point_one_sided_step" => &mut k.fixes.fillet_tangent_point_one_sided_step,
|
||||
"fillet_rails_meet_at_one_end_point" => &mut k.fixes.fillet_rails_meet_at_one_end_point,
|
||||
"fillet_nx1_tangent_side_edge" => &mut k.fixes.fillet_nx1_tangent_side_edge,
|
||||
"fillet_nx1_tangent_corner_edge" => &mut k.fixes.fillet_nx1_tangent_corner_edge,
|
||||
|
||||
+2
-4
@@ -15,9 +15,7 @@ FLOATS = ["sqrt", "cbrt", "hypot", "sin", "cos", "tan", "sin_cos", "asin", "acos
|
||||
"ln", "ln_1p", "log", "log2", "log10", "mul_add", "recip", "to_radians", "to_degrees"]
|
||||
# Per type: (file, type, path) and the methods of that type kept. A name kept on one type opens
|
||||
# nothing on another. Kept: constructors from given values, field reads and writes, and the
|
||||
# kernel's certified queries on a transform (`is_regular`, `row_lengths`, `of_placement`), and the
|
||||
# kernel's maps built from an application's numbers and their composition (`rotation`,
|
||||
# `uniform_scaling`, `reflection`, `concat`), which ops calls rather than computes.
|
||||
# kernel's certified queries on a transform (`is_regular`, `row_lengths`, `of_placement`), which ops calls rather than computes.
|
||||
TYPES = [
|
||||
("geom/vector3d.rs", "Vec3", "solidcore::Vec3",
|
||||
{"new", "from_array", "to_array", "set_uninitialized", "is_initialized", "is_undef",
|
||||
@@ -27,7 +25,7 @@ TYPES = [
|
||||
"component", "component_mut"}),
|
||||
("math.rs", "Transform", "solidcore::Transform",
|
||||
{"translation", "x_axis", "y_axis", "z_axis", "translation_part", "is_regular", "row_lengths",
|
||||
"of_placement", "rotation", "uniform_scaling", "reflection", "concat"}),
|
||||
"of_placement"}),
|
||||
("geom/transform.rs", "Transform", "solidcore::geom::transform::Transform",
|
||||
{"new", "matrix", "type_code", "type_string", "class_string", "is_kind_of", "read_from_db"}),
|
||||
("geom/matrix.rs", "Matrix", "solidcore::geom::matrix::Matrix",
|
||||
|
||||
+1
@@ -127,6 +127,7 @@ disallowed-methods = [
|
||||
{ path = "solidcore::Vec2::max_dimension", reason = "ops computes no geometry (docs/layers.md §4)" },
|
||||
{ path = "solidcore::Vec2::rotate", reason = "ops computes no geometry (docs/layers.md §4)" },
|
||||
{ path = "solidcore::Vec2::unitize", reason = "ops computes no geometry (docs/layers.md §4)" },
|
||||
{ path = "solidcore::Transform::concat", reason = "ops computes no geometry (docs/layers.md §4)" },
|
||||
{ path = "solidcore::Transform::from_axes", reason = "ops computes no geometry (docs/layers.md §4)" },
|
||||
{ path = "solidcore::Transform::transform_point", reason = "ops computes no geometry (docs/layers.md §4)" },
|
||||
{ path = "solidcore::Transform::transform_vector", reason = "ops computes no geometry (docs/layers.md §4)" },
|
||||
|
||||
-184
@@ -1,184 +0,0 @@
|
||||
//! Affine maps from an application's numbers, as the matrices [`crate::Body::transform`] and
|
||||
//! [`crate::curve::transform`] take: rows 0-2 the images of x, y and z, row 3 the translation
|
||||
//! (`p' = p·A + t`), the fourth column `(0, 0, 0, 1)`. A move, a turn about a line, a scale
|
||||
//! about a point, a mirror in a plane, and one map after another. The maps are the kernel's
|
||||
//! ([`solidcore::Transform::translation`], [`solidcore::Transform::rotation`],
|
||||
//! [`solidcore::Transform::uniform_scaling`], [`solidcore::Transform::reflection`],
|
||||
//! [`solidcore::Transform::concat`]); ops checks the numbers and passes them on.
|
||||
|
||||
use solidcore::{Transform, Vec3};
|
||||
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// An affine map in the row-vector convention of [`crate::Body::transform`].
|
||||
pub type Matrix = [[f64; 4]; 4];
|
||||
|
||||
/// Whether every number of `v` is finite.
|
||||
fn finite(v: &[f64]) -> bool {
|
||||
v.iter().all(|x| x.is_finite())
|
||||
}
|
||||
|
||||
/// `v` as the kernel's vector.
|
||||
fn vec3([x, y, z]: [f64; 3]) -> Vec3 {
|
||||
Vec3::new(x, y, z)
|
||||
}
|
||||
|
||||
/// The kernel's map, or [`Error::Argument`] for the numbers it refused.
|
||||
fn taken(map: solidcore::Result<Transform>) -> Result<Matrix> {
|
||||
map.map(|t| t.m).map_err(|_| Error::Argument)
|
||||
}
|
||||
|
||||
/// The move by `by`.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::Argument`] for a number not finite.
|
||||
pub fn translation(by: [f64; 3]) -> Result<Matrix> {
|
||||
if !finite(&by) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
Ok(Transform::translation(vec3(by)).m)
|
||||
}
|
||||
|
||||
/// The turn by `angle` radians, right-handed about `axis`, about the line through `point`
|
||||
/// along `axis` (any length but none).
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::Argument`] for a point, axis or angle not finite, or an axis of no length.
|
||||
pub fn rotation(point: [f64; 3], axis: [f64; 3], angle: f64) -> Result<Matrix> {
|
||||
taken(Transform::rotation(vec3(point), vec3(axis), angle))
|
||||
}
|
||||
|
||||
/// The scale by `factor` in every direction about `centre`, which it keeps.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::Argument`] for a centre not finite, or a factor not finite and positive (a mirror
|
||||
/// is [`reflection`]).
|
||||
pub fn scaling(centre: [f64; 3], factor: f64) -> Result<Matrix> {
|
||||
if factor.is_nan() || factor <= 0.0 {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
taken(Transform::uniform_scaling(vec3(centre), factor))
|
||||
}
|
||||
|
||||
/// The mirror in the plane through `origin` square to `normal` (any length but none).
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::Argument`] for an origin or normal not finite, or a normal of no length.
|
||||
pub fn reflection(origin: [f64; 3], normal: [f64; 3]) -> Result<Matrix> {
|
||||
taken(Transform::reflection(vec3(origin), vec3(normal)))
|
||||
}
|
||||
|
||||
/// `first`, then `second` (`p·A1 + t1`, then that `·A2 + t2`): steps stacked into one map.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::Argument`] for a map with a number not finite, given or made.
|
||||
pub fn then(first: &Matrix, second: &Matrix) -> Result<Matrix> {
|
||||
if !finite(first.as_flattened()) || !finite(second.as_flattened()) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let both = Transform { m: *first }.concat(&Transform { m: *second }).m;
|
||||
if !finite(both.as_flattened()) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
Ok(both)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::body::REGULAR_RATIO;
|
||||
use solidcore::geom::certified::Verdict;
|
||||
|
||||
/// The mirror in the plane y = 0.
|
||||
const MIRROR_Y: Matrix = [
|
||||
[1.0, 0.0, 0.0, 0.0],
|
||||
[0.0, -1.0, 0.0, 0.0],
|
||||
[0.0, 0.0, 1.0, 0.0],
|
||||
[0.0, 0.0, 0.0, 1.0],
|
||||
];
|
||||
|
||||
const IDENTITY: Matrix = [
|
||||
[1.0, 0.0, 0.0, 0.0],
|
||||
[0.0, 1.0, 0.0, 0.0],
|
||||
[0.0, 0.0, 1.0, 0.0],
|
||||
[0.0, 0.0, 0.0, 1.0],
|
||||
];
|
||||
|
||||
/// `p` under `m`, in the row-vector convention.
|
||||
fn apply(m: &Matrix, p: [f64; 3]) -> [f64; 3] {
|
||||
[0, 1, 2].map(|j| p[0] * m[0][j] + p[1] * m[1][j] + p[2] * m[2][j] + m[3][j])
|
||||
}
|
||||
|
||||
fn near(a: [f64; 3], b: [f64; 3], tol: f64) -> bool {
|
||||
a.iter().zip(b).all(|(x, y)| (x - y).abs() <= tol)
|
||||
}
|
||||
|
||||
fn regular(m: &Matrix) -> bool {
|
||||
Transform { m: *m }.is_regular(REGULAR_RATIO) == Verdict::Yes
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_mirror_in_y_0_is_the_mirror_matrix_and_twice_is_the_identity() {
|
||||
assert_eq!(reflection([0.0; 3], [0.0, 1.0, 0.0]).unwrap(), MIRROR_Y);
|
||||
// any length of an axis normal, any origin on its plane's side: exact
|
||||
for (origin, normal) in [
|
||||
([3.0, 5.0, -2.0], [0.0, 4.0, 0.0]),
|
||||
([1.0, 2.0, 7.5], [0.0, 0.0, -1.0]),
|
||||
([-6.0, 0.0, 1.0], [2.0, 0.0, 0.0]),
|
||||
] {
|
||||
let m = reflection(origin, normal).unwrap();
|
||||
assert!(regular(&m));
|
||||
assert_eq!(then(&m, &m).unwrap(), IDENTITY);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_quarter_turn_about_z_takes_x_to_y_and_a_turn_keeps_its_line() {
|
||||
let q = std::f64::consts::FRAC_PI_2;
|
||||
let m = rotation([0.0; 3], [0.0, 0.0, 1.0], q).unwrap();
|
||||
assert!(regular(&m));
|
||||
assert!(near(apply(&m, [1.0, 0.0, 0.0]), [0.0, 1.0, 0.0], 1e-15));
|
||||
assert!(near(apply(&m, [0.0, 1.0, 0.0]), [-1.0, 0.0, 0.0], 1e-15));
|
||||
// about the line through (10, 0, 0) along a slanted axis: the line's points stay
|
||||
let m = rotation([10.0, 0.0, 0.0], [1.0, 2.0, 2.0], 0.7).unwrap();
|
||||
assert!(regular(&m));
|
||||
assert!(near(apply(&m, [10.0, 0.0, 0.0]), [10.0, 0.0, 0.0], 1e-13));
|
||||
assert!(near(apply(&m, [13.0, 6.0, 6.0]), [13.0, 6.0, 6.0], 1e-12));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_scale_keeps_its_centre_and_moves_stack() {
|
||||
let c = [1.0, -2.0, 3.0];
|
||||
let m = scaling(c, 2.5).unwrap();
|
||||
assert!(regular(&m));
|
||||
assert!(near(apply(&m, c), c, 1e-14));
|
||||
assert!(near(apply(&m, [2.0, -2.0, 3.0]), [3.5, -2.0, 3.0], 1e-14));
|
||||
let (a, b) = ([1.5, -2.0, 3.0], [0.25, 4.0, -1.0]);
|
||||
let sum = [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
|
||||
let stacked = then(&translation(a).unwrap(), &translation(b).unwrap()).unwrap();
|
||||
assert_eq!(stacked, translation(sum).unwrap());
|
||||
assert!(regular(&stacked));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn numbers_not_finite_or_of_no_size_are_refused() {
|
||||
assert_eq!(translation([f64::NAN, 0.0, 0.0]), Err(Error::Argument));
|
||||
assert_eq!(rotation([0.0; 3], [0.0; 3], 1.0), Err(Error::Argument));
|
||||
assert_eq!(
|
||||
rotation([0.0; 3], [0.0, 0.0, 1.0], f64::INFINITY),
|
||||
Err(Error::Argument)
|
||||
);
|
||||
assert_eq!(scaling([0.0; 3], 0.0), Err(Error::Argument));
|
||||
assert_eq!(scaling([0.0; 3], -1.0), Err(Error::Argument));
|
||||
assert_eq!(scaling([0.0; 3], f64::NAN), Err(Error::Argument));
|
||||
assert_eq!(scaling([f64::NAN, 0.0, 0.0], 2.0), Err(Error::Argument));
|
||||
assert_eq!(reflection([0.0; 3], [0.0; 3]), Err(Error::Argument));
|
||||
assert_eq!(
|
||||
reflection([0.0, f64::NAN, 0.0], [0.0, 1.0, 0.0]),
|
||||
Err(Error::Argument)
|
||||
);
|
||||
// a stack whose numbers overflow
|
||||
let far = translation([1e308, 0.0, 0.0]).unwrap();
|
||||
assert_eq!(then(&far, &far), Err(Error::Argument));
|
||||
}
|
||||
}
|
||||
+6
-15
@@ -278,9 +278,8 @@ pub enum Error {
|
||||
}
|
||||
|
||||
/// Why the kernel refuses an offset or a draft ([`Error::OffsetRefused`]), as its
|
||||
/// `OffsetFailure` names it. More reasons get names of their own, so match it with a wildcard.
|
||||
/// `OffsetFailure` names it.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub enum OffsetRefusal {
|
||||
/// The face's offset by `distance` is not proved regular, and no wall size would help:
|
||||
/// the face bends towards that side nowhere the kernel's sizing looked, so no radius of
|
||||
@@ -337,18 +336,10 @@ pub enum OffsetRefusal {
|
||||
/// What the built body fails.
|
||||
what: solidcore::error::offset::Unsoundness,
|
||||
},
|
||||
/// A failure the kernel states only in words so far: a routine's own failure (its phrase
|
||||
/// causes `Failed`, `Missing`, `InvalidValue` and `Inconsistent`), or a reason this version
|
||||
/// of ops does not name yet. Transitional: each gets a variant of its own here once the
|
||||
/// kernel gives it a cause of its own.
|
||||
Phrase {
|
||||
/// The kernel's class of the failure.
|
||||
class: solidcore::error::ErrorKind,
|
||||
/// True when the failure is the kernel's fault, not a fact of the input (a consistency
|
||||
/// check of its own that did not hold): a bug to report, not a refusal.
|
||||
defect: bool,
|
||||
/// The kernel's statement of it.
|
||||
phrase: String,
|
||||
/// A reason this version of ops does not name yet: the kernel's own statement of it.
|
||||
Other {
|
||||
/// The kernel's statement of its reason.
|
||||
message: String,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -398,7 +389,7 @@ impl std::fmt::Display for OffsetRefusal {
|
||||
)
|
||||
}
|
||||
OffsetRefusal::Unsound { what } => write!(f, "the body built is unsound ({what:?})"),
|
||||
OffsetRefusal::Phrase { phrase, .. } => f.write_str(phrase),
|
||||
OffsetRefusal::Other { message } => write!(f, "{message}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-2
@@ -18,7 +18,6 @@
|
||||
//! the kernel's entities;
|
||||
//! - `finish`: the result contract, from what the input was (crate-internal: the families use it);
|
||||
//! - [`provenance`]: the hook an application's construction history rides on.
|
||||
//! - [`affine`]: the maps a body or a curve is moved by, from an application's numbers.
|
||||
//!
|
||||
//! The families: [`measure`] (mass properties, area, box, point class, gap samples, the picks'
|
||||
//! distances and normals), [`contact`] (whether bodies meet, their boundary distance),
|
||||
@@ -30,7 +29,6 @@
|
||||
#![deny(clippy::disallowed_methods)]
|
||||
#![cfg_attr(not(test), deny(clippy::float_arithmetic))]
|
||||
|
||||
pub mod affine;
|
||||
pub mod blend;
|
||||
pub mod body;
|
||||
pub mod boolean;
|
||||
|
||||
+46
-61
@@ -39,59 +39,49 @@ use crate::{Error, OffsetRefusal, Result};
|
||||
/// The largest offset distance or wall the operations take.
|
||||
const MAX_DISTANCE: f64 = 1e6;
|
||||
|
||||
/// What [`Error::Unsupported`] says for an offset that moves a vertex past an edge's other end.
|
||||
const EDGE_VANISHES: &str = "a vertex moved past the other end of an edge";
|
||||
|
||||
/// An offset's kernel error as the operation's: a refusal the kernel gives a reason for by that
|
||||
/// reason ([`offset_refused`]); one without by its kind ([`by_kind`]), else as
|
||||
/// [`crate::error::status`].
|
||||
/// An offset's kernel error as the operation's: walls that touch ([`Error::WallsTouch`]), a wall
|
||||
/// whose face's offset folds ([`Error::WallFolds`], with the largest wall the kernel names), a
|
||||
/// vertex moved past an edge's other end ([`Error::Unsupported`]), any other offset refusal
|
||||
/// the kernel gives a reason for as [`Error::OffsetRefused`], its face through `index` (#14783),
|
||||
/// and the rest as [`crate::error::status`].
|
||||
fn status(e: SolidError, index: &dyn Fn(FaceId) -> Option<usize>) -> Error {
|
||||
if let Some(Reason::Offset(o)) = e.failure().map(|f| &f.reason) {
|
||||
return offset_refused(*o, e.entity(), index);
|
||||
}
|
||||
by_kind(e.kind()).unwrap_or_else(|| kernel_status(e))
|
||||
}
|
||||
|
||||
/// The operation's error for an offset refusal of `kind` that has one of its own: walls that
|
||||
/// touch ([`Error::WallsTouch`]), a wall that folds ([`Error::WallFolds`], no largest wall
|
||||
/// named), a vertex moved past an edge's other end ([`Error::Unsupported`]).
|
||||
fn by_kind(kind: ErrorKind) -> Option<Error> {
|
||||
match kind {
|
||||
ErrorKind::OffsetWallsTouch => Some(Error::WallsTouch),
|
||||
ErrorKind::OffsetFolds => Some(Error::WallFolds { largest: None }),
|
||||
ErrorKind::OffsetEdgeVanishes => Some(Error::Unsupported(EDGE_VANISHES)),
|
||||
_ => None,
|
||||
match e.kind() {
|
||||
ErrorKind::OffsetWallsTouch => Error::WallsTouch,
|
||||
// (the kernel's fold kind also covers an offset not certified regular, which is no
|
||||
// fold: its own reason, #14783)
|
||||
ErrorKind::OffsetFolds => match e.failure().map(|f| &f.reason) {
|
||||
Some(Reason::Offset(OffsetFailure::Folds { largest, .. })) => Error::WallFolds {
|
||||
largest: Some(*largest),
|
||||
},
|
||||
Some(Reason::Offset(o @ OffsetFailure::NotCertified { .. })) => {
|
||||
offset_refused(*o, index)
|
||||
}
|
||||
_ => Error::WallFolds { largest: None },
|
||||
},
|
||||
ErrorKind::OffsetEdgeVanishes => {
|
||||
Error::Unsupported("a vertex moved past the other end of an edge")
|
||||
}
|
||||
_ => {
|
||||
let offset = match e.failure().map(|f| &f.reason) {
|
||||
Some(Reason::Offset(o)) => Some(*o),
|
||||
_ => None,
|
||||
};
|
||||
match offset {
|
||||
Some(o) => offset_refused(o, index),
|
||||
None => kernel_status(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The kernel's offset refusal `o` as the operation's error: walls that touch
|
||||
/// ([`Error::WallsTouch`]), a wall whose face's offset folds ([`Error::WallFolds`], with the
|
||||
/// largest wall the kernel names), a vertex moved past an edge's other end
|
||||
/// ([`Error::Unsupported`]), and every other reason as [`Error::OffsetRefused`] (#14783): why,
|
||||
/// and the face the failure is about (`entity`, the reason's own unless the kernel set another)
|
||||
/// through `index` (`None` where it names none, or one `index` does not find). A reason this
|
||||
/// version does not name yet takes the error of its class where that has one ([`by_kind`]), as
|
||||
/// it did before it had a reason; it and a phrase are otherwise said by their class and in the
|
||||
/// kernel's words ([`unnamed`], #15035).
|
||||
fn offset_refused(
|
||||
o: OffsetFailure,
|
||||
entity: Option<EntityRef>,
|
||||
index: &dyn Fn(FaceId) -> Option<usize>,
|
||||
) -> Error {
|
||||
let face = match entity {
|
||||
/// The kernel's offset refusal `o` as [`Error::OffsetRefused`]: why, and the face it names
|
||||
/// through `index` (`None` where it names none, or one `index` does not find).
|
||||
fn offset_refused(o: OffsetFailure, index: &dyn Fn(FaceId) -> Option<usize>) -> Error {
|
||||
let face = match o.entity() {
|
||||
Some(EntityRef::Face(f)) => index(f),
|
||||
_ => None,
|
||||
};
|
||||
let why = match o {
|
||||
OffsetFailure::Folds { largest, .. } => {
|
||||
return Error::WallFolds {
|
||||
largest: Some(largest),
|
||||
}
|
||||
}
|
||||
OffsetFailure::WallsTouch { .. } => return Error::WallsTouch,
|
||||
OffsetFailure::EdgeVanishes { .. } => return Error::Unsupported(EDGE_VANISHES),
|
||||
// (the kernel's fold kind also covers an offset not certified regular, which is no
|
||||
// fold: its own reason, #14783)
|
||||
OffsetFailure::NotCertified { distance, .. } => OffsetRefusal::NotCertified { distance },
|
||||
OffsetFailure::Collapses {
|
||||
distance, radius, ..
|
||||
@@ -113,23 +103,19 @@ fn offset_refused(
|
||||
OffsetFailure::ReachUncertified { .. } => OffsetRefusal::ReachUncertified,
|
||||
OffsetFailure::LoopGapUncertified { .. } => OffsetRefusal::LoopGapUncertified,
|
||||
OffsetFailure::Unsound { what } => OffsetRefusal::Unsound { what },
|
||||
// the kernel's phrase causes (`OffsetFailure::is_phrase`), and a reason the kernel adds
|
||||
// later: still classed and said, never a bare status
|
||||
other => match by_kind(other.kind()) {
|
||||
Some(e) => return e,
|
||||
None => unnamed(&other),
|
||||
},
|
||||
// a reason this version does not name (one the kernel adds later; `Folds`, `WallsTouch`
|
||||
// and `EdgeVanishes` come with their own kinds, taken above): still said, never a bare
|
||||
// status
|
||||
other => unnamed(&other),
|
||||
};
|
||||
Error::OffsetRefused { face, why }
|
||||
}
|
||||
|
||||
/// A kernel offset failure ops does not name, as [`OffsetRefusal::Phrase`]: the kernel's class
|
||||
/// of it, whether it is a defect, and its own statement of it.
|
||||
/// A kernel offset refusal ops does not name yet, as [`OffsetRefusal::Other`]: the kernel's
|
||||
/// own statement of it.
|
||||
fn unnamed(o: &OffsetFailure) -> OffsetRefusal {
|
||||
OffsetRefusal::Phrase {
|
||||
class: o.kind(),
|
||||
defect: o.is_defect(),
|
||||
phrase: o.to_string(),
|
||||
OffsetRefusal::Other {
|
||||
message: o.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -449,9 +435,8 @@ fn refused(r: DraftRefusal) -> Error {
|
||||
/// face the body does not have, a body that is not a valid solid facing out, or a pull in the
|
||||
/// neutral plane for a plane face;
|
||||
/// [`Error::Unsupported`] for a face the kernel cannot draft (what it is), or a face drafted past
|
||||
/// where its neighbours meet; [`Error::NoEffect`] for a draft that moved no face;
|
||||
/// [`Error::OffsetRefused`] for a face the kernel refuses with a reason; the kernel's other
|
||||
/// errors; those of [`finish`].
|
||||
/// where its neighbours meet; [`Error::NoEffect`] for a draft that moved no face; the kernel's
|
||||
/// other errors; those of [`finish`].
|
||||
pub fn draft(
|
||||
body: &Body,
|
||||
order: &Order,
|
||||
@@ -486,7 +471,7 @@ pub fn draft(
|
||||
match draft_surface(k, b, f, &the_draft) {
|
||||
Ok(Ok(s)) => surfaces.push(s),
|
||||
Ok(Err(r)) => return Ok(Err(refused(r))),
|
||||
Err(e) => return Ok(Err(status(e, &in_order(order)))),
|
||||
Err(e) => return Ok(Err(kernel_status(e))),
|
||||
}
|
||||
}
|
||||
// the marks live for this call only: put on the picked faces, read and removed once the
|
||||
|
||||
+6
-86
@@ -394,13 +394,11 @@ fn the_offset_refusals_are_typed() {
|
||||
|
||||
/// Each of the kernel's other offset refusals as [`Error::OffsetRefused`] (#14783): its reason,
|
||||
/// and its face by the index the call's lookup gives, `None` for a face it does not find or a
|
||||
/// refusal that names none. Never the bare status they were before, and never the kernel's text
|
||||
/// in place of a reason (#15035): a routine's own failure keeps its kind and the kernel's phrase.
|
||||
/// refusal that names none. Never the bare status they were before.
|
||||
#[test]
|
||||
fn every_other_offset_refusal_says_why_and_where() {
|
||||
use crate::OffsetRefusal as R;
|
||||
use solidcore::error::offset::{LoopCrossing, OffsetFailure as O, Unsoundness};
|
||||
use solidcore::error::ErrorKind;
|
||||
use solidcore::error::Failure;
|
||||
use solidcore::FaceId;
|
||||
let named = FaceId::from_index(3);
|
||||
@@ -503,78 +501,17 @@ fn every_other_offset_refusal_says_why_and_where() {
|
||||
what: Unsoundness::VolumeUndecided,
|
||||
},
|
||||
),
|
||||
// a routine's own failure, so far only a phrase: its class, whether it is the kernel's
|
||||
// fault, and the kernel's words, as it said them when it arrived as text
|
||||
(
|
||||
O::Failed("the face has no surface"),
|
||||
None,
|
||||
R::Phrase {
|
||||
class: ErrorKind::Failed,
|
||||
defect: false,
|
||||
phrase: "the face has no surface".to_owned(),
|
||||
},
|
||||
),
|
||||
(
|
||||
O::Missing("the face's surface"),
|
||||
None,
|
||||
R::Phrase {
|
||||
class: ErrorKind::Missing,
|
||||
defect: false,
|
||||
phrase: "the face's surface is missing".to_owned(),
|
||||
},
|
||||
),
|
||||
(
|
||||
O::InvalidValue("the face has no outer face use"),
|
||||
None,
|
||||
R::Phrase {
|
||||
class: ErrorKind::InvalidInput,
|
||||
defect: false,
|
||||
phrase: "invalid input: the face has no outer face use".to_owned(),
|
||||
},
|
||||
),
|
||||
(
|
||||
O::Inconsistent("an offset edge has no curve"),
|
||||
None,
|
||||
R::Phrase {
|
||||
class: ErrorKind::Failed,
|
||||
defect: true,
|
||||
phrase: "a consistency check failed: an offset edge has no curve".to_owned(),
|
||||
},
|
||||
),
|
||||
];
|
||||
for (o, face, why) in cases {
|
||||
let got = refused(o);
|
||||
assert_eq!(got, Error::OffsetRefused { face, why }, "{o:?}");
|
||||
assert!(!got.to_string().contains("status"), "{got}");
|
||||
// only a phrase arrives as one: every reason the kernel names is named here too
|
||||
let Error::OffsetRefused { why, .. } = &got else {
|
||||
panic!("not an offset refusal: {o:?}");
|
||||
};
|
||||
assert_eq!(matches!(why, R::Phrase { .. }), o.is_phrase(), "{o:?}");
|
||||
}
|
||||
// the face the kernel sets on a failure whose reason names none (a draft's face without a
|
||||
// surface) is the one said
|
||||
let what = "the face has no surface or no outer face use";
|
||||
let at = Failure::new("test", O::InvalidValue(what))
|
||||
.at(solidcore::EntityRef::Face(named))
|
||||
.into_error();
|
||||
assert_eq!(
|
||||
super::status(at, &index),
|
||||
Error::OffsetRefused {
|
||||
face: Some(7),
|
||||
why: R::Phrase {
|
||||
class: ErrorKind::InvalidInput,
|
||||
defect: false,
|
||||
phrase: format!("invalid input: {what}"),
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/// A reason ops does not name yet (one the kernel adds later) is said by its class and in the
|
||||
/// kernel's words, never a bare status: [`super::unnamed`], the arm every unnamed reason takes,
|
||||
/// keeps the kernel's class, whether it is a defect, and its own statement, and the error says
|
||||
/// it.
|
||||
/// A reason ops does not name yet (one the kernel adds later) is said in the kernel's words,
|
||||
/// never a bare status: [`super::unnamed`], the arm every unnamed reason takes, keeps the
|
||||
/// kernel's own statement, and the error says it.
|
||||
#[test]
|
||||
fn an_unnamed_offset_refusal_keeps_the_kernels_words() {
|
||||
use solidcore::error::offset::OffsetFailure;
|
||||
@@ -582,28 +519,11 @@ fn an_unnamed_offset_refusal_keeps_the_kernels_words() {
|
||||
let why = super::unnamed(&o);
|
||||
assert_eq!(
|
||||
why,
|
||||
crate::OffsetRefusal::Phrase {
|
||||
class: o.kind(),
|
||||
defect: o.is_defect(),
|
||||
phrase: o.to_string(),
|
||||
crate::OffsetRefusal::Other {
|
||||
message: o.to_string()
|
||||
}
|
||||
);
|
||||
assert!(!o.to_string().is_empty());
|
||||
// one whose class has an error of its own (a new reason of the walls-touch, fold or
|
||||
// vanishing-edge kind) keeps taking it, as it did before it had a reason
|
||||
assert_eq!(
|
||||
super::by_kind(solidcore::error::ErrorKind::OffsetWallsTouch),
|
||||
Some(Error::WallsTouch)
|
||||
);
|
||||
assert_eq!(
|
||||
super::by_kind(solidcore::error::ErrorKind::OffsetFolds),
|
||||
Some(Error::WallFolds { largest: None })
|
||||
);
|
||||
assert!(matches!(
|
||||
super::by_kind(solidcore::error::ErrorKind::OffsetEdgeVanishes),
|
||||
Some(Error::Unsupported(_))
|
||||
));
|
||||
assert_eq!(super::by_kind(o.kind()), None);
|
||||
let e = Error::OffsetRefused { face: Some(2), why };
|
||||
assert_eq!(
|
||||
e.to_string(),
|
||||
|
||||
+7
-152
@@ -16,12 +16,10 @@ use solidcore::nurbs::b_spline_curve::{
|
||||
BSplineCurve, BSplineCurveForm, CanonicalData, Ends, KnotType,
|
||||
};
|
||||
use solidcore::nurbs::b_spline_surface::{BSplineSurface, SurfParamType};
|
||||
use solidcore::nurbs::n_sided::{n_sided as kernel_n_sided, NSidedRefusal, MAX_SIDES, MIN_SIDES};
|
||||
use solidcore::primitive::api_curve_ops::make_curves_compatible;
|
||||
use solidcore::query::api_queries::point_distance;
|
||||
use solidcore::surface::surface::Surface;
|
||||
use solidcore::topology::brep::p3::new_brep;
|
||||
use solidcore::topology::brep::p4::stitch_faces;
|
||||
use solidcore::topology::brep::sheet::face_on;
|
||||
use solidcore::topology::Brep;
|
||||
use solidcore::SolidError;
|
||||
@@ -239,8 +237,7 @@ fn compatible(a: BSplineCurve, b: BSplineCurve) -> solidcore::Result<[BSplineCur
|
||||
/// curve, point for point at the same fraction of its range (a rational curve, an arc, stays the
|
||||
/// same curve but not the same parameter: the kernel gives the four common end weights). Three curves bound the same patch
|
||||
/// with its fourth side collapsed to the point where the third curve meets the first (a
|
||||
/// degenerate Coons patch: one side of its box is that corner). Five or more bound the
|
||||
/// certified n-sided fill ([`n_sided`]), whose certificate this drops. Each curve is taken over its
|
||||
/// degenerate Coons patch: one side of its box is that corner). Each curve is taken over its
|
||||
/// natural range (a line from its start to its end, a spline over its knots' domain).
|
||||
///
|
||||
/// The ends must meet within the model's zone tolerance: the kernel's tolerance within which
|
||||
@@ -251,14 +248,15 @@ fn compatible(a: BSplineCurve, b: BSplineCurve) -> solidcore::Result<[BSplineCur
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::Argument`] for fewer than three curves, or a curve refused (a circle, an ellipse, a
|
||||
/// traced intersection curve, numbers the kernel refuses); [`Error::BoundaryGap`] for curves
|
||||
/// that do not meet; for five or more, those of [`n_sided`]; the kernel's status.
|
||||
/// traced intersection curve, numbers the kernel refuses); [`Error::Unsupported`] for five or
|
||||
/// more curves (no n-sided patch yet); [`Error::BoundaryGap`] for curves that do not meet; the
|
||||
/// kernel's status.
|
||||
pub fn boundary(curves: &[Curve]) -> Result<Record> {
|
||||
if curves.len() < 3 {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
if curves.len() > 4 {
|
||||
return n_sided(curves).map(|(sheet, _)| sheet);
|
||||
return Err(Error::Unsupported("a patch bounded by five or more curves"));
|
||||
}
|
||||
record(run("surface::boundary", |k| {
|
||||
let mut sides = refused!(splines(k, curves)?);
|
||||
@@ -288,66 +286,6 @@ pub fn boundary(curves: &[Curve]) -> Result<Record> {
|
||||
}))
|
||||
}
|
||||
|
||||
/// The n-sided patch bounded by `curves` (three or more, meeting end to end round one loop in
|
||||
/// the order given, each turned to run on from the one before, as [`boundary`] takes them): the
|
||||
/// kernel's fill, N four-sided faces round a centre, each bounded by halves of two of the curves
|
||||
/// and two inner curves from their middles to the centre, stitched into one sheet; and the
|
||||
/// certified bound within which every curve is the sheet's boundary (`solidcore::nurbs::n_sided`).
|
||||
/// The faces meet along the inner curves within the model's zone tolerance (G0, certified);
|
||||
/// tangency across them is the fill's aim, within its angle tolerance, not certified.
|
||||
///
|
||||
/// # Errors
|
||||
/// As [`boundary`] for the curves and their joins; [`Error::Unsupported`] for a rational curve
|
||||
/// (an arc: the fill takes polynomial curves) or more than 64 curves; [`Error::PastTolerance`]
|
||||
/// for a boundary or an inner join not proved within the zone tolerance; the kernel's status.
|
||||
pub fn n_sided(curves: &[Curve]) -> Result<(Record, f64)> {
|
||||
if curves.len() < MIN_SIDES {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
if curves.len() > MAX_SIDES {
|
||||
return Err(Error::Unsupported(
|
||||
"an n-sided patch of more than 64 curves",
|
||||
));
|
||||
}
|
||||
let mut bound = None;
|
||||
let made = run("surface::n_sided", |k| {
|
||||
let mut sides = refused!(splines(k, curves)?);
|
||||
let tol = context_zone_tol_3d(k, None, SmallTopoType::NormalSize);
|
||||
refused!(looped(&mut sides, tol)?);
|
||||
match kernel_n_sided(&sides, tol)? {
|
||||
Ok(fill) => {
|
||||
bound = Some(fill.bound);
|
||||
sheet_of(k, fill.patches)
|
||||
}
|
||||
Err(NSidedRefusal::Rational) => Ok(Err(Error::Unsupported(
|
||||
"a rational curve bounding an n-sided patch",
|
||||
))),
|
||||
Err(NSidedRefusal::PastTolerance { bound }) => Ok(Err(Error::PastTolerance { bound })),
|
||||
Err(NSidedRefusal::Joins { gap }) => Ok(Err(Error::PastTolerance { bound: gap })),
|
||||
}
|
||||
});
|
||||
let sheet = record(made)?;
|
||||
Ok((sheet, bound.ok_or(Error::Exception)?))
|
||||
}
|
||||
|
||||
/// A sheet of one face on each of `patches`, over its whole natural box, stitched along the
|
||||
/// sides they share.
|
||||
fn sheet_of(k: &mut Kernel, patches: Vec<BSplineSurface>) -> solidcore::Result<Out<Brep>> {
|
||||
let mut b = new_brep(k, USE_DEFAULT, USE_DEFAULT)?;
|
||||
for s in patches {
|
||||
let s = Surface::from(s);
|
||||
let bx = s.natural_uv_domain_with_nurb(k)?;
|
||||
face_on(k, &mut b, s, &bx)?;
|
||||
}
|
||||
b.refresh_cached_topology();
|
||||
let tol = b.tolerance();
|
||||
if let Err(e) = stitch_faces(k, &mut b, tol, None, false) {
|
||||
return Ok(Err(status(e)));
|
||||
}
|
||||
b.refresh_cached_topology();
|
||||
finish(k, b, Expect::FACES)
|
||||
}
|
||||
|
||||
/// The ruled surface between `a` and `b`, one face: `S(s, t) = (1 - t) a(s) + t b(s)`, `s` the
|
||||
/// same fraction of each curve's natural range (the two first made compatible, exactly, as in
|
||||
/// [`boundary`]), `t` from 0 on `a` to 1 on `b`. So the surface's sides are the two curves
|
||||
@@ -623,7 +561,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn too_few_curves_and_circles_are_refused_and_five_fill() {
|
||||
fn too_few_or_too_many_curves_and_circles_are_refused() {
|
||||
assert_eq!(
|
||||
boundary(&[line(P[0], P[1]), line(P[1], P[0])]),
|
||||
Err(Error::Argument)
|
||||
@@ -638,8 +576,7 @@ mod tests {
|
||||
let pentagon: Vec<Curve> = (0..5)
|
||||
.map(|i| line(corners[i], corners[(i + 1) % 5]))
|
||||
.collect();
|
||||
// five curves: the n-sided fill, five faces
|
||||
assert_eq!(boundary(&pentagon).unwrap().faces.len(), 5);
|
||||
assert!(matches!(boundary(&pentagon), Err(Error::Unsupported(_))));
|
||||
let circle = Curve::Circle {
|
||||
frame: Frame {
|
||||
origin: [0.0; 3],
|
||||
@@ -653,86 +590,4 @@ mod tests {
|
||||
Err(Error::Argument)
|
||||
);
|
||||
}
|
||||
|
||||
/// The corners of a regular pentagon of radius 10 about the z axis, at height `z(x, y)`.
|
||||
fn pentagon(z: impl Fn(f64, f64) -> f64) -> Vec<[f64; 3]> {
|
||||
(0..5)
|
||||
.map(|i| {
|
||||
let a = std::f64::consts::TAU * f64::from(i) / 5.0;
|
||||
let (x, y) = (10.0 * a.cos(), 10.0 * a.sin());
|
||||
[x, y, z(x, y)]
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_regular_pentagon_of_lines_fills_flat() {
|
||||
let p = pentagon(|_, _| 0.0);
|
||||
let sides: Vec<Curve> = (0..5).map(|i| line(p[i], p[(i + 1) % 5])).collect();
|
||||
let (sheet, bound) = n_sided(&sides).unwrap();
|
||||
assert!(bound <= 1e-9, "{bound}");
|
||||
assert_eq!(sheet.faces.len(), 5);
|
||||
// five faces round a centre, stitched along the five inner curves
|
||||
assert_eq!(sheet.edges.iter().filter(|e| e.uses.len() == 2).count(), 5);
|
||||
// planar data, planar faces: every point of every face in z = 0
|
||||
for face in &sheet.faces {
|
||||
for (s, t) in [(0.0, 0.0), (0.3, 0.6), (1.0, 0.5), (0.5, 1.0)] {
|
||||
assert!(at(face, s, t)[2].abs() < 1e-9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn five_splines_on_a_saddle_bound_their_patch() {
|
||||
// each side a cubic whose control points lie on z = x y / 50 at thirds of the chord
|
||||
let z = |x: f64, y: f64| x * y / 50.0;
|
||||
let p = pentagon(z);
|
||||
let sides: Vec<Curve> = (0..5)
|
||||
.map(|i| {
|
||||
let (a, b) = (p[i], p[(i + 1) % 5]);
|
||||
let third = |t: f64| {
|
||||
let (x, y) = (a[0] + t * (b[0] - a[0]), a[1] + t * (b[1] - a[1]));
|
||||
[x, y, z(x, y)]
|
||||
};
|
||||
Curve::Spline {
|
||||
knots: vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0],
|
||||
points: vec![a, third(1.0 / 3.0), third(2.0 / 3.0), b],
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let (sheet, bound) = n_sided(&sides).unwrap();
|
||||
assert!(bound <= 1e-6, "{bound}");
|
||||
assert_eq!(sheet.faces.len(), 5);
|
||||
// the corners are vertices of the sheet: its boundary runs through them
|
||||
for corner in &p {
|
||||
assert!(
|
||||
sheet.vertices.iter().any(|v| near(v.point, *corner, 1e-6)),
|
||||
"{corner:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn five_arcs_on_a_sphere_are_refused() {
|
||||
// five arcs of the circle z = 6 on the sphere of radius 10 (radius 8), as rational
|
||||
// quadratics
|
||||
let half = std::f64::consts::PI / 5.0;
|
||||
let w = half.cos();
|
||||
let arcs: Vec<Curve> = (0..5)
|
||||
.map(|i| {
|
||||
let a = 2.0 * half * f64::from(i);
|
||||
let on = |t: f64, r: f64| [r * t.cos(), r * t.sin(), 6.0];
|
||||
let (s, m, e) = (on(a, 8.0), on(a + half, 8.0 / w), on(a + 2.0 * half, 8.0));
|
||||
Curve::RationalSpline {
|
||||
knots: vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
|
||||
homogeneous: vec![
|
||||
[s[0], s[1], s[2], 1.0],
|
||||
[m[0] * w, m[1] * w, m[2] * w, w],
|
||||
[e[0], e[1], e[2], 1.0],
|
||||
],
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
assert!(matches!(n_sided(&arcs), Err(Error::Unsupported(_))));
|
||||
}
|
||||
}
|
||||
|
||||
+1
-301
@@ -9,9 +9,7 @@ use solidcore::nurbs::crv_fit::p1::approx_knots;
|
||||
use solidcore::nurbs::geom::Rectangle;
|
||||
use solidcore::nurbs::knots::{clamped_uniform, KnotVector, Side};
|
||||
use solidcore::nurbs::math::{CPoint, NOW};
|
||||
use solidcore::nurbs::srf_basic::{
|
||||
falloff_weight as kernel_falloff, Direction, FalloffShape, Surface as NurbSurface,
|
||||
};
|
||||
use solidcore::nurbs::srf_basic::{Direction, Surface as NurbSurface};
|
||||
use solidcore::nurbs::srf_deviation::certified_distance;
|
||||
use solidcore::Vec3;
|
||||
use solidcore_record::brep::{
|
||||
@@ -81,144 +79,6 @@ pub fn move_cvs(sheet: &Brep, moves: &[((usize, usize), [f64; 3])]) -> Result<Br
|
||||
}))
|
||||
}
|
||||
|
||||
/// What [`move_net`] moves: whole rows (u indices), whole columns (v indices) or single control
|
||||
/// points `(i, j)`.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Picks {
|
||||
/// Every point of these u indices.
|
||||
Rows(Vec<usize>),
|
||||
/// Every point of these v indices.
|
||||
Columns(Vec<usize>),
|
||||
/// These points, `(u index, v index)`.
|
||||
Cvs(Vec<(usize, usize)>),
|
||||
}
|
||||
|
||||
/// The shape of a soft selection's falloff over the index distance `d` from the picks, with
|
||||
/// `s = d / (radius + 1)`.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Shape {
|
||||
/// `1 - s`.
|
||||
Linear,
|
||||
/// `1 - (3 s² - 2 s³)`: flat at the picks and at the radius's edge.
|
||||
Smooth,
|
||||
}
|
||||
|
||||
/// A soft selection: the points within `radius` index steps of the picks move too, by the
|
||||
/// weight [`Shape`] gives their distance.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Falloff {
|
||||
/// The most index steps from a pick a point moves at: 0 moves the picks alone.
|
||||
pub radius: u32,
|
||||
/// How the weight falls from 1 at the picks.
|
||||
pub shape: Shape,
|
||||
}
|
||||
|
||||
/// The weight of a point `d` index steps from the nearest pick: 1 at a pick, falling by
|
||||
/// `falloff`'s shape, 0 past its radius (and for every point off the picks without a falloff).
|
||||
#[must_use]
|
||||
pub fn falloff_weight(falloff: Option<Falloff>, d: usize) -> f64 {
|
||||
let Some(f) = falloff else {
|
||||
return if d == 0 { 1.0 } else { 0.0 };
|
||||
};
|
||||
let Ok(d) = u32::try_from(d) else {
|
||||
return 0.0;
|
||||
};
|
||||
let shape = match f.shape {
|
||||
Shape::Linear => FalloffShape::Linear,
|
||||
Shape::Smooth => FalloffShape::Smooth,
|
||||
};
|
||||
kernel_falloff(shape, f.radius, d)
|
||||
}
|
||||
|
||||
/// The weight table of a `rows` by `cols` net (row by row, as [`Net::points`]): each point's
|
||||
/// [`falloff_weight`] at its index distance from the nearest pick. A row pick's distance is
|
||||
/// along u (`|i - row|`), a column pick's along v (`|j - column|`), a point pick's the larger of
|
||||
/// the two (the square ring of the net around it).
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::Argument`] for no picks, a pick past the net, or a net of no points or more than
|
||||
/// [`REBUILD_MAX_SAMPLES`].
|
||||
pub fn net_weights(
|
||||
rows: usize,
|
||||
cols: usize,
|
||||
picks: &Picks,
|
||||
falloff: Option<Falloff>,
|
||||
) -> Result<Vec<f64>> {
|
||||
let total = rows
|
||||
.checked_mul(cols)
|
||||
.filter(|&n| n > 0 && n <= REBUILD_MAX_SAMPLES)
|
||||
.ok_or(Error::Argument)?;
|
||||
let (empty, inside) = match picks {
|
||||
Picks::Rows(p) => (p.is_empty(), p.iter().all(|&i| i < rows)),
|
||||
Picks::Columns(p) => (p.is_empty(), p.iter().all(|&j| j < cols)),
|
||||
Picks::Cvs(p) => (p.is_empty(), p.iter().all(|&(i, j)| i < rows && j < cols)),
|
||||
};
|
||||
if empty || !inside {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let nearest = |i: usize, j: usize| -> usize {
|
||||
let d = match picks {
|
||||
Picks::Rows(p) => p.iter().map(|&r| i.abs_diff(r)).min(),
|
||||
Picks::Columns(p) => p.iter().map(|&c| j.abs_diff(c)).min(),
|
||||
Picks::Cvs(p) => p
|
||||
.iter()
|
||||
.map(|&(a, b)| i.abs_diff(a).max(j.abs_diff(b)))
|
||||
.min(),
|
||||
};
|
||||
d.unwrap_or(usize::MAX)
|
||||
};
|
||||
let mut table = Vec::new();
|
||||
table
|
||||
.try_reserve_exact(total)
|
||||
.map_err(|_| Error::Argument)?;
|
||||
for i in 0..rows {
|
||||
for j in 0..cols {
|
||||
table.push(falloff_weight(falloff, nearest(i, j)));
|
||||
}
|
||||
}
|
||||
Ok(table)
|
||||
}
|
||||
|
||||
/// `sheet` with the picked control points moved by `delta`, and with a `falloff` the points
|
||||
/// around them by their weight of it ([`net_weights`]): the kernel moves each point of weight
|
||||
/// `t` by `t delta` (`Surface::displace_cpts`). The knots and degrees are kept; the result is a
|
||||
/// new one-face sheet on the moved surface, its whole box.
|
||||
///
|
||||
/// # Errors
|
||||
/// As [`control_net`] and [`net_weights`]; [`Error::Argument`] for a `delta` not finite or
|
||||
/// past the coordinate bound, or a moved point past it; the kernel's status otherwise.
|
||||
pub fn move_net(
|
||||
sheet: &Brep,
|
||||
picks: &Picks,
|
||||
delta: [f64; 3],
|
||||
falloff: Option<Falloff>,
|
||||
) -> Result<Brep> {
|
||||
if !coord_ok(&delta) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let ((u, v), points) = untrimmed(sheet)?;
|
||||
let net = control_net(sheet)?;
|
||||
let weights = net_weights(net.rows, net.cols, picks, falloff)?;
|
||||
let moves: Vec<((usize, usize), f64)> = (0..net.rows)
|
||||
.flat_map(|i| (0..net.cols).map(move |j| (i, j)))
|
||||
.zip(weights)
|
||||
.filter(|&(_, t)| t > 0.0)
|
||||
.collect();
|
||||
let mut surface = kernel_surface(u, v, points)?;
|
||||
let moved = record(run("surface_cvs::move_net", |k| {
|
||||
surface.displace_cpts(&moves, Vec3::new(delta[0], delta[1], delta[2]))?;
|
||||
sheet_of(k, BSplineSurface::from_gw_nurb(&surface))
|
||||
}))?;
|
||||
if !control_net(&moved)?
|
||||
.points
|
||||
.iter()
|
||||
.all(|p| coord_ok(p.as_slice()))
|
||||
{
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
Ok(moved)
|
||||
}
|
||||
|
||||
/// `sheet`'s surface refitted to degrees `(deg_u, deg_v)` with `(spans_u, spans_v)` uniform
|
||||
/// spans on `[0, 1]²`, and the kernel's certified bound of how far the refit is from the
|
||||
/// surface at every parameter ([`certified_distance`]), kept when within `tol`. The surface,
|
||||
@@ -514,166 +374,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_row_move_moves_exactly_that_row() {
|
||||
let s = long_sheet();
|
||||
let net = control_net(&s).unwrap();
|
||||
let delta = [0.0, 0.0, 1.0];
|
||||
let last = net.rows - 1;
|
||||
for falloff in [
|
||||
None,
|
||||
Some(Falloff {
|
||||
radius: 0,
|
||||
shape: Shape::Smooth,
|
||||
}),
|
||||
] {
|
||||
let moved = move_net(&s, &Picks::Rows(vec![last]), delta, falloff).unwrap();
|
||||
let after = control_net(&moved).unwrap();
|
||||
assert_eq!((after.rows, after.cols), (net.rows, net.cols));
|
||||
for (n, (p, q)) in net.points.iter().zip(&after.points).enumerate() {
|
||||
if n / net.cols == last {
|
||||
assert_eq!(*q, [p[0], p[1], p[2] + 1.0], "point {n}");
|
||||
} else {
|
||||
assert_eq!(p, q, "point {n}");
|
||||
}
|
||||
}
|
||||
}
|
||||
// a column the same way
|
||||
let moved = move_net(&s, &Picks::Columns(vec![0]), delta, None).unwrap();
|
||||
let after = control_net(&moved).unwrap();
|
||||
for (n, (p, q)) in net.points.iter().zip(&after.points).enumerate() {
|
||||
if n % net.cols == 0 {
|
||||
assert_eq!(*q, [p[0], p[1], p[2] + 1.0], "point {n}");
|
||||
} else {
|
||||
assert_eq!(p, q, "point {n}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn falloff_weights_are_one_at_the_pick_zero_past_the_radius_and_monotone() {
|
||||
assert_eq!(falloff_weight(None, 0), 1.0);
|
||||
assert_eq!(falloff_weight(None, 1), 0.0);
|
||||
for shape in [Shape::Linear, Shape::Smooth] {
|
||||
let f = Some(Falloff { radius: 3, shape });
|
||||
let w: Vec<f64> = (0..8).map(|d| falloff_weight(f, d)).collect();
|
||||
assert_eq!(w[0], 1.0, "{shape:?}");
|
||||
assert!(
|
||||
w[1..=3].iter().all(|&x| x > 0.0 && x < 1.0),
|
||||
"{shape:?}: {w:?}"
|
||||
);
|
||||
assert!(w[4..].iter().all(|&x| x == 0.0), "{shape:?}: {w:?}");
|
||||
assert!(
|
||||
w.windows(2).all(|p| p[1] < p[0] || p[1] == 0.0),
|
||||
"{shape:?}: {w:?}"
|
||||
);
|
||||
// the table: a point pick's distance is its square ring on the net
|
||||
let table = net_weights(9, 7, &Picks::Cvs(vec![(4, 3)]), f).unwrap();
|
||||
assert_eq!(table.len(), 63);
|
||||
for i in 0..9_usize {
|
||||
for j in 0..7_usize {
|
||||
let d = i.abs_diff(4).max(j.abs_diff(3));
|
||||
assert_eq!(table[i * 7 + j], w[d], "{shape:?} at ({i}, {j})");
|
||||
}
|
||||
}
|
||||
}
|
||||
// the smooth shape is flat at the pick: it falls less over the first step
|
||||
let (linear, smooth) = (
|
||||
falloff_weight(
|
||||
Some(Falloff {
|
||||
radius: 3,
|
||||
shape: Shape::Linear,
|
||||
}),
|
||||
1,
|
||||
),
|
||||
falloff_weight(
|
||||
Some(Falloff {
|
||||
radius: 3,
|
||||
shape: Shape::Smooth,
|
||||
}),
|
||||
1,
|
||||
),
|
||||
);
|
||||
assert!(smooth > linear, "{smooth} {linear}");
|
||||
// rows and columns by their one index
|
||||
let rows = net_weights(
|
||||
5,
|
||||
3,
|
||||
&Picks::Rows(vec![0]),
|
||||
Some(Falloff {
|
||||
radius: 1,
|
||||
shape: Shape::Linear,
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
rows,
|
||||
vec![1.0, 1.0, 1.0, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
|
||||
);
|
||||
// bad picks
|
||||
assert_eq!(
|
||||
net_weights(5, 3, &Picks::Rows(vec![]), None),
|
||||
Err(Error::Argument)
|
||||
);
|
||||
assert_eq!(
|
||||
net_weights(5, 3, &Picks::Rows(vec![5]), None),
|
||||
Err(Error::Argument)
|
||||
);
|
||||
assert_eq!(
|
||||
net_weights(5, 3, &Picks::Columns(vec![3]), None),
|
||||
Err(Error::Argument)
|
||||
);
|
||||
assert_eq!(
|
||||
net_weights(5, 3, &Picks::Cvs(vec![(0, 3)]), None),
|
||||
Err(Error::Argument)
|
||||
);
|
||||
assert_eq!(
|
||||
net_weights(0, 3, &Picks::Rows(vec![0]), None),
|
||||
Err(Error::Argument)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_soft_move_moves_each_point_by_its_weight() {
|
||||
let s = bicubic();
|
||||
let net = control_net(&s).unwrap();
|
||||
let f = Some(Falloff {
|
||||
radius: 1,
|
||||
shape: Shape::Linear,
|
||||
});
|
||||
let picks = Picks::Cvs(vec![(1, 1)]);
|
||||
let moved = move_net(&s, &picks, [0.0, 0.0, 2.0], f).unwrap();
|
||||
let after = control_net(&moved).unwrap();
|
||||
let weights = net_weights(net.rows, net.cols, &picks, f).unwrap();
|
||||
for (n, ((p, q), w)) in net
|
||||
.points
|
||||
.iter()
|
||||
.zip(&after.points)
|
||||
.zip(&weights)
|
||||
.enumerate()
|
||||
{
|
||||
assert_eq!((q[0], q[1]), (p[0], p[1]), "point {n}");
|
||||
assert!(
|
||||
(q[2] - (p[2] + 2.0 * w)).abs() <= 1e-15 * (1.0 + p[2].abs()),
|
||||
"point {n}: {p:?} to {q:?}, weight {w}"
|
||||
);
|
||||
}
|
||||
// the pick by the whole delta, its ring by half, the rest not at all
|
||||
assert_eq!(weights[5], 1.0);
|
||||
assert_eq!(weights[0], 0.5);
|
||||
assert_eq!(weights[3 * 4 + 3], 0.0);
|
||||
assert_eq!(after.points[15], net.points[15]);
|
||||
// bad moves
|
||||
assert_eq!(
|
||||
move_net(&s, &picks, [f64::NAN, 0.0, 0.0], f),
|
||||
Err(Error::Argument)
|
||||
);
|
||||
assert_eq!(
|
||||
move_net(&s, &Picks::Cvs(vec![(4, 0)]), [0.0; 3], f),
|
||||
Err(Error::Argument)
|
||||
);
|
||||
}
|
||||
|
||||
/// The Coons sheet of four bent cubic Bézier sides: a bicubic Bézier patch.
|
||||
fn bicubic() -> Brep {
|
||||
let side = |points: [[f64; 3]; 4]| Curve::Spline {
|
||||
|
||||
+16
-23
@@ -1238,15 +1238,19 @@ fn a_setback_too_long_at_a_corner_is_refused_by_the_kernel() {
|
||||
}
|
||||
|
||||
/// Cancel stops a long fillet promptly (solidcore#3247): a 2 mm round along one helical edge of
|
||||
/// a 20-turn square coil, with its tangent chain (the whole edge), runs for minutes. Cancelled
|
||||
/// from another thread a second in, the call returns `Cancelled` within 500 ms (about 5 ms
|
||||
/// measured on a desktop; the bound leaves room for shared CI runners). nextest runs each test
|
||||
/// in a process of its own, so no other test's call holds the session's kernel meanwhile.
|
||||
/// a 20-turn square coil, with its tangent chain (the whole edge), runs for minutes. Measured in
|
||||
/// the token's polls, not in time: a token cancelled from its `m`-th poll
|
||||
/// (`CancelToken::after_polls`) stops the fillet there. It returns `Cancelled`, which also shows
|
||||
/// the fillet was still running at poll `m`, and the token is polled exactly once after the poll
|
||||
/// that cancelled it: by the session as the call returns, asking whether it was cancelled
|
||||
/// (`session::run_with`, or `with_body` deciding whether to keep its edit). Any further poll
|
||||
/// would be work done after the cancel. `m` is a quarter of the 15,473 polls the fillet made in
|
||||
/// its first second, measured on SolidCraft 9a342278 (cancel-timing round 1). Should the fillet
|
||||
/// get fast enough to finish before `m`, this fails with `Ok`: then pick a longer case.
|
||||
#[test]
|
||||
fn a_cancelled_long_fillet_stops_promptly() {
|
||||
use solidcore_ops::cancel::{within, CancelToken};
|
||||
use solidcore_ops::sweep::{screw_sweep, Curve};
|
||||
use std::time::{Duration, Instant};
|
||||
all_fixes();
|
||||
let corners = [
|
||||
[19.0, 0.0, 0.0],
|
||||
@@ -1287,23 +1291,12 @@ fn a_cancelled_long_fillet_stops_promptly() {
|
||||
let chain = tangent_chain(&coil, None, &[edge], &[], 0.1)
|
||||
.unwrap()
|
||||
.members;
|
||||
let token = CancelToken::new();
|
||||
let watched = token.clone();
|
||||
let worker = std::thread::spawn(move || {
|
||||
let r = within(&watched, || {
|
||||
blend(&coil, Blend::Round, &chain, &[2.0], &[], Corners::Filled)
|
||||
});
|
||||
(r.map(|_| ()), Instant::now())
|
||||
let m: u64 = 15_473 / 4;
|
||||
let token = CancelToken::after_polls(m);
|
||||
let r = within(&token, || {
|
||||
blend(&coil, Blend::Round, &chain, &[2.0], &[], Corners::Filled)
|
||||
});
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
let cancelled = Instant::now();
|
||||
token.cancel();
|
||||
let (r, returned) = worker.join().unwrap();
|
||||
let took = returned.saturating_duration_since(cancelled);
|
||||
eprintln!("the fillet returned {took:?} after Cancel");
|
||||
assert_eq!(r, Err(Error::Cancelled));
|
||||
assert!(
|
||||
took < Duration::from_millis(500),
|
||||
"returned {took:?} after Cancel"
|
||||
);
|
||||
eprintln!("the fillet cancelled at {m}, polled {:?}", token.polls());
|
||||
assert_eq!(r.map(|_| ()), Err(Error::Cancelled));
|
||||
assert_eq!(token.polls(), Some(m + 1));
|
||||
}
|
||||
|
||||
+19
-21
@@ -315,15 +315,16 @@ fn a_dome_1e_8_off_its_circle_joins_to_its_mirror() {
|
||||
|
||||
/// Cancel stops the full check of a long coil cut (solidcore#3247): a 20-turn square coil cut
|
||||
/// from a cylinder, the case of tangency-labs/solidcraft#3962, whose check takes seconds (most
|
||||
/// of the cut's time, in the edges' curve analyses). Cancelled from another thread part way,
|
||||
/// the check returns `Cancelled` within 500 ms (a few ms on a desktop; the bound leaves room
|
||||
/// for shared CI runners). nextest runs each test in a process of its own, so no other test's
|
||||
/// call holds the session's kernel meanwhile.
|
||||
/// of the cut's time, in the edges' curve analyses). Measured in the token's polls, not in
|
||||
/// time: a counting token (`CancelToken::after_polls`) first counts the polls of the whole
|
||||
/// check, `whole`; a token cancelled from a quarter of them then stops a second check there.
|
||||
/// It returns `Cancelled`, and the token is polled exactly once after the poll that cancelled
|
||||
/// it: by the session as the call returns, asking whether it was cancelled (`run_with`, after
|
||||
/// the kernel's call). Any further poll would be work done after the cancel.
|
||||
#[test]
|
||||
fn the_check_of_a_long_coil_cut_stops_on_cancel() {
|
||||
use solidcore_ops::cancel::{within, CancelToken};
|
||||
use solidcore_ops::sweep::{screw_sweep, Curve};
|
||||
use std::time::{Duration, Instant};
|
||||
let corners = [
|
||||
[19.0, 0.0, 12.0],
|
||||
[32.0, 0.0, 12.0],
|
||||
@@ -348,23 +349,20 @@ fn the_check_of_a_long_coil_cut_stops_on_cancel() {
|
||||
.unwrap();
|
||||
let stock = solidcore_ops::make_cylinder([0.0; 3], 20.0, 304.5).unwrap();
|
||||
let cut = boolean(stock, coil, BooleanOp::Subtract).unwrap().unwrap();
|
||||
let token = CancelToken::new();
|
||||
let watched = token.clone();
|
||||
let worker = std::thread::spawn(move || {
|
||||
let r = within(&watched, || cut.check(CheckLevel::All));
|
||||
(r.map(|_| ()), Instant::now())
|
||||
});
|
||||
std::thread::sleep(Duration::from_millis(300));
|
||||
let cancelled = Instant::now();
|
||||
token.cancel();
|
||||
let (r, returned) = worker.join().unwrap();
|
||||
let took = returned.saturating_duration_since(cancelled);
|
||||
eprintln!("the check returned {took:?} after Cancel");
|
||||
assert_eq!(r, Err(Error::Cancelled));
|
||||
assert!(
|
||||
took < Duration::from_millis(500),
|
||||
"returned {took:?} after Cancel"
|
||||
let probe = CancelToken::after_polls(u64::MAX);
|
||||
within(&probe, || cut.check(CheckLevel::All)).unwrap();
|
||||
let whole = probe.polls().unwrap();
|
||||
let n = whole / 4;
|
||||
// cancelled well inside the check, not at its last poll
|
||||
assert!(n >= 1 && whole >= 2 * n, "the check polls {whole} times");
|
||||
let token = CancelToken::after_polls(n);
|
||||
let r = within(&token, || cut.check(CheckLevel::All));
|
||||
eprintln!(
|
||||
"the check polls {whole} times; cancelled at {n}, polled {:?}",
|
||||
token.polls()
|
||||
);
|
||||
assert_eq!(r.map(|_| ()), Err(Error::Cancelled));
|
||||
assert_eq!(token.polls(), Some(n + 1));
|
||||
}
|
||||
|
||||
/// #13295: a bar of L section (100 long; its step's top edge along x at y 12.527, z 19.567)
|
||||
|
||||
+32
-25
@@ -1180,11 +1180,11 @@ impl FilletSolver {
|
||||
/// `Kernel::fixes.fillet_chamfer_cone_stored_as_cone` as well (WR-572) that revolution
|
||||
/// is stored as the cone `CopyAndAddAnalytics` finds in its NURBS form, the rails made on
|
||||
/// it.
|
||||
/// Not upstream: a section centred on the arc's axis has its rail points turned into
|
||||
/// the half-planes through the edge's ends and takes the edge's start for the nearest
|
||||
/// point (WR-422); a start rail point within the solver's tolerance of the arc's axis
|
||||
/// is moved onto the axis (WR-592); the span is the turn from the start section to the
|
||||
/// end one, without `IsArc`'s start angle (WR-568).
|
||||
/// With `Kernel::fixes.fillet_torus_center_on_axis` (WR-422), a section centred on the
|
||||
/// arc's axis has its rail points turned into the half-planes through the edge's ends and
|
||||
/// takes the edge's start for the nearest point.
|
||||
/// With `Kernel::fixes.fillet_torus_rail_on_axis` (WR-592), a start rail point within
|
||||
/// the solver's tolerance of the arc's axis is moved onto the axis.
|
||||
///
|
||||
/// `SM_ERR` when the generator is not circular, the edge curve is not an arc, the
|
||||
/// radius is tiny or changes, or the nearest point on the edge is not unique; those
|
||||
@@ -1214,8 +1214,8 @@ impl FilletSolver {
|
||||
/// and `IsArc` gates. The frame is the class axis with its origin where the axis meets
|
||||
/// the plane through the edge's start square to it, its x axis the world x (y when the
|
||||
/// axis is within `acos(0.9)` of x) made square to the axis; there is no `IsArc` start
|
||||
/// angle, so the span is the turn from the start section, as in
|
||||
/// [`FilletSolver::torus_fillet_geom_parts`].
|
||||
/// angle, so the span is the turn from the start section (as with
|
||||
/// `fillet_torus_span_from_start_section`).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
@@ -1255,12 +1255,11 @@ impl FilletSolver {
|
||||
|
||||
// no work - not a circular cross-section on a circular arc (with
|
||||
// `fixes.fillet_chamfer_cone_on_arc`, or a linear one: its revolution is a cone)
|
||||
// Side effect: get circular arc RefFrame, Radius, StartAng, and EndAng (only the
|
||||
// frame is used: the span is the turn from the start section, WR-568).
|
||||
// Side effect: get circular arc RefFrame, Radius, StartAng, and EndAng.
|
||||
let section = fsg_type(s)?;
|
||||
let linear_section =
|
||||
k.fixes.fillet_chamfer_cone_on_arc && section == FilletSurfaceGeneratorType::Linear;
|
||||
let ref_frame = if let Some(class) = class {
|
||||
let (ref_frame, start_ang) = if let Some(class) = class {
|
||||
// WR-669: the circular or linear cross-section turned about the faces' common
|
||||
// axis (the class's); the frame's origin: the centre of the edge's circle, where
|
||||
// the axis meets the edge's plane
|
||||
@@ -1276,17 +1275,17 @@ impl FilletSolver {
|
||||
let x = x.unitize()?;
|
||||
let mut frame = Axis2Placement::default();
|
||||
frame.set_canonical(centre, x, z.cross(x))?;
|
||||
frame
|
||||
(frame, 0.0)
|
||||
} else {
|
||||
if section != FilletSurfaceGeneratorType::Circular && !linear_section {
|
||||
return Err(sm_err());
|
||||
}
|
||||
let Some((ref_frame, _arc_radius, _start_ang, _end_ang)) =
|
||||
let Some((ref_frame, _arc_radius, start_ang, _end_ang)) =
|
||||
curve.is_arc(k, 11, ANALYTIC_TOL)?
|
||||
else {
|
||||
return Err(sm_err());
|
||||
};
|
||||
ref_frame
|
||||
(ref_frame, start_ang)
|
||||
};
|
||||
|
||||
// select new fillet surface storage spot
|
||||
@@ -1324,13 +1323,14 @@ impl FilletSolver {
|
||||
let mut p1 = eval_offset(k, s, 1, start_uv1)?; // sP1 = rail2 point
|
||||
s.reload_surface_offsets()?;
|
||||
|
||||
// Not upstream (WR-422): when the section's centre lies on the arc's axis the rail
|
||||
// points carry no angle, so turn them into the half-planes through the edge's ends.
|
||||
// `fixes.fillet_torus_center_on_axis` (WR-422): when the section's centre lies on
|
||||
// the arc's axis the rail points carry no angle, so turn them into the half-planes
|
||||
// through the edge's ends.
|
||||
let ivl = edge::interval(b, e)?;
|
||||
let unit_axis = ref_frame.z_axis().unitize()?;
|
||||
let axis_pnt = ref_frame.origin();
|
||||
let center_tol = ops.solver().approx_tol_3d;
|
||||
let center_on_axis = {
|
||||
let center_on_axis = k.fixes.fillet_torus_center_on_axis && {
|
||||
let off = arc_center - axis_pnt;
|
||||
(off - unit_axis * off.dot(unit_axis)).length() < center_tol
|
||||
};
|
||||
@@ -1344,14 +1344,16 @@ impl FilletSolver {
|
||||
None
|
||||
};
|
||||
|
||||
// Not upstream (WR-592): a rail point within the tolerance of the axis turns round
|
||||
// it in a circle smaller than the tolerance, a point: it is the axis's point, so the
|
||||
// surface is exactly singular there.
|
||||
for rail_pnt in [&mut p0, &mut p1] {
|
||||
let off = *rail_pnt - axis_pnt;
|
||||
let on_axis = axis_pnt + unit_axis * off.dot(unit_axis);
|
||||
if on_axis.distance(*rail_pnt) < center_tol {
|
||||
*rail_pnt = on_axis;
|
||||
// `fixes.fillet_torus_rail_on_axis` (WR-592): a rail point within the tolerance of
|
||||
// the axis turns round it in a circle smaller than the tolerance, a point: it is
|
||||
// the axis's point, so the surface is exactly singular there.
|
||||
if k.fixes.fillet_torus_rail_on_axis {
|
||||
for rail_pnt in [&mut p0, &mut p1] {
|
||||
let off = *rail_pnt - axis_pnt;
|
||||
let on_axis = axis_pnt + unit_axis * off.dot(unit_axis);
|
||||
if on_axis.distance(*rail_pnt) < center_tol {
|
||||
*rail_pnt = on_axis;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1436,11 +1438,16 @@ impl FilletSolver {
|
||||
span_ang += 2.0 * PI;
|
||||
}
|
||||
|
||||
let end_ang = rad_to_deg(span_ang);
|
||||
// Not upstream (WR-568, the fork's tangency-labs/smlib@985faae): the span is the
|
||||
// turn from the start section to the end one; the start angle `IsArc` gives is
|
||||
// measured from the arc's own frame (an `SmCircle`'s analytic domain, which need
|
||||
// not start at 0), not from the start section, so it is no part of that turn
|
||||
let mut angle_span = rad_to_deg(span_ang);
|
||||
let mut angle_span = if k.fixes.fillet_torus_span_from_start_section {
|
||||
end_ang
|
||||
} else {
|
||||
end_ang - start_ang
|
||||
};
|
||||
|
||||
// Determine if we need to extend the span even when 'm_bExtendBefore' is FALSE
|
||||
if !curve.is_closed(k, &ivl, 0.0)? && !s.extend_after {
|
||||
|
||||
+37
-28
@@ -2101,8 +2101,9 @@ fn c3_make_intersector<'s>(
|
||||
|
||||
/// :2054-2090: the trace locals (tolerances, `sDir = sPVSt[1]`); a TANGENT last point
|
||||
/// reverses `st.tsect_pnts`, sets `sDir = -sPVEn[1]` and `bReverseTracing`; the local
|
||||
/// extension flags from the solver's. Not upstream (WR-603): a TANGENT first point starts
|
||||
/// the trace toward the next point.
|
||||
/// extension flags from the solver's. With
|
||||
/// `Kernel::fixes.fillet_trace_from_tangent_point_toward_next` (WR-603) a TANGENT first
|
||||
/// point starts the trace toward the next point.
|
||||
fn c3_trace_start(
|
||||
ops: &mut dyn FilletSolverOps,
|
||||
cx: &mut Cx<'_>,
|
||||
@@ -2128,20 +2129,22 @@ fn c3_trace_start(
|
||||
}
|
||||
}
|
||||
|
||||
// Not upstream (WR-603): a trace whose first through point is a tangent point starts
|
||||
// toward the next through point (the chord between their points on the first offset
|
||||
// surface): the offsets touch there and the edge's end tangent can be the line along
|
||||
// which they touch to second order.
|
||||
if let (Some(&first), Some(&next)) = (st.tsect_pnts.first(), st.tsect_pnts.get(1)) {
|
||||
let first = st.store.get(cx.fillets, first)?;
|
||||
if first.point_type == IntersectionPointType::TangentPoint {
|
||||
let uv_from = first.uv_pos(SrfIndex::First);
|
||||
let uv_to = st.store.get(cx.fillets, next)?.uv_pos(SrfIndex::First);
|
||||
let from = offset_evaluate_point(cx.k, ops, 0, uv_from)?;
|
||||
let to = offset_evaluate_point(cx.k, ops, 0, uv_to)?;
|
||||
let chord = to - from;
|
||||
if chord.length() > st.trace.approx_tol_3d {
|
||||
st.trace.dir = chord;
|
||||
// `fixes.fillet_trace_from_tangent_point_toward_next` (WR-603): a trace whose first
|
||||
// through point is a tangent point starts toward the next through point (the chord
|
||||
// between their points on the first offset surface): the offsets touch there and the
|
||||
// edge's end tangent can be the line along which they touch to second order.
|
||||
if cx.k.fixes.fillet_trace_from_tangent_point_toward_next && st.tsect_pnts.len() > 1 {
|
||||
if let (Some(&first), Some(&next)) = (st.tsect_pnts.first(), st.tsect_pnts.get(1)) {
|
||||
let first = st.store.get(cx.fillets, first)?;
|
||||
if first.point_type == IntersectionPointType::TangentPoint {
|
||||
let uv_from = first.uv_pos(SrfIndex::First);
|
||||
let uv_to = st.store.get(cx.fillets, next)?.uv_pos(SrfIndex::First);
|
||||
let from = offset_evaluate_point(cx.k, ops, 0, uv_from)?;
|
||||
let to = offset_evaluate_point(cx.k, ops, 0, uv_to)?;
|
||||
let chord = to - from;
|
||||
if chord.length() > st.trace.approx_tol_3d {
|
||||
st.trace.dir = chord;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2461,12 +2464,14 @@ mod tests_c3 {
|
||||
}
|
||||
|
||||
/// WR-603: two points, the first a tangent point, at uv `(0.2, 0.3)` and
|
||||
/// `(0.6, 0.7)` of the first surface; the trace start direction, and the chord
|
||||
/// between the points on the first offset surface.
|
||||
fn traced_from_tangent_first() -> (Vec3, Vec3) {
|
||||
/// `(0.6, 0.7)` of the first surface; the trace start direction with and without
|
||||
/// `fixes.fillet_trace_from_tangent_point_toward_next`, and the chord between the
|
||||
/// points on the first offset surface.
|
||||
fn traced_from_tangent_first(fix: bool) -> (Vec3, Vec3) {
|
||||
use crate::intersect::surface_intersector::p1::SrfIndex;
|
||||
|
||||
let k = Kernel::default();
|
||||
let mut k = Kernel::default();
|
||||
k.fixes.fillet_trace_from_tangent_point_toward_next = fix;
|
||||
let (a, b) = planes();
|
||||
let mut fs = solver(&k, &a, &b);
|
||||
let mut st = state(line(), [a, b]);
|
||||
@@ -2488,12 +2493,14 @@ mod tests_c3 {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trace_from_a_tangent_first_point_starts_toward_the_next() {
|
||||
let (dir, chord) = traced_from_tangent_first();
|
||||
fn trace_from_a_tangent_first_point_starts_toward_the_next_with_the_fix() {
|
||||
let (dir, chord) = traced_from_tangent_first(true);
|
||||
assert_eq!(dir, chord);
|
||||
assert!(chord.length() > 0.1, "{chord:?}");
|
||||
// not the filleted edge's start tangent, which upstream starts along
|
||||
assert_ne!(dir, state(line(), planes().into()).pv_start[1]);
|
||||
// upstream: the filleted edge's start tangent
|
||||
let (dir, chord) = traced_from_tangent_first(false);
|
||||
assert_eq!(dir, state(line(), planes().into()).pv_start[1]);
|
||||
assert_ne!(dir, chord);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -3817,10 +3824,12 @@ fn c6_compute_point_values(
|
||||
None
|
||||
};
|
||||
|
||||
// :3664 central difference, or one-sided at a domain end. Not upstream (WR-595): a
|
||||
// tangent point, where the intersection curve ends or branches, takes the forward
|
||||
// step alone when it found one (`bFoundForw && bFoundBack && !bTangentPoint`).
|
||||
let tangent_point = point.point_type == IntersectionPointType::TangentPoint;
|
||||
// :3664 central difference, or one-sided at a domain end. With
|
||||
// `fixes.fillet_tangent_point_one_sided_step` (WR-595) a tangent point, where the
|
||||
// intersection curve ends or branches, takes the forward step alone when it found
|
||||
// one (`bFoundForw && bFoundBack && !bTangentPoint`).
|
||||
let tangent_point = k.fixes.fillet_tangent_point_one_sided_step
|
||||
&& point.point_type == IntersectionPointType::TangentPoint;
|
||||
let (step_pt_forw, step_pt_back) = match (forw_pts, back_pts) {
|
||||
(Some((f0, f1)), Some((b0, b1))) if !tangent_point => {
|
||||
let fw = (f0 + f1) / 2.0;
|
||||
|
||||
@@ -341,7 +341,9 @@ impl FilletSolver {
|
||||
fillets.with_rail_classification(rail, |pseudo, cc| {
|
||||
context.classify(k, target, pseudo, face, cc)
|
||||
})?;
|
||||
join_intervals_on_face(fillets, rail)?;
|
||||
if k.fixes.fillet_rail_rim_on_face {
|
||||
join_intervals_on_face(fillets, rail)?;
|
||||
}
|
||||
finish_classification(
|
||||
solver,
|
||||
k,
|
||||
|
||||
-204
@@ -40,11 +40,6 @@ use super::{
|
||||
use crate::topology::{EdgeId, FaceId};
|
||||
|
||||
/// Why an offset, shell, thicken or local operation is refused.
|
||||
///
|
||||
/// A caller outside the crate matches it with a wildcard: a reason it does not name yet still
|
||||
/// has its class ([`OffsetFailure::kind`]) and says whether it is a phrase
|
||||
/// ([`OffsetFailure::is_phrase`]) or a defect ([`OffsetFailure::is_defect`]), which every reason
|
||||
/// must answer here.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub enum OffsetFailure {
|
||||
@@ -294,62 +289,6 @@ impl OffsetFailure {
|
||||
)
|
||||
}
|
||||
|
||||
/// True for a phrase cause ([`OffsetFailure::Failed`], [`OffsetFailure::Missing`],
|
||||
/// [`OffsetFailure::InvalidValue`], [`OffsetFailure::Inconsistent`]): a routine's failure
|
||||
/// not yet given a cause of its own, told apart only by its words. Its class is its status's
|
||||
/// kind; each moves to a cause of its own, as the blend's phrase causes do
|
||||
/// ([`crate::error::blend::BlendFailure`]).
|
||||
#[must_use]
|
||||
pub const fn is_phrase(&self) -> bool {
|
||||
match self {
|
||||
Self::Folds { .. }
|
||||
| Self::NotCertified { .. }
|
||||
| Self::Collapses { .. }
|
||||
| Self::WallsTouch { .. }
|
||||
| Self::LoopsCross { .. }
|
||||
| Self::EdgeVanishes { .. }
|
||||
| Self::DistanceUnresolved { .. }
|
||||
| Self::ToleranceUnresolved { .. }
|
||||
| Self::SeamOpened { .. }
|
||||
| Self::ExactCurveUncut
|
||||
| Self::FacesApart { .. }
|
||||
| Self::ReachUncertified { .. }
|
||||
| Self::LoopGapUncertified { .. }
|
||||
| Self::Unsound { .. } => false,
|
||||
Self::Failed(_) | Self::Missing(_) | Self::InvalidValue(_) | Self::Inconsistent(_) => {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// True for a failure that is the kernel's fault, not a fact of the input: a consistency
|
||||
/// check of a routine's own data that did not hold ([`OffsetFailure::Inconsistent`]).
|
||||
/// Report it. Said per cause, as its class ([`ErrorKind::Failed`]) does not say it, until
|
||||
/// offset failures move onto a class of their own.
|
||||
#[must_use]
|
||||
pub const fn is_defect(&self) -> bool {
|
||||
match self {
|
||||
Self::Inconsistent(_) => true,
|
||||
Self::Folds { .. }
|
||||
| Self::NotCertified { .. }
|
||||
| Self::Collapses { .. }
|
||||
| Self::WallsTouch { .. }
|
||||
| Self::LoopsCross { .. }
|
||||
| Self::EdgeVanishes { .. }
|
||||
| Self::DistanceUnresolved { .. }
|
||||
| Self::ToleranceUnresolved { .. }
|
||||
| Self::SeamOpened { .. }
|
||||
| Self::ExactCurveUncut
|
||||
| Self::FacesApart { .. }
|
||||
| Self::ReachUncertified { .. }
|
||||
| Self::LoopGapUncertified { .. }
|
||||
| Self::Unsound { .. }
|
||||
| Self::Failed(_)
|
||||
| Self::Missing(_)
|
||||
| Self::InvalidValue(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// The status the C++ reports for the refusal (difftest's comparison detail).
|
||||
pub(crate) const fn comparison_status(&self) -> i32 {
|
||||
match self {
|
||||
@@ -720,149 +659,6 @@ mod tests {
|
||||
assert_eq!(SolidError::sm_err("offset::op").offset_failure(), None);
|
||||
}
|
||||
|
||||
/// Every offset failure's class, and whether it is a phrase (a routine's failure not yet
|
||||
/// given a cause of its own) or a defect of the kernel, pinned. A new reason must answer
|
||||
/// all three (`kind`, `is_phrase` and `is_defect` match exhaustively) and adds its arm to
|
||||
/// `row` and its row here, so a caller outside the crate, which has to match with a
|
||||
/// wildcard, still gets its class and never only its text (#15035).
|
||||
#[test]
|
||||
fn every_offset_failure_keeps_its_class() {
|
||||
use OffsetFailure as O;
|
||||
let face = FaceId::from_index(1);
|
||||
// (the failure, its class, a phrase, a defect)
|
||||
let pinned = [
|
||||
(
|
||||
O::Folds {
|
||||
face,
|
||||
distance: 2.0,
|
||||
largest: 1.0,
|
||||
},
|
||||
ErrorKind::OffsetFolds,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
(
|
||||
O::NotCertified {
|
||||
face,
|
||||
distance: 2.0,
|
||||
},
|
||||
ErrorKind::OffsetFolds,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
(
|
||||
O::Collapses {
|
||||
face,
|
||||
distance: -5.0,
|
||||
radius: 5.0,
|
||||
},
|
||||
ErrorKind::OffsetCollapses,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
(
|
||||
O::WallsTouch {
|
||||
face: None,
|
||||
contact: WallContact::Touch,
|
||||
},
|
||||
ErrorKind::OffsetWallsTouch,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
(
|
||||
O::LoopsCross {
|
||||
face: None,
|
||||
crossing: LoopCrossing::SharesEdge,
|
||||
},
|
||||
ErrorKind::OffsetLoopsCross,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
(
|
||||
O::EdgeVanishes {
|
||||
edge: None,
|
||||
face: None,
|
||||
},
|
||||
ErrorKind::OffsetEdgeVanishes,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
(
|
||||
O::DistanceUnresolved {
|
||||
distance: 1e-9,
|
||||
resolution: 1e-6,
|
||||
},
|
||||
ErrorKind::InvalidInput,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
(
|
||||
O::ToleranceUnresolved { tolerance: 1e-13 },
|
||||
ErrorKind::InvalidInput,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
(O::SeamOpened { face }, ErrorKind::Unsupported, false, false),
|
||||
(O::ExactCurveUncut, ErrorKind::Unsupported, false, false),
|
||||
(O::FacesApart { face }, ErrorKind::Unsupported, false, false),
|
||||
(
|
||||
O::ReachUncertified { face },
|
||||
ErrorKind::Unsupported,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
(
|
||||
O::LoopGapUncertified { face: None },
|
||||
ErrorKind::Unsupported,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
(
|
||||
O::Unsound {
|
||||
what: Unsoundness::NotManifoldSolid,
|
||||
},
|
||||
ErrorKind::Unsupported,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
(O::Failed("x"), ErrorKind::Failed, true, false),
|
||||
(O::Missing("x"), ErrorKind::Missing, true, false),
|
||||
(O::InvalidValue("x"), ErrorKind::InvalidInput, true, false),
|
||||
// a consistency check of the kernel's own data that did not hold: its fault
|
||||
(O::Inconsistent("x"), ErrorKind::Failed, true, true),
|
||||
];
|
||||
let row = |o: &O| -> usize {
|
||||
match o {
|
||||
O::Folds { .. } => 0,
|
||||
O::NotCertified { .. } => 1,
|
||||
O::Collapses { .. } => 2,
|
||||
O::WallsTouch { .. } => 3,
|
||||
O::LoopsCross { .. } => 4,
|
||||
O::EdgeVanishes { .. } => 5,
|
||||
O::DistanceUnresolved { .. } => 6,
|
||||
O::ToleranceUnresolved { .. } => 7,
|
||||
O::SeamOpened { .. } => 8,
|
||||
O::ExactCurveUncut => 9,
|
||||
O::FacesApart { .. } => 10,
|
||||
O::ReachUncertified { .. } => 11,
|
||||
O::LoopGapUncertified { .. } => 12,
|
||||
O::Unsound { .. } => 13,
|
||||
O::Failed(_) => 14,
|
||||
O::Missing(_) => 15,
|
||||
O::InvalidValue(_) => 16,
|
||||
O::Inconsistent(_) => 17,
|
||||
}
|
||||
};
|
||||
for (i, (o, class, phrase, defect)) in pinned.iter().enumerate() {
|
||||
assert_eq!(row(o), i, "{o:?} is listed once, in order");
|
||||
assert_eq!(o.kind(), *class, "{o:?}");
|
||||
assert_eq!(o.is_phrase(), *phrase, "{o:?}");
|
||||
assert_eq!(o.is_defect(), *defect, "{o:?}");
|
||||
// a phrase is what replaced a bare status, and only a phrase
|
||||
assert_eq!(o.replaces_upstream_status(), *phrase, "{o:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The offset routines describe their failures: their library code builds no bare
|
||||
/// `Upstream` status, except the files listed (with the bare sites each may still
|
||||
/// hold) whose conversion waits on other open work.
|
||||
|
||||
+18
-14
@@ -2082,21 +2082,24 @@ pub trait GlobalSolve<'t, O: SolutionObject + 't> {
|
||||
// for every current solution
|
||||
let num_sols = self.solver().solutions.len();
|
||||
for ii in 0..num_sols {
|
||||
// read in place: a copy per stored solution per insertion cost a memcpy each
|
||||
let test = self.solver().solutions.at(ii)?;
|
||||
let test = self.solver().solutions.get_at(ii)?;
|
||||
|
||||
// don't add duplicate solutions (SolValues within ZeroSqrt and ParamValues within ScaledZero)
|
||||
let (pair, preferred) = self.identical_solutions(solution, test)?;
|
||||
let (pair, preferred) = self.identical_solutions(solution, &test)?;
|
||||
if pair == SolutionPairType::Identical {
|
||||
// save the preferred solution if possible, else better solution - improve tolerances
|
||||
// (keeping the stored one, `Second`, is writing it back unchanged: nothing to do)
|
||||
let keep = match preferred {
|
||||
Some(PreferredSolution::First) => true,
|
||||
Some(PreferredSolution::Second) => false,
|
||||
None => solution.start.solution_value.abs() < test.start.solution_value.abs(),
|
||||
Some(PreferredSolution::First) => Some(solution),
|
||||
Some(PreferredSolution::Second) => Some(&test),
|
||||
None if solution.start.solution_value.abs()
|
||||
< test.start.solution_value.abs() =>
|
||||
{
|
||||
Some(solution)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
if keep {
|
||||
self.solver_mut().solutions.set_at(ii, solution)?;
|
||||
if let Some(keep) = keep {
|
||||
self.solver_mut().solutions.set_at(ii, keep)?;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
@@ -2250,11 +2253,10 @@ pub trait GlobalSolve<'t, O: SolutionObject + 't> {
|
||||
// points contained in intervals get absorbed.
|
||||
// disjoint intervals remain disjoint.
|
||||
// point solutions remain distinct.
|
||||
// in place: combine_min_max changes the pair only on its split path,
|
||||
// after every fallible step, so an error leaves both as they were, as
|
||||
// the copies did
|
||||
let (min, max) = s.solutions.pair_mut(k, j)?;
|
||||
let combined = combine_min_max(op, min, max, combine_contiguous_ranges)?;
|
||||
let mut min = s.solutions.get_at(k)?;
|
||||
let mut max = s.solutions.get_at(j)?;
|
||||
let combined =
|
||||
combine_min_max(op, &mut min, &mut max, combine_contiguous_ranges)?;
|
||||
if let Some(combined) = combined {
|
||||
s.solutions.set_at(k, &combined)?;
|
||||
s.solutions.remove_at(j, 1);
|
||||
@@ -2262,6 +2264,8 @@ pub trait GlobalSolve<'t, O: SolutionObject + 't> {
|
||||
break;
|
||||
}
|
||||
// (the split of a range around a tight point is made in place)
|
||||
s.solutions.set_at(k, &min)?;
|
||||
s.solutions.set_at(j, &max)?;
|
||||
j += 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -1538,37 +1538,6 @@ impl<O: SolutionObject> SolutionArray<O> {
|
||||
.ok_or_else(|| bad_index("geom::solution_array::SolutionArray::get_at"))
|
||||
}
|
||||
|
||||
/// Solution `index`, borrowed (`GetAt` without the copy).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`SolidError::InvalidInput`] for an index past the size.
|
||||
pub fn at(&self, index: usize) -> Result<&Solution<O>> {
|
||||
self.data
|
||||
.get(index)
|
||||
.ok_or_else(|| bad_index("geom::solution_array::SolutionArray::at"))
|
||||
}
|
||||
|
||||
/// Solutions `i` and `j` (`i < j`), both borrowed mutably.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`SolidError::InvalidInput`] unless `i < j` and `j` is within the size.
|
||||
pub fn pair_mut(&mut self, i: usize, j: usize) -> Result<(&mut Solution<O>, &mut Solution<O>)> {
|
||||
const OP: &str = "geom::solution_array::SolutionArray::pair_mut";
|
||||
if i >= j {
|
||||
return Err(bad_index(OP));
|
||||
}
|
||||
let (lo, hi) = self
|
||||
.data
|
||||
.split_at_mut_checked(j)
|
||||
.ok_or_else(|| bad_index(OP))?;
|
||||
match (lo.get_mut(i), hi.first_mut()) {
|
||||
(Some(a), Some(b)) => Ok((a, b)),
|
||||
_ => Err(bad_index(OP)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Replaces solution `index` (`SetAt`).
|
||||
///
|
||||
/// # Errors
|
||||
@@ -2263,28 +2232,4 @@ mod tests {
|
||||
assert_eq!(r.solution_type, SolutionType::Unknown);
|
||||
assert_eq!(r.start.parameters[0], 1.0);
|
||||
}
|
||||
|
||||
/// `at` borrows what `get_at` copies; `pair_mut` hands out two distinct solutions and refuses
|
||||
/// a pair out of order, the same index twice or an index past the size.
|
||||
#[test]
|
||||
fn borrowed_access() {
|
||||
let mut a: SolutionArray<Obj> = SolutionArray::new();
|
||||
a.add(&single(1.0, &[1.0]));
|
||||
a.add(&single(2.0, &[2.0]));
|
||||
a.add(&single(3.0, &[3.0]));
|
||||
assert_eq!(*a.at(1).unwrap(), a.get_at(1).unwrap());
|
||||
assert!(matches!(a.at(3), Err(SolidError::InvalidInput { .. })));
|
||||
let (x, y) = a.pair_mut(0, 2).unwrap();
|
||||
assert_eq!((x.start.solution_value, y.start.solution_value), (1.0, 3.0));
|
||||
x.start.solution_value = 10.0;
|
||||
y.start.solution_value = 30.0;
|
||||
assert_eq!(a.get_at(0).unwrap().start.solution_value, 10.0);
|
||||
assert_eq!(a.get_at(2).unwrap().start.solution_value, 30.0);
|
||||
for (i, j) in [(1, 1), (2, 1), (0, 3)] {
|
||||
assert!(matches!(
|
||||
a.pair_mut(i, j),
|
||||
Err(SolidError::InvalidInput { .. })
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+146
-6
@@ -36,7 +36,7 @@
|
||||
//! already holds.
|
||||
|
||||
use core::cell::{Cell, RefCell};
|
||||
use core::sync::atomic::{AtomicBool, Ordering};
|
||||
use core::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::error::SolidError;
|
||||
@@ -459,6 +459,8 @@ pub struct UpstreamFixes {
|
||||
pub curve_loop_normal_from_area: bool,
|
||||
/// `CalcRailIntEdgeuse`'s retry rail search holds a variable round's radius only where its first search does (at a corner calculated as constant radius), so the rail misses the arc it would meet only at that radius and the corner cuts the round off against the body; needs `fillet_nx1_end_rib_global_merge` there (WR-300, #1023). `RailRailIntersect` holds the radii at their end values only there too (its callers `CalcRailIntRail` and `CalcRailRailInterpolation`'s cubic rail interpolant): held at any other corner, two variable rounds meeting at a box's corner met where the laws are smaller and the trace from there failed with 1001 (the fork's tangency-labs/smlib@465b62b, the rule of 1d06159).
|
||||
pub fillet_retry_radius_as_first: bool,
|
||||
/// `TestRollOver` joins a rail's neighbouring intervals on its face or the face's boundary before the rollover, so a rail along its face's rim does not fail "Unable to Process Rollover" (WR-301, #485).
|
||||
pub fillet_rail_rim_on_face: bool,
|
||||
/// The fillet executive refuses a blend whose rails leave the faces they roll on (`RailsFitTheirFaces`, no room) (WR-302, #1024/#841).
|
||||
pub fillet_refuse_rails_leaving_faces: bool,
|
||||
/// `create_fillet_brep` refuses blends that take the whole of a face they roll on (`FacesOutlastTheirBlends`, no room) (WR-320, #879).
|
||||
@@ -475,6 +477,8 @@ pub struct UpstreamFixes {
|
||||
pub fillet_corner_ball_center_from_axes: bool,
|
||||
/// `SmFilletCorner::MakeVariableRadiusFillet` refuses to turn a four-sided corner into a variable-radius fillet when the new fillet's rail along the opposite fillet leaves that fillet's surface (more than ten times the corner's tolerance from its natural domain): the rail replaces the opposite edge, which bounds the opposite fillet's face, so that face would end off its own edge. The corner is then the patch on its exact edges. At a drafted pocket's top corners the rail ran 0.42 off the corner round at R3, 10 degrees (WR-645, #968).
|
||||
pub fillet_corner_variable_radius_rail_on_opposite_fillet: bool,
|
||||
/// `CalcTorusFilletGeom` takes the round's span as the turn from its start section to its end one, without taking `IsArc`'s start angle off it: an `SmCircle`'s start angle is measured from the circle's own frame (a boolean's circle framed on a cylinder's seam starts at 60 or 115 degrees), so the torus came out short by it and the end rib found no fillet surface at the far end (1001 at an Nx1 corner) (WR-568; the fork's tangency-labs/smlib@985faae).
|
||||
pub fillet_torus_span_from_start_section: bool,
|
||||
/// `CalcTorusFilletGeom` revolves a linear section into a cone (WR-421, #420).
|
||||
pub fillet_chamfer_cone_on_arc: bool,
|
||||
/// With `fillet_chamfer_cone_on_arc`, `CalcTorusFilletGeom` stores a linear section's revolution as the `SmCone` it is: the revolution's NURBS form is copied (`SmBSplineSurface::Copy`) and passed through `CopyAndAddAnalytics`, and when that gives a cone it is the fillet surface and the rails are made on it (as a straight edge's round is stored as its cylinder); anything else stays the surface of revolution. Without it a chamfer round a hole's rim is a surface of revolution, which reads back as a rational B-spline, not the cone it is (WR-572, #1503; the fork's tangency-labs/smlib@b5743ec).
|
||||
@@ -483,7 +487,11 @@ pub struct UpstreamFixes {
|
||||
pub corner_blend_coons_patch_tangent_to_sides: bool,
|
||||
/// `SmFilletCorner::CreateCornerPatch` marks the patch sides that lie on a face of the body (a fillet edge with an original face) and `sm_CreateCoonsSrf` gives a corner between such a side and a blend's side the blend side's twist instead of the mean of the two: a bicubic Coons patch meets a side's cross-boundary derivative only where the corner twists are the other sides' own, so the mean tilted the patch off the face's tangent plane beside that side and it crossed the face (0.037 out of a bar's side face where the bar's top-edge round meets the concave round at a cylinder it runs tangent into, r 2). Corners between two face sides or two blend sides keep the mean (WR-613, #1836; the fork's tangency-labs/smlib@7fd9fb0 (commit not in fork history at 26cc451), from the issue's diff).
|
||||
pub corner_patch_face_side_twist: bool,
|
||||
/// The fillet solver decides whether a constant round is exact by one classification of its spine (the curve the ball's centre runs on) from its faces' kinds and placement, read as the natural quadrics they lie on (plane, sphere, circular cylinder or cone, an offset of one): a line (two planes; a plane or cylinder parallel to a cylinder's axis) is built as the cylinder, a circle about a common axis (coaxial quadrics, a plane square to the axis, a sphere's centre on it) as the section turned about that axis, a point (two cylinders as wide as the ball, axes meeting) as the ball's piece; every other round is traced. This replaces upstream's try-and-fall-through chain (`CanMakeAnalytic`, planes and cones only, then cylinder, torus and with `fillet_corner_ball_patch` the sphere, each deciding from the edge's shape) at `CalcFilletGeom` and at `ReCalcFilletGeom` (where a line round is rebuilt as its cylinder, not the cone), and a builder's failure is then an error, not a trace. So a round on a sphere is exact, and a circular round's frame comes from the faces' axis, not the edge's `IsArc` (WR-669; the fork's tangency-labs/smlib@b5f2867 and 7ac037e). The torus and ball builders' own flags still apply inside them: the fork's behaviour is this with `fillet_chamfer_cone_on_arc`, `fillet_chamfer_cone_stored_as_cone` and `fillet_corner_ball_center_from_axes` on (a chamfer round a circle is traced without `fillet_chamfer_cone_on_arc`). A conic spine (a plane with a cylinder or cone at any other angle; two cylinders or cones touching a common ball the same way round) is built as the exact rational surface of the ball's arcs, checked against the ball before it is kept (the fork's tangency-labs/smlib@0061c41 and 1b5f1e7); the fork's nodal-quartic rounds are classified but still traced here. A line round's cylinder is built on its exact spine, the line its offsets meet in, its rails on the faces' feet of it, and keeps that frame instead of the stations' centres and a frame refitted from its net (#2405, solidcore-native).
|
||||
/// A torus blend whose section centre is on the arc's axis turns rail points into the edge ends' half-planes (WR-422, #417).
|
||||
pub fillet_torus_center_on_axis: bool,
|
||||
/// `CalcTorusFilletGeom` moves a section's rail point that lies within the solver's tolerance (`GetThisApproxTol3d`) of the edge arc's axis onto the axis: it turns about the axis in a circle smaller than the tolerance, so the rail collapses to the axis's point and the surface of revolution is exactly singular there, where upstream left the point off the axis by the model's residues (1.2e-11 after a sketch's 1e-10) and the surface nearly singular, which `smlib_brep_check` refused (`SmSurface` #5, Bad Surf Pole Approximation; a round along an earlier round's end arc of the ball's own radius) (WR-592; the fork's tangency-labs/smlib@994f129). Whether the result then passes the check depends on the input's residues: on the bar `tests/wr592_torus_rail_on_axis.rs` rebuilds, the pinned SMLib with this hunk alone and the port both leave `SmSurfOfRevolution` #7 failing by roundoff, where the fork's own input passes (`plan/findings/blend.fillet_solver_p2--wr592-revolution-polar-start-angle.md`).
|
||||
pub fillet_torus_rail_on_axis: bool,
|
||||
/// The fillet solver decides whether a constant round is exact by one classification of its spine (the curve the ball's centre runs on) from its faces' kinds and placement, read as the natural quadrics they lie on (plane, sphere, circular cylinder or cone, an offset of one): a line (two planes; a plane or cylinder parallel to a cylinder's axis) is built as the cylinder, a circle about a common axis (coaxial quadrics, a plane square to the axis, a sphere's centre on it) as the section turned about that axis, a point (two cylinders as wide as the ball, axes meeting) as the ball's piece; every other round is traced. This replaces upstream's try-and-fall-through chain (`CanMakeAnalytic`, planes and cones only, then cylinder, torus and with `fillet_corner_ball_patch` the sphere, each deciding from the edge's shape) at `CalcFilletGeom` and at `ReCalcFilletGeom` (where a line round is rebuilt as its cylinder, not the cone), and a builder's failure is then an error, not a trace. So a round on a sphere is exact, and a circular round's frame comes from the faces' axis, not the edge's `IsArc` (WR-669; the fork's tangency-labs/smlib@b5f2867 and 7ac037e). The torus and ball builders' own flags still apply inside them: the fork's behaviour is this with `fillet_torus_span_from_start_section`, `fillet_chamfer_cone_on_arc`, `fillet_chamfer_cone_stored_as_cone`, `fillet_torus_center_on_axis`, `fillet_torus_rail_on_axis` and `fillet_corner_ball_center_from_axes` on (a chamfer round a circle is traced without `fillet_chamfer_cone_on_arc`). A conic spine (a plane with a cylinder or cone at any other angle; two cylinders or cones touching a common ball the same way round) is built as the exact rational surface of the ball's arcs, checked against the ball before it is kept (the fork's tangency-labs/smlib@0061c41 and 1b5f1e7); the fork's nodal-quartic rounds are classified but still traced here. A line round's cylinder is built on its exact spine, the line its offsets meet in, its rails on the faces' feet of it, and keeps that frame instead of the stations' centres and a frame refitted from its net (#2405, solidcore-native).
|
||||
pub fillet_exact_round_classes: bool,
|
||||
/// `SmFilletExecutive::CreateFilletBrep` stitches the fillet body keeping a fillet's own edge where an edge of corner-patch faces only meets an edge of other fillet faces only (`SmFilletPatchStitchCallback`): an N-sided corner patch is bounded by its surfaces' isoparametric curves, the approximations it was fitted to, and the default choice between the two near-equal distances left one half of a set-back section the exact arc and the other the polynomial approximation, off the round (WR-546, #1377; the fork's tangency-labs/smlib@1072d1b).
|
||||
pub fillet_patch_keeps_fillet_edges: bool,
|
||||
@@ -1057,8 +1065,12 @@ pub struct UpstreamFixes {
|
||||
pub imprint_reuses_coincident_face_edge: bool,
|
||||
/// `SmFilletSolver::ComputePointValues` chooses its finite-difference step for each point (1e-3 at a tangent point, 1e-5 otherwise), not once per process as the function-local static `sdStaticStep`, so a blend does not depend on the blends before it ([`Kernel::fillet_point_step`] is then neither read nor set, and an existing latch is kept as it is) (ND-3, #1173; the fork's tangency-labs/smlib@9e17f64 (was 0a05860)).
|
||||
pub fillet_point_step_per_point: bool,
|
||||
/// `SmFilletSolver::CalcFilletGeom` starts a trace whose first through point (after the reversal for a tangent last point) is a tangent point toward the next through point, the chord between their points on the first offset surface (when longer than the tolerance), not along the filleted edge's end tangent: the offsets touch at a tangent point, and that tangent can be the line along which they touch to second order, so the first step lay on both offsets within tolerance, the trace missed the next point and ran round a cylinder (a bar's top edge rounded into a cylinder tangent to the bar's side, refused as fillet error 8) (WR-603; the fork's tangency-labs/smlib@68fb916).
|
||||
pub fillet_trace_from_tangent_point_toward_next: bool,
|
||||
/// `SmBSplineSurface::CopyAndAddAnalytics` keeps an exact copy where the recognised analytic surface does not give the same points at the same parameters (7 x 7 grid), so a face split by `MakeEdgeInFaceWithVU` keeps trims that still lie on its edges (WR-450, #387; the fork's tangency-labs/smlib@7af1987 (was 276b509)).
|
||||
pub analytic_copy_keeps_parameterization: bool,
|
||||
/// `SmFilletSolver::ComputePointValues` sets a tangent point's curve tangent from the forward finite-difference step alone (along the trace, the previous point's tangent), and from the backward step only when the forward one finds nothing, not from their central difference: the intersection curve ends or branches at a tangent point, and where the offset surfaces touch only to within the model's accuracy (a sketch's 1e-10 residues) the backward step lands on the near-tangent crossing beyond it, 18 degrees off the curve, so every step after the point failed the angle test and the trace crept on in steps down to 8e-10 (repeated rail control points, an internal pole) (WR-595; the fork's tangency-labs/smlib@f8fd83e). Use with `fillet_trace_from_tangent_point_toward_next`, as the fork has both: alone, the forward step from a trace's tangent first point runs along the edge's end tangent, and `Fillet/nurb_cone.smb` edge 8 at r 0.5, which upstream builds, fails.
|
||||
pub fillet_tangent_point_one_sided_step: bool,
|
||||
/// `SmFace::ComputePreciseProperties` scales the volume and moment error bounds by the body size (ten times the root of the estimated area, or the origin's distance when larger), not 1.0.1's tenth of it, so the integration converges at tight accuracies instead of returning `SM_ERR` (WR-472; the SolidCraft fork's tangency-labs/smlib@073d4740).
|
||||
pub precise_properties_body_length_scale: bool,
|
||||
/// `SmVertex::IsTopologicalVertex` ([`crate::topology::vertex::is_topological_vertex`])
|
||||
@@ -1377,11 +1389,39 @@ const fn cancelled_error(operation: &'static str) -> SolidError {
|
||||
/// on any clone signals every [`Kernel`] configured with [`Kernel::set_cancel_token`], so
|
||||
/// one token can also cancel a group of jobs. [`Self::either`] makes a token that also fires
|
||||
/// with another one, for work started under two (a dialog's job of a design that may close).
|
||||
///
|
||||
/// Not upstream: a token made by [`Self::after_polls`] counts the times it is polled
|
||||
/// ([`Self::is_cancelled`], [`Self::check`], and so [`Kernel::is_cancelled`] and
|
||||
/// [`Kernel::check_cancelled`]) and turns cancelled at a given poll. That stops an operation
|
||||
/// at a point fixed by its own work, not by a clock: for deterministic tests that an
|
||||
/// operation stops where it is cancelled, and for an application's progress or abort UI
|
||||
/// (SolidCraft's), which can read how far an operation got ([`Self::polls`]) or stop it after
|
||||
/// a budget of work. A token made by [`Self::new`] counts nothing and pays nothing for it.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct CancelToken {
|
||||
flag: Arc<AtomicBool>,
|
||||
/// Not upstream: the token this one also fires with ([`Self::either`]).
|
||||
also: Option<Arc<CancelToken>>,
|
||||
/// Not upstream: the polls counted, and the one it turns cancelled at
|
||||
/// ([`Self::after_polls`]); `None` for a token that counts nothing.
|
||||
polls: Option<Arc<PollCount>>,
|
||||
}
|
||||
|
||||
/// The polls a [`CancelToken::after_polls`] token counts.
|
||||
#[derive(Debug)]
|
||||
struct PollCount {
|
||||
/// The poll the token is cancelled from (1: the first).
|
||||
from: u64,
|
||||
/// The polls made so far.
|
||||
made: AtomicU64,
|
||||
}
|
||||
|
||||
impl PollCount {
|
||||
/// Counts one poll: whether the token is cancelled from it on.
|
||||
fn poll(&self) -> bool {
|
||||
let made = self.made.fetch_add(1, Ordering::Relaxed).saturating_add(1);
|
||||
made >= self.from
|
||||
}
|
||||
}
|
||||
|
||||
impl CancelToken {
|
||||
@@ -1396,9 +1436,39 @@ impl CancelToken {
|
||||
self.flag.store(true, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Clears the cancellation signal (`false`) so the token can be reused.
|
||||
/// Clears the cancellation signal (`false`) so the token can be reused. A token made by
|
||||
/// [`Self::after_polls`] starts counting its polls again from 0.
|
||||
pub fn reset(&self) {
|
||||
self.flag.store(false, Ordering::Relaxed);
|
||||
if let Some(p) = &self.polls {
|
||||
p.made.store(0, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
/// Not upstream: an uncancelled token that counts its polls and is cancelled from its
|
||||
/// `n`-th on (`n` = 0 or 1: from the first). [`Self::cancel`] cancels it at once as any
|
||||
/// token's; `after_polls(u64::MAX)` only counts. Clones share the count.
|
||||
#[must_use]
|
||||
pub fn after_polls(n: u64) -> Self {
|
||||
Self {
|
||||
polls: Some(Arc::new(PollCount {
|
||||
from: n,
|
||||
made: AtomicU64::new(0),
|
||||
})),
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Not upstream: how many times this token has been polled since it was made or last
|
||||
/// [`Self::reset`], for a token made by [`Self::after_polls`]; `None` for one made by
|
||||
/// [`Self::new`], which counts nothing. A token made by [`Self::either`] counts as the
|
||||
/// token it was made from (`self`): a poll of it is a poll of `self` and of the token it
|
||||
/// was joined with, and is counted by each of them that counts.
|
||||
#[must_use]
|
||||
pub fn polls(&self) -> Option<u64> {
|
||||
self.polls
|
||||
.as_deref()
|
||||
.map(|p| p.made.load(Ordering::Relaxed))
|
||||
}
|
||||
|
||||
/// Not upstream: a token that is cancelled when `self` or `other` is. Cancelling it
|
||||
@@ -1412,16 +1482,22 @@ impl CancelToken {
|
||||
Some(more) => more.either(other),
|
||||
None => other.clone(),
|
||||
})),
|
||||
// a poll of the joined token is a poll of self: counted, and cancelling, as self's
|
||||
polls: self.polls.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether [`Self::cancel`] has been called since construction or the last
|
||||
/// [`Self::reset`] (on this token, or on a token it was joined with by [`Self::either`]).
|
||||
/// [`Self::reset`] (on this token, or on a token it was joined with by [`Self::either`]),
|
||||
/// or, for a token made by [`Self::after_polls`], this is its `n`-th poll or a later one.
|
||||
/// Every call is one poll of each counting token in the chain: none is skipped because
|
||||
/// an earlier one is already cancelled.
|
||||
#[must_use]
|
||||
#[inline]
|
||||
pub fn is_cancelled(&self) -> bool {
|
||||
self.flag.load(Ordering::Relaxed)
|
||||
|| self.also.as_deref().is_some_and(CancelToken::is_cancelled)
|
||||
let counted = self.polls.as_deref().is_some_and(PollCount::poll);
|
||||
let also = self.also.as_deref().is_some_and(CancelToken::is_cancelled);
|
||||
counted || also || self.flag.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Returns `Err(SolidError::Cancelled { operation })` when [`Self::is_cancelled`] is
|
||||
@@ -1699,6 +1775,70 @@ mod tests {
|
||||
assert!(a.is_cancelled() && !b.is_cancelled() && abc.is_cancelled());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn after_polls_cancels_from_its_nth_poll_and_counts_every_poll() {
|
||||
assert_eq!(CancelToken::new().polls(), None);
|
||||
let t = CancelToken::after_polls(3);
|
||||
assert_eq!(t.polls(), Some(0));
|
||||
assert!(!t.is_cancelled());
|
||||
assert_eq!(t.check("kernel::test"), Ok(()));
|
||||
assert_eq!(
|
||||
t.check("kernel::test"),
|
||||
Err(SolidError::cancelled("kernel::test"))
|
||||
);
|
||||
// cancelled from the third poll on, and every poll counted
|
||||
assert!(t.is_cancelled());
|
||||
assert_eq!(t.polls(), Some(4));
|
||||
// a clone shares the count; a kernel's polls are the token's
|
||||
let mut k = Kernel::default();
|
||||
k.set_cancel_token(t.clone());
|
||||
t.reset();
|
||||
assert_eq!(t.polls(), Some(0));
|
||||
assert!(!k.is_cancelled());
|
||||
assert_eq!(k.check_cancelled("kernel::test"), Ok(()));
|
||||
assert!(k.is_cancelled());
|
||||
assert_eq!(t.polls(), Some(3));
|
||||
// from the first poll for 0 and 1; cancel() at once
|
||||
assert!(CancelToken::after_polls(0).is_cancelled());
|
||||
assert!(CancelToken::after_polls(1).is_cancelled());
|
||||
let c = CancelToken::after_polls(u64::MAX);
|
||||
assert!(!c.is_cancelled());
|
||||
c.cancel();
|
||||
assert!(c.is_cancelled());
|
||||
assert_eq!(c.polls(), Some(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn either_counts_a_poll_on_each_counting_token_of_its_chain() {
|
||||
let (a, b) = (
|
||||
CancelToken::after_polls(u64::MAX),
|
||||
CancelToken::after_polls(2),
|
||||
);
|
||||
let ab = a.either(&b);
|
||||
assert!(!ab.is_cancelled());
|
||||
// the joined token counts as a, and its poll is one of a and one of b
|
||||
assert_eq!(
|
||||
(ab.polls(), a.polls(), b.polls()),
|
||||
(Some(1), Some(1), Some(1))
|
||||
);
|
||||
// b's second poll cancels b and the joined token, not a
|
||||
assert!(ab.is_cancelled());
|
||||
assert!(b.is_cancelled() && !a.is_cancelled());
|
||||
assert_eq!((a.polls(), b.polls()), (Some(3), Some(3)));
|
||||
// a token that counts nothing, joined with b: b counts the poll, the joined token none
|
||||
let plain = CancelToken::new().either(&b);
|
||||
assert!(plain.is_cancelled());
|
||||
assert_eq!((plain.polls(), b.polls()), (None, Some(4)));
|
||||
// a's countdown is the joined token's too, and an earlier cancel skips no count
|
||||
let (c, d) = (
|
||||
CancelToken::after_polls(1),
|
||||
CancelToken::after_polls(u64::MAX),
|
||||
);
|
||||
let cd = c.either(&d);
|
||||
assert!(cd.is_cancelled() && cd.is_cancelled());
|
||||
assert_eq!((c.polls(), d.polls()), (Some(2), Some(2)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancel_token_round_trip() {
|
||||
let mut k = Kernel::default();
|
||||
|
||||
-187
@@ -13,7 +13,6 @@
|
||||
//! | [`Transform::concat`] | `SmTransform::ConcatTransform` / `smgu_4x4Mult`|
|
||||
//! | [`Transform::transform_point`] | `SmTransform::TransformPoint` |
|
||||
//! | [`Transform::transform_vector`] | `SmTransform::TransformVector` |
|
||||
//! | [`Transform::rotation`], [`Transform::uniform_scaling`], [`Transform::reflection`] | (not upstream) |
|
||||
//! | [`EFF_ZERO`] etc. | `SM_EFF_ZERO` etc. |
|
||||
//! | [`is_infinite_param`] | `SM_IS_INFINITE` |
|
||||
//! | [`max2`], [`min2`], [`max3`], [`min3`], [`max4`], [`min4`] | `smos_Max`, `smos_Min`, `smos_3Max`, `smos_3Min`, `smos_4Max`, `smos_4Min` |
|
||||
@@ -410,196 +409,10 @@ impl Transform {
|
||||
}
|
||||
}
|
||||
|
||||
/// The operation name of the refusals of the maps built from their numbers.
|
||||
const OP_MAPS: &str = "math::Transform";
|
||||
|
||||
/// Whether every coordinate of `v` is finite.
|
||||
fn finite3(v: Vec3) -> bool {
|
||||
v.x.is_finite() && v.y.is_finite() && v.z.is_finite()
|
||||
}
|
||||
|
||||
/// Not upstream: the rigid motions and scalings an application asks for by their numbers
|
||||
/// (a turn about a line, a scale about a point, a mirror in a plane), built here so the
|
||||
/// layers above the kernel only pass the numbers on. Each is in the row-vector convention
|
||||
/// of [`Transform`]: rows 0 to 2 the images of x, y and z, row 3 the translation.
|
||||
impl Transform {
|
||||
/// The linear map whose rows are `x`, `y` and `z`, about the point `fixed`, which it
|
||||
/// keeps: `p' = (p - fixed) A + fixed`, its translation `fixed - fixed A`.
|
||||
fn about(fixed: Point3, x: Vec3, y: Vec3, z: Vec3) -> Self {
|
||||
let image = x * fixed.x + y * fixed.y + z * fixed.z;
|
||||
Self::from_axes(x, y, z, fixed - image)
|
||||
}
|
||||
|
||||
/// The turn by `angle` radians, right-handed about `axis`, about the line through
|
||||
/// `point` along `axis` (any length, made unit here): Rodrigues' rotation formula,
|
||||
/// `R = c I + s [k]x + (1 - c) k kᵀ` with `k` the unit axis, `c` and `s` the cosine and
|
||||
/// sine of the angle, transposed for the row-vector convention.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// `InvalidInput` for a point, axis or angle not finite, or an axis whose squared
|
||||
/// length is below [`EFF_ZERO_SQ`].
|
||||
pub fn rotation(point: Point3, axis: Vec3, angle: f64) -> crate::error::Result<Self> {
|
||||
use crate::error::SolidError;
|
||||
if !(finite3(point) && finite3(axis) && angle.is_finite()) {
|
||||
return Err(SolidError::invalid_input(
|
||||
OP_MAPS,
|
||||
"a turn's numbers not finite",
|
||||
));
|
||||
}
|
||||
let k = axis
|
||||
.unitize()
|
||||
.map_err(|_| SolidError::invalid_input(OP_MAPS, "a turn's axis of no length"))?;
|
||||
let (s, c) = angle.sin_cos();
|
||||
let v = 1.0 - c;
|
||||
let (x, y, z) = (k.x, k.y, k.z);
|
||||
Ok(Self::about(
|
||||
point,
|
||||
Vec3::new(c + x * x * v, x * y * v + z * s, x * z * v - y * s),
|
||||
Vec3::new(x * y * v - z * s, c + y * y * v, y * z * v + x * s),
|
||||
Vec3::new(x * z * v + y * s, y * z * v - x * s, c + z * z * v),
|
||||
))
|
||||
}
|
||||
|
||||
/// The scale by `factor` in every direction about `centre`, which it keeps. A negative
|
||||
/// factor turns the scale through the centre as well.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// `InvalidInput` for a centre or factor not finite, or a factor of zero (a map with
|
||||
/// no inverse).
|
||||
pub fn uniform_scaling(centre: Point3, factor: f64) -> crate::error::Result<Self> {
|
||||
use crate::error::SolidError;
|
||||
if !(finite3(centre) && factor.is_finite()) || factor == 0.0 {
|
||||
return Err(SolidError::invalid_input(
|
||||
OP_MAPS,
|
||||
"a scale's numbers not finite, or a factor of zero",
|
||||
));
|
||||
}
|
||||
Ok(Self::about(
|
||||
centre,
|
||||
Vec3::new(factor, 0.0, 0.0),
|
||||
Vec3::new(0.0, factor, 0.0),
|
||||
Vec3::new(0.0, 0.0, factor),
|
||||
))
|
||||
}
|
||||
|
||||
/// The mirror in the plane through `origin` square to `normal` (any length, made unit
|
||||
/// here): `p' = p - 2 ((p - origin)·n) n`, its linear part `I - 2 n nᵀ` (symmetric, so
|
||||
/// the same in either convention) and its translation `2 (origin·n) n`. For a normal
|
||||
/// along an axis every entry is exact, so the mirror twice is the identity exactly.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// `InvalidInput` for an origin or normal not finite, or a normal whose squared length
|
||||
/// is below [`EFF_ZERO_SQ`].
|
||||
pub fn reflection(origin: Point3, normal: Vec3) -> crate::error::Result<Self> {
|
||||
use crate::error::SolidError;
|
||||
if !(finite3(origin) && finite3(normal)) {
|
||||
return Err(SolidError::invalid_input(
|
||||
OP_MAPS,
|
||||
"a mirror's numbers not finite",
|
||||
));
|
||||
}
|
||||
let n = normal
|
||||
.unitize()
|
||||
.map_err(|_| SolidError::invalid_input(OP_MAPS, "a mirror's normal of no length"))?;
|
||||
let row = |e: Vec3, ni: f64| e - n * (2.0 * ni);
|
||||
Ok(Self::from_axes(
|
||||
row(Vec3::X, n.x),
|
||||
row(Vec3::Y, n.y),
|
||||
row(Vec3::Z, n.z),
|
||||
n * (2.0 * origin.dot(n)),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The largest difference between two matrices' entries.
|
||||
fn apart(a: &Transform, b: &Transform) -> f64 {
|
||||
a.m.iter()
|
||||
.flatten()
|
||||
.zip(b.m.iter().flatten())
|
||||
.map(|(x, y)| (x - y).abs())
|
||||
.fold(0.0, f64::max)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_quarter_turn_about_z_takes_x_to_y_and_keeps_its_line() {
|
||||
use crate::geom::certified::Verdict;
|
||||
let q = core::f64::consts::FRAC_PI_2;
|
||||
let turn = Transform::rotation(Point3::ZERO, Vec3::new(0.0, 0.0, 2.0), q).unwrap();
|
||||
let want = Transform::from_axes(Vec3::Y, -Vec3::X, Vec3::Z, Point3::ZERO);
|
||||
assert!(apart(&turn, &want) <= 1e-15, "{turn:?}");
|
||||
// about the line through (10, 0, 0) along z: that point stays, (11, 0, 0) goes to
|
||||
// (10, 1, 0)
|
||||
let p = Point3::new(10.0, 0.0, 0.0);
|
||||
let turn = Transform::rotation(p, Vec3::Z, q).unwrap();
|
||||
assert!(turn.transform_point(p).distance(p) <= 1e-14);
|
||||
let image = turn.transform_point(Point3::new(11.0, 0.0, 0.0));
|
||||
assert!(
|
||||
image.distance(Point3::new(10.0, 1.0, 0.0)) <= 1e-14,
|
||||
"{image:?}"
|
||||
);
|
||||
// a turn about a skew axis keeps lengths and its axis
|
||||
let axis = Vec3::new(1.0, 2.0, 2.0);
|
||||
let turn = Transform::rotation(p, axis, 0.7).unwrap();
|
||||
assert!(
|
||||
turn.transform_point(p + axis * 3.0)
|
||||
.distance(p + axis * 3.0)
|
||||
<= 1e-12
|
||||
);
|
||||
for r in turn.row_lengths() {
|
||||
assert!((r - 1.0).abs() <= 1e-14);
|
||||
}
|
||||
assert_eq!(turn.is_regular(1e-9), Verdict::Yes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_scale_keeps_its_centre_and_a_mirror_twice_is_the_identity() {
|
||||
let c = Point3::new(1.0, -2.0, 3.0);
|
||||
let scale = Transform::uniform_scaling(c, 2.5).unwrap();
|
||||
assert!(scale.transform_point(c).distance(c) <= 1e-14);
|
||||
let p = scale.transform_point(c + Vec3::X);
|
||||
assert!(p.distance(c + Vec3::X * 2.5) <= 1e-14, "{p:?}");
|
||||
// the mirror in y = 5, an axis normal of any length: every entry exact
|
||||
let mirror =
|
||||
Transform::reflection(Point3::new(7.0, 5.0, -1.0), Vec3::new(0.0, 3.0, 0.0)).unwrap();
|
||||
assert_eq!(
|
||||
mirror.transform_point(Point3::new(1.0, 2.0, 3.0)),
|
||||
Point3::new(1.0, 8.0, 3.0)
|
||||
);
|
||||
assert_eq!(mirror.concat(&mirror), Transform::IDENTITY);
|
||||
// a slanted mirror keeps its plane's points and reverses its normal
|
||||
let n = Vec3::new(1.0, 1.0, 0.0);
|
||||
let mirror = Transform::reflection(c, n).unwrap();
|
||||
assert!(mirror.transform_point(c).distance(c) <= 1e-14);
|
||||
let back = mirror.transform_point(c + n);
|
||||
assert!(back.distance(c - n) <= 1e-14, "{back:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_from_numbers_refuse_what_is_not_finite_or_has_no_size() {
|
||||
let bad = |r: crate::error::Result<Transform>| matches!(r, Err(crate::error::SolidError::InvalidInput { operation, .. }) if operation == OP_MAPS);
|
||||
assert!(bad(Transform::rotation(Point3::ZERO, Vec3::ZERO, 1.0)));
|
||||
assert!(bad(Transform::rotation(Point3::ZERO, Vec3::Z, f64::NAN)));
|
||||
assert!(bad(Transform::rotation(
|
||||
Point3::new(f64::INFINITY, 0.0, 0.0),
|
||||
Vec3::Z,
|
||||
1.0
|
||||
)));
|
||||
assert!(bad(Transform::uniform_scaling(Point3::ZERO, 0.0)));
|
||||
assert!(bad(Transform::uniform_scaling(Point3::ZERO, f64::NAN)));
|
||||
assert!(bad(Transform::reflection(Point3::ZERO, Vec3::ZERO)));
|
||||
assert!(bad(Transform::reflection(
|
||||
Point3::ZERO,
|
||||
Vec3::new(f64::NAN, 1.0, 0.0)
|
||||
)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_rigid_motion_is_regular_and_a_flattening_is_not() {
|
||||
use crate::geom::certified::Verdict;
|
||||
|
||||
-1
@@ -31,7 +31,6 @@ pub mod iges;
|
||||
pub(crate) mod index;
|
||||
pub mod knots;
|
||||
pub mod math;
|
||||
pub mod n_sided;
|
||||
pub mod nurbs_crv;
|
||||
pub mod nurbs_srf;
|
||||
pub mod spiral;
|
||||
|
||||
+5
-5
@@ -119,7 +119,7 @@ fn bad(reason: &'static str) -> SolidError {
|
||||
/// equal is the polynomial curve on its points divided by that weight, exactly (a record
|
||||
/// spline's kernel curve carries a weight of 1 on every point). Only unequal weights make it
|
||||
/// rational.
|
||||
pub(crate) fn nurb_of(c: &BSplineCurve) -> Result<Curve> {
|
||||
fn nurb_of(c: &BSplineCurve) -> Result<Curve> {
|
||||
let mut nurb = c
|
||||
.nurb
|
||||
.clone()
|
||||
@@ -182,7 +182,7 @@ fn surface(net: Vec<Vec<Vec3>>, p: usize, q: usize, u: &[f64], v: &[f64]) -> Res
|
||||
}
|
||||
|
||||
/// `c` moved by minus `o`, into the network's own frame.
|
||||
pub(crate) fn moved(c: &Curve, o: CPoint) -> Result<Curve> {
|
||||
fn moved(c: &Curve, o: CPoint) -> Result<Curve> {
|
||||
let pw = c
|
||||
.cpts()
|
||||
.iter()
|
||||
@@ -192,7 +192,7 @@ pub(crate) fn moved(c: &Curve, o: CPoint) -> Result<Curve> {
|
||||
}
|
||||
|
||||
/// `s` moved by minus `o`, into the network's own frame.
|
||||
pub(crate) fn moved_surface(s: &Surface, o: CPoint) -> Result<Surface> {
|
||||
fn moved_surface(s: &Surface, o: CPoint) -> Result<Surface> {
|
||||
let net = s
|
||||
.rows()
|
||||
.map(|row| {
|
||||
@@ -206,7 +206,7 @@ pub(crate) fn moved_surface(s: &Surface, o: CPoint) -> Result<Surface> {
|
||||
}
|
||||
|
||||
/// The largest coordinate of `points`.
|
||||
pub(crate) fn largest<'a>(points: impl Iterator<Item = &'a CPoint>) -> f64 {
|
||||
fn largest<'a>(points: impl Iterator<Item = &'a CPoint>) -> f64 {
|
||||
points
|
||||
.map(|c| c.x.abs().max(c.y.abs()).max(c.z.abs()))
|
||||
.fold(0.0_f64, f64::max)
|
||||
@@ -226,7 +226,7 @@ fn room<T>(count: usize) -> Result<Vec<T>> {
|
||||
}
|
||||
|
||||
/// Whether `x` is at most `tol`; a NaN is not.
|
||||
pub(crate) fn within(x: f64, tol: f64) -> bool {
|
||||
fn within(x: f64, tol: f64) -> bool {
|
||||
matches!(x.partial_cmp(&tol), Some(Ordering::Less | Ordering::Equal))
|
||||
}
|
||||
|
||||
|
||||
-311
@@ -1,311 +0,0 @@
|
||||
//! An n-sided patch bounded by curves meeting end to end round one loop, certified: the kernel's
|
||||
//! fill (`N_FillNSidedHole`, [`BSplineSurface::create_n_sided_patch`]) splits each boundary
|
||||
//! curve at its middle and fills the hole with N four-sided patches round a centre, each bounded
|
||||
//! by halves of two boundary curves and two inner curves from their middles to the centre.
|
||||
//!
|
||||
//! Upstream: none (not upstream; solidcore-native). The fill is upstream's; the certificate is
|
||||
//! this module's.
|
||||
//!
|
||||
//! | Rust | Upstream |
|
||||
//! |---------------|----------|
|
||||
//! | [`n_sided`] | none |
|
||||
//!
|
||||
//! What is certified, and what is not:
|
||||
//! - **The boundary**: each half of each boundary curve against the patch side it is (the side
|
||||
//! whose certified distance to it is least, either way round, both on `[0, 1]`):
|
||||
//! [`certified_distance`], plus the rounding of extracting the side and splitting the curve by
|
||||
//! knot insertion (`γ_(12 (d + 1))` of twice the largest coordinate each, a coordinate error
|
||||
//! `e` moving a point by at most `2 e`), counted in the loop's own frame (minus its first
|
||||
//! control point, each moved coordinate rounding once). The largest is returned.
|
||||
//! - **The inner joins (G0)**: every patch side that is no boundary half against the nearest
|
||||
//! side of another patch, certified the same way; the largest must be within the tolerance.
|
||||
//! - **Tangency across the inner curves (G1)** is the fill's aim, within its angle tolerance,
|
||||
//! and is **not** certified here.
|
||||
//!
|
||||
//! Deviations: none (no upstream counterpart). Non-rational curves only (a curve whose weights
|
||||
//! are all one value is its polynomial form). No recursion.
|
||||
|
||||
use crate::error::{Result, SolidError};
|
||||
use crate::nurbs::b_spline_curve::BSplineCurve;
|
||||
use crate::nurbs::b_spline_surface::BSplineSurface;
|
||||
use crate::nurbs::crv_basic::Curve;
|
||||
use crate::nurbs::crv_deviation::certified_distance;
|
||||
use crate::nurbs::geom::Interval;
|
||||
use crate::nurbs::gordon::{largest, moved, moved_surface, nurb_of, within};
|
||||
use crate::nurbs::math::gamma;
|
||||
use crate::nurbs::srf_basic::{Direction, Surface};
|
||||
use crate::nurbs::srf_tool::p1::extract_iso_crv;
|
||||
|
||||
const OP: &str = "nurbs::n_sided::n_sided";
|
||||
|
||||
/// The fewest curves of an n-sided loop.
|
||||
pub const MIN_SIDES: usize = 3;
|
||||
/// The most curves of an n-sided loop.
|
||||
pub const MAX_SIDES: usize = 64;
|
||||
|
||||
/// The angle (degrees) within which the fill matches the cross-boundary derivatives it builds
|
||||
/// across its inner curves: the fill's own tests' value.
|
||||
const ANGLE_DEG: f64 = 1.0;
|
||||
|
||||
/// The N patches of an n-sided fill and its certificates.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NSided {
|
||||
/// The patches, one per boundary curve.
|
||||
pub patches: Vec<BSplineSurface>,
|
||||
/// The largest certified distance between a boundary curve's half and its patch side.
|
||||
pub bound: f64,
|
||||
/// The largest certified distance between the two patch sides of an inner join.
|
||||
pub joins: f64,
|
||||
}
|
||||
|
||||
/// Why a loop has no certified n-sided patch here.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum NSidedRefusal {
|
||||
/// A curve is rational (its weights not all one value): the fill takes polynomial curves.
|
||||
Rational,
|
||||
/// A boundary curve is not proved within the tolerance of the patches: the bound.
|
||||
PastTolerance {
|
||||
/// The largest certified distance.
|
||||
bound: f64,
|
||||
},
|
||||
/// Two patches are not proved to meet within the tolerance along an inner curve: the
|
||||
/// largest certified gap.
|
||||
Joins {
|
||||
/// The largest certified gap between two patches' sides.
|
||||
gap: f64,
|
||||
},
|
||||
}
|
||||
|
||||
fn bad(reason: &'static str) -> SolidError {
|
||||
SolidError::invalid_input(OP, reason)
|
||||
}
|
||||
|
||||
/// `c` on `[0, 1]`, as it runs or reversed.
|
||||
fn on_unit(c: &Curve, reversed: bool) -> Result<Curve> {
|
||||
let mut c = c.clone();
|
||||
if reversed {
|
||||
c.reverse()?;
|
||||
}
|
||||
c.reparam_to_interval(Interval::new(0.0, 1.0))?;
|
||||
Ok(c)
|
||||
}
|
||||
|
||||
/// The four sides of patch `s`, each on `[0, 1]`.
|
||||
fn sides(s: &Surface) -> Result<[Curve; 4]> {
|
||||
let b = s.parameter_bounds();
|
||||
Ok([
|
||||
on_unit(&extract_iso_crv(s, b.vb, Direction::U)?, false)?,
|
||||
on_unit(&extract_iso_crv(s, b.vt, Direction::U)?, false)?,
|
||||
on_unit(&extract_iso_crv(s, b.ul, Direction::V)?, false)?,
|
||||
on_unit(&extract_iso_crv(s, b.ur, Direction::V)?, false)?,
|
||||
])
|
||||
}
|
||||
|
||||
/// The least certified distance from `c` (on `[0, 1]`) to `side` either way round.
|
||||
fn nearest(c: &Curve, side: &Curve) -> Result<f64> {
|
||||
let forward = certified_distance(c, side)?;
|
||||
let backward = certified_distance(c, &on_unit(side, true)?)?;
|
||||
Ok(forward.min(backward))
|
||||
}
|
||||
|
||||
/// The n-sided fill of `curves` (each running on from the one before, the last back to the
|
||||
/// first), its boundary and inner joins certified within `tol` (module docs).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// `InvalidInput` for fewer than [`MIN_SIDES`] or more than [`MAX_SIDES`] curves, a curve
|
||||
/// without its NURB or `tol` not finite and positive; those of the fill (curves that do not
|
||||
/// meet within its model tolerance, ...), the extraction, the split and the certificates.
|
||||
pub fn n_sided(
|
||||
curves: &[BSplineCurve],
|
||||
tol: f64,
|
||||
) -> Result<std::result::Result<NSided, NSidedRefusal>> {
|
||||
if !(MIN_SIDES..=MAX_SIDES).contains(&curves.len()) {
|
||||
return Err(bad("fewer than three or more than 64 curves"));
|
||||
}
|
||||
if !(tol.is_finite() && tol > 0.0) {
|
||||
return Err(bad("a tolerance not finite and positive"));
|
||||
}
|
||||
let nurbs: Vec<Curve> = curves.iter().map(nurb_of).collect::<Result<_>>()?;
|
||||
if nurbs.iter().any(Curve::is_rational) {
|
||||
return Ok(Err(NSidedRefusal::Rational));
|
||||
}
|
||||
|
||||
// the fill, on copies of the polynomial curves (it changes its inputs)
|
||||
let mut bounding: Vec<BSplineCurve> = nurbs
|
||||
.iter()
|
||||
.map(|c| BSplineCurve::from_gw_nurb(3, c))
|
||||
.collect();
|
||||
let mut across: Vec<Option<BSplineCurve>> = bounding.iter().map(|_| None).collect();
|
||||
let patches =
|
||||
BSplineSurface::create_n_sided_patch(&mut bounding, &mut across, ANGLE_DEG, None)?;
|
||||
|
||||
// the loop's own frame
|
||||
let origin = nurbs
|
||||
.first()
|
||||
.and_then(|c| c.cpts().first().copied())
|
||||
.ok_or_else(|| bad("a curve without points"))?;
|
||||
let curves_local: Vec<Curve> = nurbs
|
||||
.iter()
|
||||
.map(|c| moved(c, origin))
|
||||
.collect::<Result<_>>()?;
|
||||
let mut local: Vec<Surface> = Vec::new();
|
||||
for p in &patches {
|
||||
let s = p
|
||||
.nurb
|
||||
.as_ref()
|
||||
.ok_or_else(|| bad("a patch without its NURB"))?;
|
||||
local.push(moved_surface(s, origin)?);
|
||||
}
|
||||
let size = largest(
|
||||
curves_local
|
||||
.iter()
|
||||
.flat_map(Curve::cpts)
|
||||
.chain(local.iter().flat_map(Surface::rows).flatten()),
|
||||
);
|
||||
let degree = curves_local
|
||||
.iter()
|
||||
.map(Curve::degree)
|
||||
.chain(local.iter().map(|s| {
|
||||
let (p, q) = s.degrees();
|
||||
p.max(q)
|
||||
}))
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
// splitting a curve and extracting a side (knot insertion each), and the two moves
|
||||
let rounding = 2.0 * (4.0 * gamma(12 * (degree + 1)) * size) + 2.0 * (2.0 * gamma(1) * size);
|
||||
|
||||
let all_sides: Vec<[Curve; 4]> = local.iter().map(sides).collect::<Result<_>>()?;
|
||||
// which patch sides are boundary halves
|
||||
let mut boundary_side = vec![[false; 4]; all_sides.len()];
|
||||
let mut bound = 0.0_f64;
|
||||
for c in &curves_local {
|
||||
let (a, b) = c.param_bounds();
|
||||
let (first, second) = c.split(0.5 * (a + b))?;
|
||||
for half in [first, second] {
|
||||
let half = on_unit(&half, false)?;
|
||||
let mut best: Option<(f64, usize, usize)> = None;
|
||||
for (i, four) in all_sides.iter().enumerate() {
|
||||
for (j, side) in four.iter().enumerate() {
|
||||
let d = nearest(&half, side)?;
|
||||
if best.is_none_or(|(b, _, _)| d < b) {
|
||||
best = Some((d, i, j));
|
||||
}
|
||||
}
|
||||
}
|
||||
let Some((d, i, j)) = best else {
|
||||
return Err(bad("a fill without patches"));
|
||||
};
|
||||
if let Some(flag) = boundary_side.get_mut(i).and_then(|f| f.get_mut(j)) {
|
||||
*flag = true;
|
||||
}
|
||||
bound = bound.max((d + rounding) * (1.0 + gamma(3)));
|
||||
}
|
||||
}
|
||||
if !within(bound, tol) {
|
||||
return Ok(Err(NSidedRefusal::PastTolerance { bound }));
|
||||
}
|
||||
|
||||
// the inner joins: each side that is no boundary half against the nearest side of another
|
||||
// patch
|
||||
let mut joins = 0.0_f64;
|
||||
for (i, four) in all_sides.iter().enumerate() {
|
||||
for (j, side) in four.iter().enumerate() {
|
||||
if boundary_side
|
||||
.get(i)
|
||||
.and_then(|f| f.get(j))
|
||||
.copied()
|
||||
.unwrap_or(true)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let mut best = f64::INFINITY;
|
||||
for (other, theirs) in all_sides.iter().enumerate() {
|
||||
if other == i {
|
||||
continue;
|
||||
}
|
||||
for s in theirs {
|
||||
best = best.min(nearest(side, s)?);
|
||||
}
|
||||
}
|
||||
joins = joins.max((best + rounding) * (1.0 + gamma(3)));
|
||||
}
|
||||
}
|
||||
if !within(joins, tol) {
|
||||
return Ok(Err(NSidedRefusal::Joins { gap: joins }));
|
||||
}
|
||||
Ok(Ok(NSided {
|
||||
patches,
|
||||
bound,
|
||||
joins,
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::math::Vec3;
|
||||
use crate::nurbs::math::CPoint;
|
||||
|
||||
/// The polynomial curve of degree `p` on `points` with clamped uniform knots.
|
||||
fn spline(points: &[[f64; 3]], p: usize) -> BSplineCurve {
|
||||
let n = points.len();
|
||||
let mut knots = vec![0.0; p + 1];
|
||||
for i in 1..n - p {
|
||||
knots.push(i as f64 / (n - p) as f64);
|
||||
}
|
||||
knots.extend(vec![1.0; p + 1]);
|
||||
let pw = points
|
||||
.iter()
|
||||
.map(|q| CPoint::from_point(Vec3::new(q[0], q[1], q[2])))
|
||||
.collect();
|
||||
BSplineCurve::from_gw_nurb(3, &Curve::new(pw, p, knots).unwrap())
|
||||
}
|
||||
|
||||
/// The corners of a regular pentagon of radius 10 in z = 0.
|
||||
fn pentagon() -> Vec<[f64; 3]> {
|
||||
(0..5)
|
||||
.map(|i| {
|
||||
let a = std::f64::consts::TAU * f64::from(i) / 5.0;
|
||||
[10.0 * a.cos(), 10.0 * a.sin(), 0.0]
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_pentagon_of_lines_is_filled_flat() {
|
||||
let p = pentagon();
|
||||
let lines: Vec<BSplineCurve> = (0..5).map(|i| spline(&[p[i], p[(i + 1) % 5]], 1)).collect();
|
||||
let f = n_sided(&lines, 1e-6).unwrap().unwrap();
|
||||
assert_eq!(f.patches.len(), 5);
|
||||
assert!(
|
||||
f.bound <= 1e-9 && f.joins <= 1e-9,
|
||||
"{} {}",
|
||||
f.bound,
|
||||
f.joins
|
||||
);
|
||||
// planar data, planar patches: every control point in z = 0
|
||||
for s in &f.patches {
|
||||
for c in s.nurb.as_ref().unwrap().rows().flatten() {
|
||||
assert!(c.z.abs() < 1e-9, "{c:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rational_curves_are_refused() {
|
||||
let p = pentagon();
|
||||
let mut curves: Vec<BSplineCurve> =
|
||||
(0..5).map(|i| spline(&[p[i], p[(i + 1) % 5]], 1)).collect();
|
||||
let pw = vec![
|
||||
CPoint::new(p[0][0], p[0][1], 0.0, 1.0),
|
||||
CPoint::new(2.0 * p[1][0], 2.0 * p[1][1], 0.0, 2.0),
|
||||
];
|
||||
curves[0] =
|
||||
BSplineCurve::from_gw_nurb(3, &Curve::new(pw, 1, vec![0.0, 0.0, 1.0, 1.0]).unwrap());
|
||||
assert_eq!(
|
||||
n_sided(&curves, 1e-6).unwrap().unwrap_err(),
|
||||
NSidedRefusal::Rational
|
||||
);
|
||||
}
|
||||
}
|
||||
+1
-116
@@ -42,8 +42,6 @@
|
||||
//! | [`Surface::average_len`], [`AverageLen`] | `N_SrfGetAverageLen` |
|
||||
//! | [`Direction`] | `NL_UDIR` / `NL_VDIR` / `NL_UVDIR` |
|
||||
//! | [`WEIGHT_MIN`], [`WEIGHT_MAX`], [`WEIGHT_TOL`] | `NL_WMIN`, `NL_WMAX`, `NL_WTOL` |
|
||||
//! | [`Surface::displace_cpts`] | none |
|
||||
//! | [`falloff_weight`], [`FalloffShape`] | none |
|
||||
//!
|
||||
//! Deviations:
|
||||
//! - [`Surface`] owns its control net and knot arrays (upstream points at caller- or
|
||||
@@ -94,7 +92,7 @@ use crate::nurbs::geom::{
|
||||
dist_pt_pt, dist_signed_pt_plane, plane_pt_normal_to_implicit, Rectangle,
|
||||
};
|
||||
use crate::nurbs::knots::{KnotVector, MAX_DEGREE};
|
||||
use crate::nurbs::math::{combine2_pts, combine4_pts, CPoint, MODEL_TOL, NOW, NOZ};
|
||||
use crate::nurbs::math::{combine2_pts, combine4_pts, CPoint, MODEL_TOL, NOW};
|
||||
|
||||
/// Minimum valid weight (`NL_WMIN`).
|
||||
pub const WEIGHT_MIN: f64 = 1.0e-03;
|
||||
@@ -109,7 +107,6 @@ const OP_NEW: &str = "nurbs::srf_basic::Surface::new";
|
||||
const MIXED_RATIONALITY: &str = "control points mix rational and non-rational";
|
||||
const OP_WITH_INDICES: &str = "nurbs::srf_basic::Surface::with_indices";
|
||||
const OP_CPT: &str = "nurbs::srf_basic::Surface::cpt";
|
||||
const OP_DISPLACE: &str = "nurbs::srf_basic::Surface::displace_cpts";
|
||||
const OP_SET_CPTS: &str = "nurbs::srf_basic::Surface::set_cpts_and_knots";
|
||||
const OP_SET_SIZE: &str = "nurbs::srf_basic::Surface::set_size_indices";
|
||||
const OP_COUNTS: &str = "nurbs::srf_basic::Surface::counts_are_valid";
|
||||
@@ -452,45 +449,6 @@ impl Surface {
|
||||
self.rows().map(<[CPoint]>::to_vec).collect()
|
||||
}
|
||||
|
||||
/// Moves control points by fractions of one displacement (not upstream): each
|
||||
/// `((i, j), t)` of `moves` moves the Euclidean point of `Pw[i][j]` by `t * delta`, its
|
||||
/// weight kept (a rational point's weighted coordinates move by `w t delta`). A point
|
||||
/// listed twice moves by the sum of its fractions. All or nothing: on a refusal the net
|
||||
/// is unchanged.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// `InvalidInput` for an index past the net, a fraction or `delta` not finite, a 2D net
|
||||
/// (no `z`), or a moved point not finite.
|
||||
pub fn displace_cpts(&mut self, moves: &[((usize, usize), f64)], delta: Vec3) -> Result<()> {
|
||||
if !(delta.x.is_finite() && delta.y.is_finite() && delta.z.is_finite()) {
|
||||
return Err(invalid(OP_DISPLACE, "a displacement not finite"));
|
||||
}
|
||||
let mut next = self.clone();
|
||||
for &((i, j), t) in moves {
|
||||
if !t.is_finite() {
|
||||
return Err(invalid(OP_DISPLACE, "a fraction not finite"));
|
||||
}
|
||||
let c = next.cpt_mut(i, j)?;
|
||||
if c.z == NOZ {
|
||||
return Err(invalid(OP_DISPLACE, "a 2D control net"));
|
||||
}
|
||||
let w = if c.w == NOW { 1.0 } else { c.w };
|
||||
let moved = CPoint {
|
||||
x: c.x + w * t * delta.x,
|
||||
y: c.y + w * t * delta.y,
|
||||
z: c.z + w * t * delta.z,
|
||||
w: c.w,
|
||||
};
|
||||
if !(moved.x.is_finite() && moved.y.is_finite() && moved.z.is_finite()) {
|
||||
return Err(invalid(OP_DISPLACE, "a moved point not finite"));
|
||||
}
|
||||
*c = moved;
|
||||
}
|
||||
*self = next;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The u knots `U[0..=r]` (`N_SrfGetKnots`).
|
||||
#[must_use]
|
||||
pub fn u_knots(&self) -> &[f64] {
|
||||
@@ -1137,31 +1095,6 @@ fn rescale(u: &mut [f64], r: usize, p: usize, lo: f64, hi: f64) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// How a soft move's share falls with a control point's index distance from the moved points
|
||||
/// (not upstream), with `s = d / (radius + 1)` for a point `d` index steps away.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum FalloffShape {
|
||||
/// `1 - s`.
|
||||
Linear,
|
||||
/// `1 - (3 s² - 2 s³)`: flat at the moved points and at the radius's edge.
|
||||
Smooth,
|
||||
}
|
||||
|
||||
/// The fraction of a soft move ([`Surface::displace_cpts`]) taken by a control point `d` index
|
||||
/// steps from the nearest moved point (not upstream): 1 at `d = 0`, falling by `shape` with
|
||||
/// `s = d / (radius + 1)`, and 0 past `radius`.
|
||||
#[must_use]
|
||||
pub fn falloff_weight(shape: FalloffShape, radius: u32, d: u32) -> f64 {
|
||||
if d > radius {
|
||||
return 0.0;
|
||||
}
|
||||
let s = f64::from(d) / (f64::from(radius) + 1.0);
|
||||
match shape {
|
||||
FalloffShape::Linear => 1.0 - s,
|
||||
FalloffShape::Smooth => 1.0 - s * s * (3.0 - 2.0 * s),
|
||||
}
|
||||
}
|
||||
|
||||
/// Makes the surfaces rationality-compatible (`N_MakeSrfsRatCompatible`): when any
|
||||
/// is rational, every non-rational one gets weights `1.0`.
|
||||
pub fn make_rat_compatible(surfaces: &mut [Surface]) {
|
||||
@@ -1290,54 +1223,6 @@ mod tests {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn falloff_is_one_at_the_moved_points_and_zero_past_the_radius() {
|
||||
for shape in [FalloffShape::Linear, FalloffShape::Smooth] {
|
||||
assert_eq!(falloff_weight(shape, 0, 0), 1.0);
|
||||
assert_eq!(falloff_weight(shape, 0, 1), 0.0);
|
||||
assert_eq!(falloff_weight(shape, 3, 0), 1.0);
|
||||
assert_eq!(falloff_weight(shape, 3, 4), 0.0);
|
||||
assert_eq!(falloff_weight(shape, 3, u32::MAX), 0.0);
|
||||
let w: Vec<f64> = (0..=3).map(|d| falloff_weight(shape, 3, d)).collect();
|
||||
assert!(w.windows(2).all(|p| p[1] < p[0]), "{shape:?}: {w:?}");
|
||||
}
|
||||
// s = 1/4 at one step of radius 3
|
||||
assert_eq!(falloff_weight(FalloffShape::Linear, 3, 1), 0.75);
|
||||
assert_eq!(
|
||||
falloff_weight(FalloffShape::Smooth, 3, 1),
|
||||
1.0 - 0.0625 * 2.5
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn displaced_points_move_by_their_fraction_of_the_delta() {
|
||||
let mut s = bilinear();
|
||||
let d = Vec3::new(0.0, 0.0, 2.0);
|
||||
// a point listed twice moves by the sum of its fractions
|
||||
s.displace_cpts(&[((1, 1), 0.5), ((0, 0), 1.0), ((1, 1), 0.25)], d)
|
||||
.unwrap();
|
||||
assert_eq!(s.cpt(0, 0).unwrap(), CPoint::new(0.0, 0.0, 2.0, NOW));
|
||||
assert_eq!(s.cpt(1, 1).unwrap(), CPoint::new(1.0, 1.0, 1.5, NOW));
|
||||
assert_eq!(s.cpt(0, 1).unwrap(), CPoint::new(0.0, 1.0, 0.0, NOW));
|
||||
assert_eq!(s.cpt(1, 0).unwrap(), CPoint::new(1.0, 0.0, 0.0, NOW));
|
||||
// a rational point keeps its weight; its weighted coordinates move by w t delta
|
||||
let mut r = bilinear();
|
||||
r.non_rat_to_rat();
|
||||
*r.cpt_mut(1, 0).unwrap() = CPoint::new(2.0, 0.0, 0.0, 2.0);
|
||||
r.displace_cpts(&[((1, 0), 1.0)], d).unwrap();
|
||||
assert_eq!(r.cpt(1, 0).unwrap(), CPoint::new(2.0, 0.0, 4.0, 2.0));
|
||||
// all or nothing: a bad move after a good one leaves the net as it was
|
||||
let before = bilinear();
|
||||
let mut t = before.clone();
|
||||
assert!(t.displace_cpts(&[((0, 0), 1.0), ((2, 0), 1.0)], d).is_err());
|
||||
assert!(t.displace_cpts(&[((0, 0), f64::NAN)], d).is_err());
|
||||
assert!(t
|
||||
.displace_cpts(&[((0, 0), 1.0)], Vec3::new(f64::INFINITY, 0.0, 0.0))
|
||||
.is_err());
|
||||
assert!(t.displace_cpts(&[((0, 0), f64::MAX)], d).is_err());
|
||||
assert_eq!(t, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn construct_and_access() {
|
||||
let s = bilinear();
|
||||
|
||||
+14
-94
@@ -842,8 +842,7 @@ impl Dv {
|
||||
}
|
||||
|
||||
/// The trace of a plane in a normal section, or of two planes' line: the foot `x0` of the
|
||||
/// edge point on it, its squared distance `foot2` from the point, and its direction `u`,
|
||||
/// unit only within rounding (a point `x0 + τ u` is `|τ| |u|` from the foot, not `|τ|`).
|
||||
/// edge point on it, its squared distance `foot2` from the point, and its unit direction.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Trace {
|
||||
x0: Dv,
|
||||
@@ -1109,11 +1108,8 @@ fn plan_of(line: &Trace, q: &AlongLine, quadric: &Implicit, tolerance: f64) -> P
|
||||
|
||||
/// The squared distances from the edge point of the meetings `plan` takes (each a bound on
|
||||
/// the reach: the meeting taken is the nearest), or `None` where the plan takes none or its
|
||||
/// formula is not defined on these enclosures. A meeting at `τ` is `foot² + τ² |u|²` from
|
||||
/// the point: `|u|²` enclosed as `u·u`, since the direction is unit only within rounding,
|
||||
/// and an upper bound taken as if `|u| = 1` could sit an ulp below the distance.
|
||||
/// formula is not defined on these enclosures.
|
||||
fn planned(plan: &Plan, line: &Trace, q: &AlongLine) -> Option<Vec<Di>> {
|
||||
let unit = line.u.dot(line.u);
|
||||
match plan {
|
||||
Plan::Roots {
|
||||
positive,
|
||||
@@ -1124,7 +1120,7 @@ fn planned(plan: &Plan, line: &Trace, q: &AlongLine) -> Option<Vec<Di>> {
|
||||
.iter()
|
||||
.map(|&i| {
|
||||
let tau = taus.get(i).copied().flatten()?;
|
||||
Some(line.foot2 + tau * tau * unit)
|
||||
Some(line.foot2 + tau * tau)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -1133,7 +1129,7 @@ fn planned(plan: &Plan, line: &Trace, q: &AlongLine) -> Option<Vec<Di>> {
|
||||
return None;
|
||||
}
|
||||
let tau = (Di::constant(0.0) - q.b) / (Di::constant(2.0) * q.a);
|
||||
Some(vec![line.foot2 + tau * tau * unit])
|
||||
Some(vec![line.foot2 + tau * tau])
|
||||
}
|
||||
Plan::Cover => {
|
||||
if !q.a.excludes_zero() {
|
||||
@@ -1151,7 +1147,7 @@ fn planned(plan: &Plan, line: &Trace, q: &AlongLine) -> Option<Vec<Di>> {
|
||||
v: Ivl::from(tau),
|
||||
d: Ivl::whole(),
|
||||
};
|
||||
Some(vec![line.foot2 + tau * tau * unit])
|
||||
Some(vec![line.foot2 + tau * tau])
|
||||
}
|
||||
Plan::Apart | Plan::Unknown => None,
|
||||
}
|
||||
@@ -1836,14 +1832,18 @@ fn straight_edges_between(
|
||||
}
|
||||
|
||||
/// Where the line of a kept straight edge ([`StraightEdge`]) meets the plane `(o, ν)`,
|
||||
/// from the vertex `p`: [`line_meets_plane`] on the line from the edge's first control
|
||||
/// point along its unit direction (unit within rounding).
|
||||
/// from the vertex `p`: the line's point `x0 + τ u` (`x0` the foot of `p`, `u` the unit
|
||||
/// direction, `τ = (o − x0)·ν / (u·ν)`) at `√(|x0 − p|² + τ²)`, raised by the curve's
|
||||
/// distance from the line over the sine of their angle (`dev |ν| / |u·ν|`); nowhere near
|
||||
/// where the line is within `tangent_sine` of parallel to the plane (as two planes'
|
||||
/// line, [`planes_line`]); not decided where the enclosures do not settle it.
|
||||
fn straight_edge_meets_plane(
|
||||
p: Vec3,
|
||||
line: StraightEdge,
|
||||
plane: Tangent,
|
||||
tangent_sine: f64,
|
||||
) -> CornerMeeting {
|
||||
let pv = Dv::constant(p);
|
||||
let a = Dv::constant(line.a);
|
||||
let d = Dv::constant(line.b).sub(a);
|
||||
let dd = d.dot(d);
|
||||
@@ -1851,24 +1851,6 @@ fn straight_edge_meets_plane(
|
||||
return CornerMeeting::Uncertified;
|
||||
}
|
||||
let u = d.scale(Di::constant(1.0) / dd.sqrt());
|
||||
line_meets_plane(Dv::constant(p), a, u, line.dev, plane, tangent_sine)
|
||||
}
|
||||
|
||||
/// Where the line `a + s u` meets the plane `(o, ν)`, from `p`: its point `M = x0 + τ u`
|
||||
/// (`x0 = a + u (u·(p − a))`, the foot of `p` where `|u| = 1`, a point of the line in any
|
||||
/// case; `τ = (o − x0)·ν / (u·ν)`) at `|M − p|`, enclosed as it is, so it holds whatever
|
||||
/// `|u|` (the direction is unit only within rounding), raised by the curve's distance `dev`
|
||||
/// from the line over the sine of their angle, `|u·ν| / (|u| |ν|)`; nowhere near where the
|
||||
/// line is within `tangent_sine` of parallel to the plane (as two planes' line,
|
||||
/// [`planes_line`]); not decided where the enclosures do not settle it.
|
||||
fn line_meets_plane(
|
||||
pv: Dv,
|
||||
a: Dv,
|
||||
u: Dv,
|
||||
dev: f64,
|
||||
plane: Tangent,
|
||||
tangent_sine: f64,
|
||||
) -> CornerMeeting {
|
||||
let x0 = a.add(u.scale(pv.sub(a).dot(u)));
|
||||
let nu = Dv::constant(plane.raw);
|
||||
let nn = nu.dot(nu).sqrt();
|
||||
@@ -1880,9 +1862,9 @@ fn line_meets_plane(
|
||||
return CornerMeeting::Nowhere;
|
||||
}
|
||||
let tau = Dv::constant(plane.point).sub(x0).dot(nu) / un;
|
||||
let to = x0.add(u.scale(tau)).sub(pv);
|
||||
let far = to.dot(to).v.hi.max(0.0);
|
||||
let slack = (Ivl::from(dev) * nn.v * u.dot(u).v.sqrt() / Ivl::from(un.v.mig())).hi;
|
||||
let foot = x0.sub(pv);
|
||||
let far = (foot.dot(foot) + tau * tau).v.hi.max(0.0);
|
||||
let slack = (Ivl::from(line.dev) * nn.v / Ivl::from(un.v.mig())).hi;
|
||||
let reach = round_up(round_up(sqrt(far)) + slack);
|
||||
if reach.is_finite() {
|
||||
CornerMeeting::Within(reach)
|
||||
@@ -3534,66 +3516,4 @@ mod tests {
|
||||
assert_eq!(xs(&left), vec![0.0, 0.5, 1.5]);
|
||||
assert_eq!(xs(&right), vec![1.5, 2.5, 4.0]);
|
||||
}
|
||||
|
||||
/// A meeting's distance takes the trace direction's length: the unit sphere and the line
|
||||
/// `x0 + τ u` through its centre with `u = (2, 0, 0)` (far from unit, to show the
|
||||
/// factor; a trace's own `u` is unit within rounding), its edge point `f` off the centre.
|
||||
/// The roots `τ = ±½` are the points `(±1, 0, 0)`, `√(f² + 1)` from the edge point; as
|
||||
/// if `|u| = 1` the bound would be `√(f² + ¼)`, below it.
|
||||
#[test]
|
||||
fn a_meeting_is_as_far_as_its_point_whatever_the_direction_length() {
|
||||
let sphere = Implicit::Sphere {
|
||||
centre: Vec3::new(0.0, 0.0, 0.0),
|
||||
radius: 1.0,
|
||||
};
|
||||
for f in [0.0, 0.5, 3.0] {
|
||||
let line = Trace {
|
||||
x0: Dv::constant(Vec3::new(0.0, 0.0, 0.0)),
|
||||
foot2: Di::constant(f * f),
|
||||
u: Dv::constant(Vec3::new(2.0, 0.0, 0.0)),
|
||||
};
|
||||
let q = along_line(&sphere, &line).unwrap();
|
||||
let plan = plan_of(&line, &q, &sphere, 1e-5);
|
||||
assert!(matches!(plan, Plan::Roots { .. }), "{plan:?}");
|
||||
let found = planned(&plan, &line, &q).unwrap();
|
||||
assert_eq!(found.len(), 2, "{found:?}");
|
||||
let exact = f * f + 1.0;
|
||||
for g in found {
|
||||
assert!(g.v.lo <= exact && exact <= g.v.hi, "{f}: {g:?} {exact}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A line's meeting with a plane is as far as its point, whatever the direction's
|
||||
/// length: the line through the origin along `u = (2, 0, 0)` (far from unit, to show
|
||||
/// it; an edge's own `u` is unit within rounding) meets the plane `x = 3` at `(3, 0, 0)`,
|
||||
/// `√5` from `p = (1, 1, 0)`; `x0 = (4, 0, 0)` is a point of the line but not `p`'s foot.
|
||||
/// A curve `dev` off the line, which is square to the plane, adds `dev` (the sine is
|
||||
/// `|u·ν| / (|u| |ν|)` = 1), not `dev / 2`.
|
||||
#[test]
|
||||
fn a_line_meets_a_plane_as_far_as_its_point_whatever_the_direction_length() {
|
||||
let plane = Tangent {
|
||||
point: Vec3::new(3.0, 0.0, 0.0),
|
||||
normal: Vec3::new(1.0, 0.0, 0.0),
|
||||
raw: Vec3::new(1.0, 0.0, 0.0),
|
||||
};
|
||||
for dev in [0.0, 0.1] {
|
||||
let met = line_meets_plane(
|
||||
Dv::constant(Vec3::new(1.0, 1.0, 0.0)),
|
||||
Dv::constant(Vec3::new(0.0, 0.0, 0.0)),
|
||||
Dv::constant(Vec3::new(2.0, 0.0, 0.0)),
|
||||
dev,
|
||||
plane,
|
||||
1e-3,
|
||||
);
|
||||
let exact = 5.0_f64.sqrt() + dev;
|
||||
let CornerMeeting::Within(reach) = met else {
|
||||
panic!("{dev}: {met:?}");
|
||||
};
|
||||
assert!(
|
||||
reach >= exact && reach <= exact * (1.0 + 1e-12),
|
||||
"{dev}: {reach} {exact}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-41
@@ -19,7 +19,6 @@
|
||||
//! | [`PrimitiveCreation::create_sphere_no_pole`] | `CreateSphereNoPole` |
|
||||
//! | [`PrimitiveCreation::create_cone_elliptic_ends`] | `CreateConeEllipticEnds` |
|
||||
//! | [`analytic_rotational_sweep`] | `sm_CreateAnalyticRotationalSweep` |
|
||||
//! | [`coplanar_with_axis`] | (not upstream, WR-776) |
|
||||
//!
|
||||
//! Deviations: missing topology edits return Unported at their first call.
|
||||
//! Mutations after that call are deferred; no success is claimed for those paths.
|
||||
@@ -27,11 +26,6 @@
|
||||
//! them. `SmBrep` is caller-owned; edits retain upstream edit-mode side effects.
|
||||
//! Face construction uses caller notification records, attributes and identity keys.
|
||||
//! No recursion.
|
||||
//! Not upstream (WR-776): [`analytic_rotational_sweep`] makes its cone only for a line in
|
||||
//! one plane with the axis ([`coplanar_with_axis`]); upstream makes the cone through the
|
||||
//! end circles of a line skew to the axis too, which sweeps a hyperboloid of one sheet. A
|
||||
//! skew line is refused here (a described failure, `SM_ERR` status) before any edit, so
|
||||
//! the caller gives it its surface of revolution.
|
||||
//! Not upstream (WR-565, tangency-labs/solidcore#1477, opt-in
|
||||
//! `Kernel::fixes.linear_sweep_arc_is_cylinder`; SolidCraft change, tangency-labs/smlib
|
||||
//! 12918e9): [`PrimitiveCreation::create_linear_sweep`] makes a circular arc short of a
|
||||
@@ -1437,35 +1431,9 @@ impl PrimitiveCreation {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the segment from `start` to `end` lies in one plane with the axis through
|
||||
/// `axis_base` along `axis`, within `tol`: the lines are parallel (the sine of their
|
||||
/// angle within [`EFF_ZERO`]), or their common perpendicular is no longer than `tol` (one
|
||||
/// not finite is not). Only such a line, neither parallel nor square to the axis, sweeps a
|
||||
/// cone; a skew one sweeps a hyperboloid of one sheet, its radius about the axis the root
|
||||
/// of a quadratic in the axial coordinate, not linear in it (WR-776).
|
||||
#[must_use]
|
||||
pub(crate) fn coplanar_with_axis(
|
||||
axis_base: crate::math::Point3,
|
||||
axis: Vec3,
|
||||
start: crate::math::Point3,
|
||||
end: crate::math::Point3,
|
||||
tol: f64,
|
||||
) -> bool {
|
||||
let line = end - start;
|
||||
let normal = line.cross(axis);
|
||||
let sine = normal.length();
|
||||
if sine <= EFF_ZERO * line.length() * axis.length() {
|
||||
return true;
|
||||
}
|
||||
((start - axis_base).dot(normal) / sine).abs() <= tol
|
||||
}
|
||||
|
||||
/// Create an analytic rotational sweep (`sm_CreateAnalyticRotationalSweep`).
|
||||
/// Lines on the axis, interior axis crossings and unsuitable circular arcs return
|
||||
/// `SM_ERR`; topology construction defers at its exact owning unit. Not upstream
|
||||
/// (WR-776): a line skew to the axis and neither parallel nor square to it
|
||||
/// ([`coplanar_with_axis`] at the B-rep's tolerance) fails, described, before any edit,
|
||||
/// where upstream makes it the cone through its end circles.
|
||||
/// `SM_ERR`; topology construction defers at its exact owning unit.
|
||||
pub fn analytic_rotational_sweep(
|
||||
k: &mut Kernel,
|
||||
brep: &mut Brep,
|
||||
@@ -1546,14 +1514,6 @@ pub fn analytic_rotational_sweep(
|
||||
)),
|
||||
};
|
||||
} else {
|
||||
// Not upstream (WR-776): a line skew to the axis sweeps a hyperboloid, not the
|
||||
// cone through its end circles
|
||||
if !coplanar_with_axis(axis_base, axis, ends.start, ends.end, tol) {
|
||||
return Err(SolidError::primitive_failed(
|
||||
op,
|
||||
"the line is skew to the axis: it sweeps a hyperboloid, not a cone",
|
||||
));
|
||||
}
|
||||
let (bottom_radius, bottom_parameter) =
|
||||
line_point_distance(axis_base, axis, ends.start);
|
||||
let (top_radius, top_parameter) = line_point_distance(axis_base, axis, ends.end);
|
||||
|
||||
@@ -703,69 +703,6 @@ mod tests {
|
||||
)
|
||||
}
|
||||
|
||||
/// WR-776: a line skew to the axis sweeps a hyperboloid of one sheet. Every point of
|
||||
/// the result is as far from the axis as the profile's point at the same axial
|
||||
/// coordinate, both measured here from the profile's and the axis's own numbers.
|
||||
/// Upstream's one-curve special case made the cone through the end circles, 3.85 mm
|
||||
/// off its hyperboloid half way along this line; with the analytic-faces fix or
|
||||
/// without, the line is its surface of revolution.
|
||||
#[test]
|
||||
fn a_line_skew_to_the_axis_revolves_onto_its_hyperboloid() {
|
||||
let (p0, p1) = ([600.0, 0.0, 0.0], [700.0, 0.0, 200.0]);
|
||||
let o = [500.0, -50.0, 0.0];
|
||||
let n = (40.0_f64 * 40.0 + 80.0 * 80.0 + 300.0 * 300.0).sqrt();
|
||||
let a = [40.0 / n, 80.0 / n, 300.0 / n];
|
||||
let dot = |u: [f64; 3], v: [f64; 3]| u[0] * v[0] + u[1] * v[1] + u[2] * v[2];
|
||||
// the axial coordinate of p and its distance from the axis
|
||||
let polar = |p: [f64; 3]| {
|
||||
let q = [p[0] - o[0], p[1] - o[1], p[2] - o[2]];
|
||||
let h = dot(q, a);
|
||||
(h, (dot(q, q) - h * h).max(0.0).sqrt())
|
||||
};
|
||||
let ((h0, _), (h1, _)) = (polar(p0), polar(p1));
|
||||
let profile = segment(p0, p1);
|
||||
for analytic in [false, true] {
|
||||
let mut k = Kernel::default();
|
||||
k.fixes.revolve_analytic_profile_faces = analytic;
|
||||
let sheet = create_rotational_sweep(
|
||||
&mut k,
|
||||
&[Some(&profile)],
|
||||
Point3::new(o[0], o[1], o[2]),
|
||||
Vec3::new(a[0], a[1], a[2]),
|
||||
360.0,
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
let faces = crate::topology::brep::p1::faces(&sheet, None).unwrap();
|
||||
assert!(!faces.is_empty());
|
||||
let mut worst = 0.0_f64;
|
||||
for f in faces {
|
||||
let surface = crate::topology::face::p2::surface(&sheet, f)
|
||||
.unwrap()
|
||||
.expect("a surface");
|
||||
let d = surface.natural_uv_domain_with_nurb(&k).unwrap();
|
||||
for i in 0..=24 {
|
||||
for j in 0..=24 {
|
||||
let uv = crate::geom::vector2d::Point2::new(
|
||||
d.u_min() + (d.u_max() - d.u_min()) * f64::from(i) / 24.0,
|
||||
d.v_min() + (d.v_max() - d.v_min()) * f64::from(j) / 24.0,
|
||||
);
|
||||
let x = surface.evaluate_point(&k, uv).unwrap();
|
||||
let (h, r) = polar([x.x, x.y, x.z]);
|
||||
let t = (h - h0) / (h1 - h0);
|
||||
assert!((-1e-9..=1.0 + 1e-9).contains(&t), "past the profile: {x:?}");
|
||||
let on = [0, 1, 2].map(|c| p0[c] + t * (p1[c] - p0[c]));
|
||||
worst = worst.max((r - polar(on).1).abs());
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
worst <= 1e-6,
|
||||
"{worst} mm off the hyperboloid (fix {analytic})"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A full turn of a half annulus about its diameter is a hollow ball with the
|
||||
/// opt-in; upstream makes the inner ball a second solid.
|
||||
#[test]
|
||||
|
||||
+8
-37
@@ -160,7 +160,7 @@ pub struct RailedLoft {
|
||||
/// A section curve as plain numbers: degree, full knots, Euclidean control points, weights
|
||||
/// (`None` for a polynomial curve).
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct Plain {
|
||||
struct Plain {
|
||||
degree: usize,
|
||||
knots: Vec<f64>,
|
||||
points: Vec<Point3>,
|
||||
@@ -361,7 +361,7 @@ fn wire_point(curves: &[(Plain, NurbCurve)], g: f64) -> Result<Point3> {
|
||||
}
|
||||
|
||||
/// A station's curves, and per rail its parameter and point there.
|
||||
pub(crate) type StationAt = (Vec<Plain>, Vec<(f64, Point3)>);
|
||||
type StationAt = (Vec<Plain>, Vec<(f64, Point3)>);
|
||||
|
||||
/// An affine move: `x` goes to `to + m (x - from)`.
|
||||
struct Affine {
|
||||
@@ -710,11 +710,11 @@ impl<'a> Loft<'a> {
|
||||
|
||||
/// A wire of the loft: a section's curves or a station's, at loft parameter `(span, f)`, and
|
||||
/// per rail the rail's parameter and point there.
|
||||
pub(crate) struct Wire {
|
||||
pub(crate) span: usize,
|
||||
pub(crate) f: f64,
|
||||
pub(crate) curves: Vec<Plain>,
|
||||
pub(crate) hits: Vec<(f64, Point3)>,
|
||||
struct Wire {
|
||||
span: usize,
|
||||
f: f64,
|
||||
curves: Vec<Plain>,
|
||||
hits: Vec<(f64, Point3)>,
|
||||
}
|
||||
|
||||
/// The loft through `sections` guided by `rails` (module docs).
|
||||
@@ -759,35 +759,6 @@ pub fn loft_rails(
|
||||
hits,
|
||||
});
|
||||
}
|
||||
skin_and_certify(
|
||||
k,
|
||||
wires,
|
||||
spans,
|
||||
rails,
|
||||
opts,
|
||||
&mut |k: &Kernel, i: usize, f: f64| loft.station(k, i, f),
|
||||
)
|
||||
}
|
||||
|
||||
/// The loft's skin-and-certificate loop (module docs, "Where the stations go, and the
|
||||
/// certificate"): from the wires `wires` (the sections, each with its rails' crossings, `spans`
|
||||
/// spans between them), a station placed by `station(k, span, f)` at each span's middle, the
|
||||
/// wires skinned, each rail bounded against the skin on every span between neighbouring wires,
|
||||
/// and a span whose bound is above the tolerance given a station at its middle, until every
|
||||
/// span is certified; past [`RailLoftOptions::max_wires`] refused, naming the bound reached.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`crate::error::sweep::SweepFailure::RailPastStationCap`] past the wire cap; those of
|
||||
/// `station`, the skin and the bound.
|
||||
pub(crate) fn skin_and_certify(
|
||||
k: &mut Kernel,
|
||||
mut wires: Vec<Wire>,
|
||||
spans: usize,
|
||||
rails: &[&Curve],
|
||||
opts: &RailLoftOptions,
|
||||
station: &mut dyn FnMut(&Kernel, usize, f64) -> Result<StationAt>,
|
||||
) -> Result<RailedLoft> {
|
||||
// one station at each span's middle
|
||||
let mut pending: Vec<(usize, f64)> = (0..spans).map(|i| (i, 0.5)).collect();
|
||||
// the largest bound the last skin proved (none before the first)
|
||||
@@ -805,7 +776,7 @@ pub(crate) fn skin_and_certify(
|
||||
.into_error());
|
||||
}
|
||||
for (i, f) in pending.drain(..) {
|
||||
let (curves, hits) = station(&*k, i, f)?;
|
||||
let (curves, hits) = loft.station(k, i, f)?;
|
||||
wires.push(Wire {
|
||||
span: i,
|
||||
f,
|
||||
|
||||
+21
-33
@@ -1,7 +1,6 @@
|
||||
//! Fixes of the fork's fillet defects. WR-300 (#1023, #421), WR-302 (#841, #1024), WR-303
|
||||
//! (#841) and WR-320 (#879) are opt-in (`Kernel::fixes`): their cases run with the field off
|
||||
//! (SMLib's behaviour) and on. WR-301 (#485) is the default: its cases run on the default
|
||||
//! kernel.
|
||||
//! Opt-in fixes of the fork's fillet defects (`Kernel::fixes`): WR-300 (#1023, #421), WR-301
|
||||
//! (#485), WR-302 (#841, #1024), WR-303 (#841) and WR-320 (#879). Each case runs with the
|
||||
//! field off (SMLib's behaviour) and on.
|
||||
#![allow(
|
||||
clippy::unwrap_used,
|
||||
clippy::expect_used,
|
||||
@@ -116,9 +115,9 @@ fn holed_cube_blend(k: &mut Kernel, size: f64, xsect: u32) -> Outcome {
|
||||
}
|
||||
|
||||
/// A blend whose setback reaches exactly the openings' rims (5): SMLib's rollover fails
|
||||
/// on the rail touching the rim's corners and running along its top edge. Joining the
|
||||
/// rail's intervals on its face first, it builds, to the closed forms 4000 - 12.5 * 20
|
||||
/// (chamfer) and 4000 - (1 - pi/4) * 25 * 20 (round).
|
||||
/// on the rail touching the rim's corners and running along its top edge. With
|
||||
/// `fillet_rail_rim_on_face` it builds, to the closed forms 4000 - 12.5 * 20 (chamfer) and
|
||||
/// 4000 - (1 - pi/4) * 25 * 20 (round).
|
||||
#[test]
|
||||
fn rail_along_its_face_rim_rolls_over_only_where_it_leaves_the_face() {
|
||||
for (xsect, expected) in [
|
||||
@@ -126,6 +125,12 @@ fn rail_along_its_face_rim_rolls_over_only_where_it_leaves_the_face() {
|
||||
(1, 4000.0 - (1.0 - std::f64::consts::FRAC_PI_4) * 500.0),
|
||||
] {
|
||||
let mut k = Kernel::default();
|
||||
assert_eq!(
|
||||
holed_cube_blend(&mut k, 5.0, xsect),
|
||||
Outcome::Refused { code: 4 },
|
||||
"xsect {xsect}: SM_FILERR_EDGE_PROBLEM (Unable to Process Rollover)"
|
||||
);
|
||||
let mut k = kernel(|f| f.fillet_rail_rim_on_face = true);
|
||||
let Outcome::Built { volume, manifold } = holed_cube_blend(&mut k, 5.0, xsect) else {
|
||||
panic!("xsect {xsect}: refused");
|
||||
};
|
||||
@@ -136,22 +141,14 @@ fn rail_along_its_face_rim_rolls_over_only_where_it_leaves_the_face() {
|
||||
}
|
||||
}
|
||||
|
||||
/// A size that leaves the rims alone (4) takes the closed forms 4000 - 8 * 20 (chamfer)
|
||||
/// and 4000 - (1 - pi/4) * 16 * 20 (round).
|
||||
/// Sizes that leave the rim alone build the same either way.
|
||||
#[test]
|
||||
fn rail_clear_of_the_rim_builds_to_the_closed_form() {
|
||||
for (xsect, expected) in [
|
||||
(0, 3840.0),
|
||||
(1, 4000.0 - (1.0 - std::f64::consts::FRAC_PI_4) * 320.0),
|
||||
] {
|
||||
fn rail_rim_on_face_leaves_other_sizes_alone() {
|
||||
for xsect in [0, 1] {
|
||||
let mut k = Kernel::default();
|
||||
let Outcome::Built { volume, manifold } = holed_cube_blend(&mut k, 4.0, xsect) else {
|
||||
panic!("xsect {xsect}: refused");
|
||||
};
|
||||
assert!(
|
||||
manifold && near(volume, expected, 1e-4),
|
||||
"xsect {xsect}: {volume}"
|
||||
);
|
||||
let off = holed_cube_blend(&mut k, 4.0, xsect);
|
||||
let mut k = kernel(|f| f.fillet_rail_rim_on_face = true);
|
||||
assert_eq!(holed_cube_blend(&mut k, 4.0, xsect), off);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,8 +259,7 @@ fn no_room_reason(
|
||||
|
||||
/// The `NoRoom` refusals name themselves (#960): a rail leaving its face (the holed cube's
|
||||
/// blend of 6) and a blend taking a whole face (the cylinder's chamfer of 10), where the
|
||||
/// comment was the only difference; a refusal under another code names none. The holed
|
||||
/// cube's chamfer of 5, once refused under another code (WR-301's rollover), builds.
|
||||
/// comment was the only difference; a refusal under another code names none.
|
||||
#[test]
|
||||
fn no_room_refusals_are_named() {
|
||||
let mut k = kernel(|f| f.fillet_refuse_rails_leaving_faces = true);
|
||||
@@ -291,17 +287,9 @@ fn no_room_refusals_are_named() {
|
||||
Some(NoRoomReason::FaceConsumed)
|
||||
);
|
||||
|
||||
// the holed cube's chamfer of 5, whose rollover failed (SM_FILERR_EDGE_PROBLEM) before
|
||||
// WR-301 became the default: 4000 - 12.5 * 20
|
||||
let mut k = Kernel::default();
|
||||
let Outcome::Built { volume, manifold } = holed_cube_blend(&mut k, 5.0, 0) else {
|
||||
panic!("size 5: refused");
|
||||
};
|
||||
assert!(manifold && near(volume, 3750.0, 1e-4), "size 5: {volume}");
|
||||
|
||||
// a chamfer of 25 on the cube of 20 (SM_FILERR_BAD_INT)
|
||||
// the rim-rollover failure of the holed cube's blend of 5 (SM_FILERR_EDGE_PROBLEM)
|
||||
let mut k = Kernel::default();
|
||||
let mut b = holed_cube(&mut k);
|
||||
let e = edge_between(&k, &b, [0.0, 0.0, 20.0], [20.0, 0.0, 20.0]);
|
||||
assert_eq!(no_room_reason(&mut k, &mut b, &[e], 25.0), None);
|
||||
assert_eq!(no_room_reason(&mut k, &mut b, &[e], 5.0), None);
|
||||
}
|
||||
|
||||
@@ -110,6 +110,7 @@ fn kernel() -> Kernel {
|
||||
k.fixes.fillet_run_on_past_far_end_only = true;
|
||||
k.fixes.fillet_run_on_bspline_trims = true;
|
||||
k.fixes.fillet_chamfer_cone_on_arc = true;
|
||||
k.fixes.fillet_torus_span_from_start_section = true;
|
||||
k.fixes.cover_wraps_across_closed_seam = true;
|
||||
k
|
||||
}
|
||||
|
||||
@@ -115,6 +115,7 @@ fn kernel() -> Kernel {
|
||||
k.fixes.fillet_run_on_past_far_end_only = true;
|
||||
k.fixes.fillet_run_on_bspline_trims = true;
|
||||
k.fixes.fillet_chamfer_cone_on_arc = true;
|
||||
k.fixes.fillet_torus_span_from_start_section = true;
|
||||
k.fixes.cover_wraps_across_closed_seam = true;
|
||||
k.fixes.bspline_check_min_radius = true;
|
||||
k.fixes.curve_check_zero_speed_tangent_end = true;
|
||||
|
||||
@@ -76,11 +76,13 @@ fn operand(k: &mut Kernel) -> Brep {
|
||||
fn solidcraft_fixes() -> UpstreamFixes {
|
||||
let mut f = UpstreamFixes::default();
|
||||
f.fillet_retry_radius_as_first = true;
|
||||
f.fillet_rail_rim_on_face = true;
|
||||
f.fillet_refuse_rails_leaving_faces = true;
|
||||
f.fillet_refuse_face_consuming_blends = true;
|
||||
f.fillet_refuse_non_manifold_result = true;
|
||||
f.fillet_corner_ball_patch = true;
|
||||
f.fillet_chamfer_cone_on_arc = true;
|
||||
f.fillet_torus_center_on_axis = true;
|
||||
f.offset_excess_faces_after_merge = true;
|
||||
f.offset_clip_right_side_out = true;
|
||||
f.revolution_offset_axis_both_sides = true;
|
||||
|
||||
@@ -303,11 +303,13 @@ fn write_issue_1818_operand() {
|
||||
fn solidcraft_fixes() -> UpstreamFixes {
|
||||
let mut f = UpstreamFixes::default();
|
||||
f.fillet_retry_radius_as_first = true;
|
||||
f.fillet_rail_rim_on_face = true;
|
||||
f.fillet_refuse_rails_leaving_faces = true;
|
||||
f.fillet_refuse_face_consuming_blends = true;
|
||||
f.fillet_refuse_non_manifold_result = true;
|
||||
f.fillet_corner_ball_patch = true;
|
||||
f.fillet_chamfer_cone_on_arc = true;
|
||||
f.fillet_torus_center_on_axis = true;
|
||||
f.offset_excess_faces_after_merge = true;
|
||||
f.offset_clip_right_side_out = true;
|
||||
f.revolution_offset_axis_both_sides = true;
|
||||
|
||||
@@ -36,6 +36,7 @@ fn sc_fixes(k: &mut Kernel) {
|
||||
k.fixes.fillet_trim_status = true;
|
||||
k.fixes.curve_loop_normal_from_area = true;
|
||||
k.fixes.fillet_retry_radius_as_first = true;
|
||||
k.fixes.fillet_rail_rim_on_face = true;
|
||||
k.fixes.fillet_refuse_rails_leaving_faces = true;
|
||||
k.fixes.fillet_refuse_face_consuming_blends = true;
|
||||
k.fixes.fillet_refuse_overlapping_bands = true;
|
||||
@@ -43,10 +44,13 @@ fn sc_fixes(k: &mut Kernel) {
|
||||
k.fixes.fillet_vertex_error_diagnostics = true;
|
||||
k.fixes.fillet_corner_ball_patch = true;
|
||||
k.fixes.fillet_corner_ball_center_from_axes = true;
|
||||
k.fixes.fillet_torus_span_from_start_section = true;
|
||||
k.fixes.fillet_chamfer_cone_on_arc = true;
|
||||
k.fixes.fillet_chamfer_cone_stored_as_cone = true;
|
||||
k.fixes.corner_blend_coons_patch_tangent_to_sides = true;
|
||||
k.fixes.corner_patch_face_side_twist = true;
|
||||
k.fixes.fillet_torus_center_on_axis = true;
|
||||
k.fixes.fillet_torus_rail_on_axis = true;
|
||||
k.fixes.fillet_patch_keeps_fillet_edges = true;
|
||||
k.fixes.fillet_trim_inside_by_best_dot = true;
|
||||
k.fixes.fillet_knife_edge_rounds = true;
|
||||
@@ -135,7 +139,9 @@ fn sc_fixes(k: &mut Kernel) {
|
||||
k.fixes.imprint_extends_edge_to_proto_vertex = true;
|
||||
k.fixes.imprint_reuses_coincident_face_edge = true;
|
||||
k.fixes.fillet_point_step_per_point = true;
|
||||
k.fixes.fillet_trace_from_tangent_point_toward_next = true;
|
||||
k.fixes.analytic_copy_keeps_parameterization = true;
|
||||
k.fixes.fillet_tangent_point_one_sided_step = true;
|
||||
k.fixes.precise_properties_body_length_scale = true;
|
||||
k.fixes.offset_extension_from_base = true;
|
||||
k.fixes.corner_blend_stays_inside_neighbours = true;
|
||||
|
||||
+2
@@ -78,11 +78,13 @@ fn drilled(k: &mut Kernel) -> Brep {
|
||||
fn solidcraft_fixes() -> UpstreamFixes {
|
||||
let mut f = UpstreamFixes::default();
|
||||
f.fillet_retry_radius_as_first = true;
|
||||
f.fillet_rail_rim_on_face = true;
|
||||
f.fillet_refuse_rails_leaving_faces = true;
|
||||
f.fillet_refuse_face_consuming_blends = true;
|
||||
f.fillet_refuse_non_manifold_result = true;
|
||||
f.fillet_corner_ball_patch = true;
|
||||
f.fillet_chamfer_cone_on_arc = true;
|
||||
f.fillet_torus_center_on_axis = true;
|
||||
f.offset_excess_faces_after_merge = true;
|
||||
f.offset_clip_right_side_out = true;
|
||||
f.revolution_offset_axis_both_sides = true;
|
||||
|
||||
+40
-19
@@ -2,7 +2,8 @@
|
||||
//! where the box's top meets the cylinder rounded r 2. The issue was about
|
||||
//! `fillet_through_extended_rail_end` (WR-468), which made the filleted edge's end at a
|
||||
//! rail ending on an extended edge a through point of the trace; that flag is removed
|
||||
//! (#2500), and these cases keep the arc's rounds.
|
||||
//! (#2500), and these cases keep the arc's rounds with and without
|
||||
//! `fillet_torus_span_from_start_section`.
|
||||
//!
|
||||
//! The case is a box 40 long, 10 wide and 15 high joined with `create_cylinder` r 10, h 30
|
||||
//! at the origin (bases coplanar), with the box along +X, -X, +Y and -Y, so the
|
||||
@@ -10,9 +11,11 @@
|
||||
//! way. At the arc's ends the section's rail on the top face lies past the box's side
|
||||
//! face (y 6 against the face's y 5), off the top plane's domain.
|
||||
//!
|
||||
//! The pinned C++ rounds the +X and +Y boxes and fails the -X and -Y boxes with 1001 at
|
||||
//! their corner (`blend.fillet_executive_p3`, `attributed-box-cylinder-arc-round-*`): its
|
||||
//! torus span takes off the arc's own start angle (WR-568). Every box rounds here.
|
||||
//! - No flag: the +X and +Y boxes round. The -X and -Y boxes fail with 1001 at their
|
||||
//! corner, and so does the pinned C++ (`blend.fillet_executive_p3`,
|
||||
//! `attributed-box-cylinder-arc-round-*`). That is WR-568, which
|
||||
//! `fillet_torus_span_from_start_section` fixes.
|
||||
//! - With `fillet_torus_span_from_start_section`, every box rounds.
|
||||
//!
|
||||
//! The rounded volume is the box-cylinder union plus the material under the round:
|
||||
//! `6000 + 3000 pi - 15 (2 (2.5 sqrt 75 + 50 asin 0.5))` plus
|
||||
@@ -41,12 +44,12 @@ use solidcore::{Brep, Kernel};
|
||||
/// The union and the round's material, in closed form (see the module docs).
|
||||
const ROUNDED_VOLUME: f64 = 13_998.813_587_665_97;
|
||||
|
||||
/// The boxes: origin and size.
|
||||
const BOXES: [([f64; 3], [f64; 3]); 4] = [
|
||||
([0.0, -5.0, 0.0], [40.0, 10.0, 15.0]),
|
||||
([-40.0, -5.0, 0.0], [40.0, 10.0, 15.0]),
|
||||
([-5.0, 0.0, 0.0], [10.0, 40.0, 15.0]),
|
||||
([-5.0, -40.0, 0.0], [10.0, 40.0, 15.0]),
|
||||
/// The boxes: origin and size, and whether the arc rounds with no flag on.
|
||||
const BOXES: [([f64; 3], [f64; 3], bool); 4] = [
|
||||
([0.0, -5.0, 0.0], [40.0, 10.0, 15.0], true),
|
||||
([-40.0, -5.0, 0.0], [40.0, 10.0, 15.0], false),
|
||||
([-5.0, 0.0, 0.0], [10.0, 40.0, 15.0], true),
|
||||
([-5.0, -40.0, 0.0], [10.0, 40.0, 15.0], false),
|
||||
];
|
||||
|
||||
fn volume(k: &Kernel, b: &mut Brep) -> f64 {
|
||||
@@ -96,16 +99,34 @@ fn round(k: &mut Kernel, origin: [f64; 3], size: [f64; 3]) -> (Brep, solidcore::
|
||||
(b, r)
|
||||
}
|
||||
|
||||
/// Every box rounds, to the closed-form volume (WR-568).
|
||||
#[test]
|
||||
fn issue_1952_every_box_rounds() {
|
||||
for (origin, size) in BOXES {
|
||||
fn check(torus_span: bool) {
|
||||
for (origin, size, rounds_unflagged) in BOXES {
|
||||
let mut k = Kernel::default();
|
||||
k.fixes.fillet_torus_span_from_start_section = torus_span;
|
||||
let (mut b, r) = round(&mut k, origin, size);
|
||||
let case = format!("box at {origin:?}");
|
||||
r.unwrap_or_else(|e| panic!("{case}: {e:?}"));
|
||||
assert!(is_manifold_solid(&b).unwrap(), "{case}");
|
||||
let v = volume(&k, &mut b);
|
||||
assert!((v - ROUNDED_VOLUME).abs() < 1e-5, "{case}: {v}");
|
||||
let case = format!("box at {origin:?}, torus span {torus_span}");
|
||||
if rounds_unflagged || torus_span {
|
||||
r.unwrap_or_else(|e| panic!("{case}: {e:?}"));
|
||||
assert!(is_manifold_solid(&b).unwrap(), "{case}");
|
||||
let v = volume(&k, &mut b);
|
||||
assert!((v - ROUNDED_VOLUME).abs() < 1e-5, "{case}: {v}");
|
||||
} else {
|
||||
assert!(
|
||||
r.as_ref().err().and_then(solidcore::error::upstream_status) == Some(1001),
|
||||
"{case}: {r:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// No flag: the pinned behaviour (the C++ fails the -X and -Y boxes too).
|
||||
#[test]
|
||||
fn issue_1952_flags_off_round_as_the_pinned_cpp() {
|
||||
check(false);
|
||||
}
|
||||
|
||||
/// The torus span fix rounds every box (WR-568).
|
||||
#[test]
|
||||
fn issue_1952_torus_span_rounds_every_box() {
|
||||
check(true);
|
||||
}
|
||||
|
||||
@@ -264,6 +264,7 @@ fn solidcraft_flags(k: &mut Kernel) {
|
||||
k.fixes.fillet_patch_keeps_fillet_edges = true;
|
||||
k.fixes.fillet_point_step_per_point = true;
|
||||
k.fixes.fillet_rail_end_on_continued_surface = true;
|
||||
k.fixes.fillet_rail_rim_on_face = true;
|
||||
k.fixes.fillet_rails_meet_at_one_end_point = true;
|
||||
k.fixes.fillet_refuse_face_consuming_blends = true;
|
||||
k.fixes.fillet_refuse_non_manifold_result = true;
|
||||
@@ -273,6 +274,11 @@ fn solidcraft_flags(k: &mut Kernel) {
|
||||
k.fixes.fillet_retry_radius_as_first = true;
|
||||
k.fixes.fillet_run_on_past_far_end_only = true;
|
||||
k.fixes.fillet_split_edge_keeps_near_half = true;
|
||||
k.fixes.fillet_tangent_point_one_sided_step = true;
|
||||
k.fixes.fillet_torus_center_on_axis = true;
|
||||
k.fixes.fillet_torus_rail_on_axis = true;
|
||||
k.fixes.fillet_torus_span_from_start_section = true;
|
||||
k.fixes.fillet_trace_from_tangent_point_toward_next = true;
|
||||
k.fixes.fillet_trace_reaches_rails = true;
|
||||
k.fixes.fillet_trim_inside_by_best_dot = true;
|
||||
k.fixes.fillet_trim_status = true;
|
||||
|
||||
@@ -392,14 +392,16 @@ fn revolution_round_crease_is_unchanged() {
|
||||
assert_eq!(on.volume.to_bits(), off.unwrap().volume.to_bits());
|
||||
}
|
||||
|
||||
/// The blend corner alone (with the SSI fix the issue's preset needs to reach the crease
|
||||
/// round): the r 1 round's End on the cylinder is a tangent end, its rail on the torus
|
||||
/// ending on the torus/cylinder seam where the round touches the cylinder. Without the
|
||||
/// tangent end (#2500) the rail end was taken as the rail's crossing of the cylinder, which
|
||||
/// has none there, and the cap edge was marched into the tangency: fillet code 5.
|
||||
/// The blend corner alone (with the two fillet fixes the issue's preset needs to reach the
|
||||
/// crease round): the r 1
|
||||
/// round's End on the cylinder is a tangent end, its rail on the torus ending on the
|
||||
/// torus/cylinder seam where the round touches the cylinder. Without the tangent end
|
||||
/// (#2500) the rail end was taken as the rail's crossing of the cylinder, which has none
|
||||
/// there, and the cap edge was marched into the tangency: fillet code 5.
|
||||
#[test]
|
||||
fn the_blend_corner_alone_rounds_the_crease_to_its_tangent_end() {
|
||||
let mut k = Kernel::default();
|
||||
k.fixes.fillet_torus_span_from_start_section = true;
|
||||
k.fixes.ssi_tangent_curve_tracer = true;
|
||||
let (code, built) = crease_round_with(k, Form::AsBuilt);
|
||||
assert_eq!(code, 0);
|
||||
|
||||
@@ -136,6 +136,7 @@ const SOLIDCRAFT_C494A18: &[&str] = &[
|
||||
"fillet_patch_keeps_fillet_edges",
|
||||
"fillet_point_step_per_point",
|
||||
"fillet_rail_end_on_continued_surface",
|
||||
"fillet_rail_rim_on_face",
|
||||
"fillet_rails_meet_at_one_end_point",
|
||||
"fillet_rail_trim_at_near_knot",
|
||||
"fillet_refuse_face_consuming_blends",
|
||||
@@ -149,6 +150,11 @@ const SOLIDCRAFT_C494A18: &[&str] = &[
|
||||
"fillet_run_on_past_far_end_only",
|
||||
"fillet_split_edge_keeps_near_half",
|
||||
"fillet_statuses_pass_on",
|
||||
"fillet_tangent_point_one_sided_step",
|
||||
"fillet_torus_center_on_axis",
|
||||
"fillet_torus_rail_on_axis",
|
||||
"fillet_torus_span_from_start_section",
|
||||
"fillet_trace_from_tangent_point_toward_next",
|
||||
"fillet_trace_reaches_rails",
|
||||
"fillet_trim_inside_by_best_dot",
|
||||
"fillet_trim_status",
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
//! tangency-labs/solidcore#417 (WR-422): a round as wide as the corner round it runs round
|
||||
//! is centred on that round's axis, where the offset faces collapse, so the rail points
|
||||
//! carry no angle and the torus could turn the long way round. The solver turns them into
|
||||
//! the half-planes through the edge's ends. The case is the fork's capi test: a
|
||||
//! 297 x 100 x 60 block with its vertical edges rounded R5, then its top rim R5.
|
||||
//! carry no angle and the torus could turn the long way round.
|
||||
//! `fixes.fillet_torus_center_on_axis` turns them into the half-planes through the edge's
|
||||
//! ends. The case is the fork's capi test: a 297 x 100 x 60 block with its vertical edges
|
||||
//! rounded R5, then its top rim R5.
|
||||
#![allow(
|
||||
clippy::unwrap_used,
|
||||
clippy::expect_used,
|
||||
@@ -47,8 +48,9 @@ fn edges_where(k: &Kernel, b: &Brep, keep: impl Fn(Point3) -> bool) -> Vec<EdgeI
|
||||
|
||||
/// The block's volume after its corners are rounded, after its top rim is rounded, and the
|
||||
/// rim round's result.
|
||||
fn rounded_block() -> (f64, f64, solidcore::Result<()>) {
|
||||
fn rounded_block(fix: bool) -> (f64, f64, solidcore::Result<()>) {
|
||||
let mut k = Kernel::default();
|
||||
k.fixes.fillet_torus_center_on_axis = fix;
|
||||
let mut block = create_box(&mut k, Point3::new(0.0, 0.0, 0.0), 297.0, 100.0, 60.0).unwrap();
|
||||
let corners = edges_where(&k, &block, |m| (m.z - 30.0).abs() < 1e-6);
|
||||
fillet_edges(&mut k, &mut block, &corners, 5.0, 1, 1, 1.0).unwrap();
|
||||
@@ -59,15 +61,19 @@ fn rounded_block() -> (f64, f64, solidcore::Result<()>) {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_top_rim_round_turns_the_right_way_round_the_corners() {
|
||||
fn the_top_rim_round_turns_the_right_way_round_the_corners_with_the_fix() {
|
||||
// each straight edge loses (1 - pi/4) 25 along 287 or 90, each corner the square
|
||||
// 5 x 5 less the quarter disc, turned a quarter about the axis: (pi/2) (62.5 - 125/3)
|
||||
let removed = 754.0 * 25.0 * (1.0 - PI / 4.0) + 4.0 * (PI / 2.0) * (62.5 - 125.0 / 3.0);
|
||||
|
||||
let (rounded, after, r) = rounded_block();
|
||||
let (rounded, after, r) = rounded_block(true);
|
||||
r.unwrap();
|
||||
assert!(
|
||||
(after - (rounded - removed)).abs() < 1e-7 * rounded,
|
||||
"{rounded} {after}"
|
||||
);
|
||||
|
||||
// upstream: the torus turns the long way round and the rollover test fails
|
||||
let (_, _, r) = rounded_block(false);
|
||||
assert!(r.is_err(), "upstream should fail: {r:?}");
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"bend_volume_trim_param_box_across_period",
|
||||
"curve_loop_normal_from_area",
|
||||
"fillet_retry_radius_as_first",
|
||||
"fillet_rail_rim_on_face",
|
||||
"fillet_refuse_rails_leaving_faces",
|
||||
"fillet_refuse_face_consuming_blends",
|
||||
"fillet_refuse_overlapping_bands",
|
||||
@@ -19,10 +20,13 @@
|
||||
"fillet_corner_ball_patch",
|
||||
"fillet_corner_ball_center_from_axes",
|
||||
"fillet_corner_variable_radius_rail_on_opposite_fillet",
|
||||
"fillet_torus_span_from_start_section",
|
||||
"fillet_chamfer_cone_on_arc",
|
||||
"fillet_chamfer_cone_stored_as_cone",
|
||||
"corner_blend_coons_patch_tangent_to_sides",
|
||||
"corner_patch_face_side_twist",
|
||||
"fillet_torus_center_on_axis",
|
||||
"fillet_torus_rail_on_axis",
|
||||
"fillet_exact_round_classes",
|
||||
"fillet_patch_keeps_fillet_edges",
|
||||
"fillet_trim_inside_by_best_dot",
|
||||
@@ -163,7 +167,9 @@
|
||||
"imprint_extends_edge_to_proto_vertex",
|
||||
"imprint_reuses_coincident_face_edge",
|
||||
"fillet_point_step_per_point",
|
||||
"fillet_trace_from_tangent_point_toward_next",
|
||||
"analytic_copy_keeps_parameterization",
|
||||
"fillet_tangent_point_one_sided_step",
|
||||
"precise_properties_body_length_scale",
|
||||
"is_topological_vertex_rejects_cusps_and_tangent_errors",
|
||||
"offset_extension_from_base",
|
||||
|
||||
+11
-12
@@ -11,8 +11,7 @@
|
||||
//! The pinned C++ (v1.0.1) fails the arc round with 1001 too. With only the fork's line added
|
||||
//! to `SmAdvSurfaceIntersector::ComputeSingularityPoint` (`SmAdvSurfaceIntersector.cpp`
|
||||
//! recompiled and linked ahead of the pinned `libSMLib.a`) it builds one manifold solid of
|
||||
//! volume 9531.3100142533 (`SmApiBrepComputeVolume` at 1e-8), the reference below; with
|
||||
//! WR-568's span as well, which the default kernel takes, 9531.3100486849. There is
|
||||
//! volume 9531.3100142533 (`SmApiBrepComputeVolume` at 1e-8), the reference below. There is
|
||||
//! no closed form: the round's ends, on the front round and into the knife edge, are traced.
|
||||
//! #1522's second case (a 20 cube, its top back edge rounded r3, then the end face's top
|
||||
//! edge r1) builds to the same volume with and without the flag, in the pinned C++ and with
|
||||
@@ -50,10 +49,6 @@ const ARC: [f64; 3] = [48.045114044109852, -5.4635486396119797, 10.0];
|
||||
|
||||
/// The pinned C++ with the fork's line: the part after both rounds.
|
||||
const BOTH_ROUNDS: f64 = 9531.3100142533;
|
||||
/// The same with WR-568's span as well, which the default kernel takes: the fork's one line
|
||||
/// in `SmFilletSolver::CalcTorusFilletGeom` (tangency-labs/smlib@985faae) recompiled and
|
||||
/// linked ahead too, `SmApiBrepComputeVolume` at 1e-8.
|
||||
const BOTH_ROUNDS_SPAN: f64 = 9531.3100486849;
|
||||
|
||||
/// The plate joined with the corner box less the cylinder, as the fork's capi test builds it.
|
||||
fn part(k: &mut Kernel) -> Brep {
|
||||
@@ -111,8 +106,13 @@ fn assert_refused(r: solidcore::Result<()>) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The arc round built one manifold solid that checks clean, of the C++ volume `want` within
|
||||
/// `tol` relative.
|
||||
/// The arc round built one manifold solid that checks clean, of the C++ volume within `tol`
|
||||
/// relative.
|
||||
fn assert_built(k: &mut Kernel, b: &mut Brep, r: solidcore::Result<()>, tol: f64) {
|
||||
assert_built_to(k, b, r, tol, BOTH_ROUNDS);
|
||||
}
|
||||
|
||||
/// [`assert_built`] against the volume `want`.
|
||||
fn assert_built_to(k: &mut Kernel, b: &mut Brep, r: solidcore::Result<()>, tol: f64, want: f64) {
|
||||
r.unwrap();
|
||||
let infinite = b.infinite_region();
|
||||
@@ -141,15 +141,14 @@ fn default_kernel_refuses_the_arc_round() {
|
||||
assert_refused(r);
|
||||
}
|
||||
|
||||
/// The default kernel with the flag: the C++ body with the fork's line and WR-568's span,
|
||||
/// [`BOTH_ROUNDS_SPAN`], to 1e-9 (the gate's tolerance; the same construction and integration
|
||||
/// on both sides).
|
||||
/// The default kernel with the flag: the C++ body with the fork's line, to 1e-9 (the gate's
|
||||
/// tolerance; the same construction and integration on both sides).
|
||||
#[test]
|
||||
fn the_flag_builds_the_arc_round() {
|
||||
let mut k = Kernel::default();
|
||||
k.fixes.ssi_singular_start_inside_direction = true;
|
||||
let (mut b, r) = both_rounds(&mut k);
|
||||
assert_built_to(&mut k, &mut b, r, 1e-9, BOTH_ROUNDS_SPAN);
|
||||
assert_built(&mut k, &mut b, r, 1e-9);
|
||||
}
|
||||
|
||||
/// Every fix with the blend corner End: the arc round's end on the front round, where the
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user