Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad3cf7ecf0 | ||
|
|
5075668363 | ||
|
|
d09b59863a | ||
|
|
685d63dd71 | ||
|
|
00e61a90aa | ||
|
|
07b2237074 | ||
|
|
5d4f1f3d01 | ||
|
|
0d4dfb570d | ||
|
|
ea81a17325 | ||
|
|
e7c61497df | ||
|
|
08da151a6e | ||
|
|
7929a747ad | ||
|
|
eac0f4d265 | ||
|
|
d6ae424de0 | ||
|
|
27ba3c4880 | ||
|
|
7ade8db130 | ||
|
|
e91023f354 | ||
|
|
303b90f7f1 | ||
|
|
875091f78a | ||
|
|
0716e4c842 | ||
|
|
ee6e1bebdc | ||
|
|
9d4f5bb124 | ||
|
|
21f442a4ad | ||
|
|
24d6f6f4cb | ||
|
|
d3e037a760 | ||
|
|
5a38423677 | ||
|
|
38113b1de7 | ||
|
|
fec9d07d43 | ||
|
|
4b09d6227f | ||
|
|
eab1e82de2 | ||
|
|
f7a8d5b406 | ||
|
|
85de3929e5 | ||
|
|
0a8c6d8bc2 | ||
|
|
a00efd15c6 | ||
|
|
06c8ab6204 | ||
|
|
cd55562c46 | ||
|
|
6d142b62e7 | ||
|
|
da2f503367 | ||
|
|
a9ac66788a | ||
|
|
4cc9f9cf01 | ||
|
|
7b1898d9fd | ||
|
|
e4d297893e | ||
|
|
0173dd8c40 | ||
|
|
cf344d773e | ||
|
|
8c51c1d527 | ||
|
|
6e48ea9ddd | ||
|
|
488f538040 | ||
|
|
17cc9f3a5a | ||
|
|
bf2c508826 | ||
|
|
f2e68b1a2a | ||
|
|
5e873e5081 | ||
|
|
3beed70eb8 | ||
|
|
73258d4264 | ||
|
|
8ae7482918 | ||
|
|
917ce5554c | ||
|
|
58ab0d5496 | ||
|
|
13b0699349 | ||
|
|
acd3a9800d | ||
|
|
03b36c1b61 | ||
|
|
b714d88185 | ||
|
|
eff3fea479 | ||
|
|
7358e90b54 | ||
|
|
3c2b0de9d2 | ||
|
|
a384f838fb | ||
|
|
1053594e9a | ||
|
|
72cde263cc | ||
|
|
b670a19aba | ||
|
|
2d9fa5c033 | ||
|
|
8be0f0ae85 | ||
|
|
00ffa4bd80 | ||
|
|
e5e0905d03 | ||
|
|
10d13793b0 | ||
|
|
70d5180438 | ||
|
|
50fe626694 | ||
|
|
ca403c92ed | ||
|
|
bab959474f | ||
|
|
27b522c155 | ||
|
|
875231c335 | ||
|
|
89810b06ca | ||
|
|
90e512b5d6 | ||
|
|
19fe382208 | ||
|
|
994c56b05f | ||
|
|
db76164ea8 | ||
|
|
58c5ed3681 | ||
|
|
05bd5c7470 | ||
|
|
2276f4ef8c | ||
|
|
188e72eb32 | ||
|
|
03306043ea | ||
|
|
28038fe0ea | ||
|
|
5b5cb8c538 | ||
|
|
1c20d18156 | ||
|
|
97920fbc90 | ||
|
|
37fb7d928a | ||
|
|
2056b359bd | ||
|
|
b4cf00292a | ||
|
|
6748dd37ab | ||
|
|
ef35c1dca1 | ||
|
|
8cdedd8752 | ||
|
|
e4ba0b166c | ||
|
|
586ac06c7e | ||
|
|
0d745fca35 | ||
|
|
5270e67abe | ||
|
|
14051845a3 | ||
|
|
d6fa937212 | ||
|
|
37aee260c1 | ||
|
|
56d64428ae | ||
|
|
0f6db7016d | ||
|
|
cbffc1171b | ||
|
|
21e034ee34 | ||
|
|
8f81f27d92 | ||
|
|
d729a7d3f2 | ||
|
|
17601916f4 |
@@ -384,7 +384,7 @@ jobs:
|
||||
osx:
|
||||
name: macOS Clang
|
||||
needs: [linux]
|
||||
runs-on: macos-14
|
||||
runs-on: macos-15
|
||||
strategy:
|
||||
fail-fast: true
|
||||
steps:
|
||||
@@ -403,7 +403,7 @@ jobs:
|
||||
- name: Prepare bext dependencies
|
||||
uses: ./brlcad/.github/actions/prepare-bext
|
||||
with:
|
||||
platform-channel: macos-14-${{ runner.arch }}-release
|
||||
platform-channel: macos-15-${{ runner.arch }}-release
|
||||
source-dir: ${{ github.workspace }}/brlcad
|
||||
output-dir: ${{ runner.temp }}/bext_output
|
||||
build-type: Release
|
||||
@@ -447,3 +447,45 @@ jobs:
|
||||
export PATH=$GITHUB_WORKSPACE:$PATH
|
||||
make package
|
||||
working-directory: ${{ github.workspace }}/build_osx
|
||||
|
||||
# BRL-CAD's STABLE branch is intended to represent the most recent
|
||||
# SHA1 that has successfully passed our primary tests on our CI
|
||||
# platforms. This job is intended to execute that update if the
|
||||
# prior tasks succeed. It's "needs" section should contain every
|
||||
# test that is not already the dependency of another tests.
|
||||
# sync-stable-branch:
|
||||
# name: Sync STABLE branch
|
||||
# runs-on: ubuntu-latest
|
||||
# needs: [archives, componentbext, windows_std_tools, osx]
|
||||
# # This job should only run on a successful push to the main branch
|
||||
# if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
# steps:
|
||||
# # 1. Generate a short-lived token using the GitHub App
|
||||
# - name: Generate GitHub App Token
|
||||
# id: generate_token
|
||||
# uses: tibdex/github-app-token@v2
|
||||
# with:
|
||||
# app_id: ${{ secrets.STABLESYNC_APP_ID }}
|
||||
# private_key: ${{ secrets.STABLESYNC_APP_PRIVATE_KEY }}
|
||||
|
||||
# # 2. Checkout the repository using the App Token
|
||||
# - name: Checkout Code
|
||||
# uses: actions/checkout@v4
|
||||
# with:
|
||||
# token: ${{ steps.generate_token.outputs.token }}
|
||||
# fetch-depth: 0 # Fetches all history to safely move branches
|
||||
|
||||
# # 3. Securely update and push the STABLE branch pointer
|
||||
# - name: Update STABLE Branch Pointer
|
||||
# run: |
|
||||
# # Configure Git to identify as the GitHub Actions bot
|
||||
# git config --global user.name "github-actions[bot]"
|
||||
# git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
# echo "Updating STABLE branch to point to the latest successfully CI tested main SHA: ${{ github.sha }}"
|
||||
|
||||
# # Force the local STABLE branch to point directly to the current main SHA
|
||||
# git branch -f STABLE ${{ github.sha }}
|
||||
|
||||
# # Push the updated stable branch reference securely back to origin
|
||||
# git push origin STABLE --force
|
||||
|
||||
@@ -15,16 +15,24 @@ on:
|
||||
schedule:
|
||||
- cron: '0 3 * * *' # daily at 03:00 UTC
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: 'The branch ref to run the conditional jobs on (e.g., refs/heads/pgo)'
|
||||
required: true
|
||||
default: 'refs/heads/main'
|
||||
type: string
|
||||
|
||||
env:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
CLANG_VERSION: 18
|
||||
|
||||
jobs:
|
||||
|
||||
linux_bext:
|
||||
name: Linux - BRL-CAD bext build
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: &ubuntu_runner ubuntu-24.04 # Define the ubuntu runner anchor here
|
||||
strategy:
|
||||
fail-fast: true
|
||||
env:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
steps:
|
||||
- name: Setup - CMake
|
||||
uses: lukka/get-cmake@latest
|
||||
@@ -177,7 +185,7 @@ jobs:
|
||||
osx_bext:
|
||||
name: macOS - BRL-CAD bext build
|
||||
needs: [linux_bext]
|
||||
runs-on: macos-14
|
||||
runs-on: macos-15
|
||||
strategy:
|
||||
fail-fast: true
|
||||
if: ${{ !cancelled() }}
|
||||
@@ -225,12 +233,13 @@ jobs:
|
||||
linux_static:
|
||||
name: Clang Static Analyser
|
||||
needs: [linux_bext]
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: *ubuntu_runner # Use the ubuntu runner alias
|
||||
strategy:
|
||||
fail-fast: true
|
||||
env:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
if: ${{ !cancelled() && github.ref == 'refs/heads/main' }}
|
||||
if: |
|
||||
!cancelled() &&
|
||||
(github.event_name == 'workflow_dispatch' && github.ref == inputs.branch ||
|
||||
github.event_name == 'schedule' && github.ref == 'refs/heads/main')
|
||||
steps:
|
||||
- name: Setup - CMake
|
||||
uses: lukka/get-cmake@latest
|
||||
@@ -239,19 +248,19 @@ jobs:
|
||||
- name: Clang Static Analyzer Setup
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install clang-18 clang-tools-18
|
||||
sudo apt-get install -y clang-${{ env.CLANG_VERSION }} clang-tools-${{ env.CLANG_VERSION }}
|
||||
# Install X/OpenGL dev pkgs
|
||||
sudo apt-get install xserver-xorg-dev libx11-dev libxi-dev libxext-dev libglu1-mesa-dev libfontconfig-dev
|
||||
sudo apt-get install -y xserver-xorg-dev libx11-dev libxi-dev libxext-dev libglu1-mesa-dev libfontconfig-dev
|
||||
# Install tools
|
||||
sudo apt-get install astyle re2c xsltproc libxml2-utils
|
||||
sudo apt-get install -y astyle re2c xsltproc libxml2-utils
|
||||
# Install dependency dev pkgs
|
||||
sudo apt-get install zlib1g-dev libpng-dev libjpeg-dev libtiff-dev libeigen3-dev libgdal-dev libassimp-dev libopencv-dev tcl-dev tk-dev
|
||||
sudo apt-get install -y zlib1g-dev libpng-dev libjpeg-dev libtiff-dev libeigen3-dev libgdal-dev libassimp-dev libopencv-dev tcl-dev tk-dev
|
||||
# Install XCB/OpenGL dev pkgs for Qt - see:
|
||||
# https://wiki.qt.io/Building_Qt_6_from_Git
|
||||
# https://doc.qt.io/qt-6/linux-requirements.html
|
||||
sudo apt-get install libfontconfig1-dev libfreetype6-dev libx11-dev libx11-xcb-dev libxext-dev libxfixes-dev libxi-dev libxrender-dev libxcb1-dev libxcb-cursor-dev libxcb-glx0-dev libxcb-keysyms1-dev libxcb-image0-dev libxcb-shm0-dev libxcb-icccm4-dev libxcb-sync-dev libxcb-xfixes0-dev libxcb-shape0-dev libxcb-randr0-dev libxcb-render-util0-dev libxcb-util-dev libxcb-xinerama0-dev libxcb-xkb-dev libxcb-xinput-dev libxcb-xrm-dev libxkbcommon-dev libxkbcommon-x11-dev
|
||||
sudo apt-get install libgl-dev
|
||||
sudo apt-get install libinput-dev # Cleanup
|
||||
sudo apt-get install -y libfontconfig1-dev libfreetype6-dev libx11-dev libx11-xcb-dev libxext-dev libxfixes-dev libxi-dev libxrender-dev libxcb1-dev libxcb-cursor-dev libxcb-glx0-dev libxcb-keysyms1-dev libxcb-image0-dev libxcb-shm0-dev libxcb-icccm4-dev libxcb-sync-dev libxcb-xfixes0-dev libxcb-shape0-dev libxcb-randr0-dev libxcb-render-util0-dev libxcb-util-dev libxcb-xinerama0-dev libxcb-xkb-dev libxcb-xinput-dev libxcb-xrm-dev libxkbcommon-dev libxkbcommon-x11-dev
|
||||
sudo apt-get install -y libgl-dev
|
||||
sudo apt-get install -y libinput-dev
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
@@ -261,7 +270,7 @@ jobs:
|
||||
- name: Prepare bext dependencies
|
||||
uses: ./brlcad/.github/actions/prepare-bext
|
||||
with:
|
||||
platform-channel: ubuntu-24.04-${{ runner.arch }}-release
|
||||
platform-channel: ubuntu-*ubuntu_version-${{ runner.arch }}-release
|
||||
source-dir: ${{ github.workspace }}/brlcad
|
||||
output-dir: ${{ runner.temp }}/bext_output
|
||||
build-type: Release
|
||||
@@ -278,7 +287,7 @@ jobs:
|
||||
env:
|
||||
BRLCAD_EXT_DIR: ${{ runner.temp }}/bext_output
|
||||
run: |
|
||||
export PATH=$GITHUB_WORKSPACE:/usr/share/clang/scan-build-18/bin:/usr/share/clang/scan-build-18/libexec:$PATH
|
||||
# The script will now receive the version number and find the tools itself
|
||||
cp $GITHUB_WORKSPACE/brlcad/misc/clang-static-analyzer-run.sh .
|
||||
./clang-static-analyzer-run.sh
|
||||
working-directory: ${{ github.workspace }}/brlcad/build_clangstatic
|
||||
@@ -290,17 +299,339 @@ jobs:
|
||||
html2text scan-reports-all/*/index.html
|
||||
working-directory: ${{ github.workspace }}/brlcad/build_clangstatic
|
||||
|
||||
linux_pgo:
|
||||
name: GCC Profile-Guided Optimization
|
||||
needs: [linux_bext]
|
||||
runs-on: *ubuntu_runner # Use the ubuntu runner alias
|
||||
timeout-minutes: 180
|
||||
strategy:
|
||||
fail-fast: true
|
||||
env:
|
||||
CMAKE_BUILD_PARALLEL_LEVEL: "2"
|
||||
PGO_PATH: ${{ github.workspace }}/pgo-profile
|
||||
PGO_BENCHMARK_TARGETS: >-
|
||||
rt pixcmp benchmark_cp BENCHMARK_PIX_cp PIX_LOG_cp
|
||||
bldg391.g m35.g moss.g sphflake.g star.g world.g
|
||||
PGO_TRAINING_TARGETS: >-
|
||||
havoc.g Generic_Twin.g NIST_MBE_PMI_2.g
|
||||
PGO_TRAINING_SIZE: "512"
|
||||
PGO_TRAINING_AZIMUTH: "35"
|
||||
PGO_TRAINING_ELEVATION: "25"
|
||||
PGO_BENCHMARK_TIMEFRAME: "8"
|
||||
PGO_BENCHMARK_MAXTIME: "60"
|
||||
PGO_BENCHMARK_AVERAGE: "3"
|
||||
PGO_BENCHMARK_DEVIATION: "3"
|
||||
PGO_MIN_SPEEDUP: "1.0"
|
||||
if: |
|
||||
!cancelled() &&
|
||||
(github.event_name == 'workflow_dispatch' && github.ref == inputs.branch ||
|
||||
github.event_name == 'schedule' && github.ref == 'refs/heads/main')
|
||||
steps:
|
||||
- name: Setup - CMake
|
||||
uses: lukka/get-cmake@latest
|
||||
|
||||
- name: Install apt dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
# Install X/OpenGL dev pkgs
|
||||
sudo apt-get install xserver-xorg-dev libx11-dev libxi-dev libxext-dev libglu1-mesa-dev libfontconfig-dev
|
||||
# Install tools
|
||||
sudo apt-get install astyle re2c xsltproc libxml2-utils
|
||||
# Install dependency dev pkgs
|
||||
sudo apt-get install zlib1g-dev libpng-dev libjpeg-dev libtiff-dev libeigen3-dev libgdal-dev libassimp-dev libopencv-dev tcl-dev tk-dev
|
||||
# Install XCB/OpenGL dev pkgs for a fallback bext build
|
||||
sudo apt-get install libfontconfig1-dev libfreetype6-dev libx11-dev libx11-xcb-dev libxext-dev libxfixes-dev libxi-dev libxrender-dev libxcb1-dev libxcb-cursor-dev libxcb-glx0-dev libxcb-keysyms1-dev libxcb-image0-dev libxcb-shm0-dev libxcb-icccm4-dev libxcb-sync-dev libxcb-xfixes0-dev libxcb-shape0-dev libxcb-randr0-dev libxcb-render-util0-dev libxcb-util-dev libxcb-xinerama0-dev libxcb-xkb-dev libxcb-xinput-dev libxcb-xrm-dev libxkbcommon-dev libxkbcommon-x11-dev
|
||||
sudo apt-get install libgl-dev libinput-dev
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
path: brlcad
|
||||
|
||||
- name: Prepare bext dependencies
|
||||
uses: ./brlcad/.github/actions/prepare-bext
|
||||
with:
|
||||
platform-channel: ubuntu-*ubuntu_version-${{ runner.arch }}-release
|
||||
source-dir: ${{ github.workspace }}/brlcad
|
||||
output-dir: ${{ runner.temp }}/bext_output
|
||||
build-type: Release
|
||||
artifact-token: ${{ secrets.BEXT_ARTIFACT_TOKEN }}
|
||||
|
||||
- name: Directory setup
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cmake -E make_directory $HOME/.cache/BRL-CAD
|
||||
if [[ -e "$PGO_PATH" ]]; then
|
||||
echo "::error::PGO profile path unexpectedly exists before the generation build"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Configure baseline build
|
||||
run: |
|
||||
cmake -S brlcad -B build_pgo_baseline -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
|
||||
-DBUILD_TESTING=ON \
|
||||
-DBRLCAD_EXT_DIR=${{ runner.temp }}/bext_output \
|
||||
-DBRLCAD_ENABLE_QT=OFF \
|
||||
-DBRLCAD_ENABLE_STEP=ON \
|
||||
-DBRLCAD_ENABLE_STRICT=ON
|
||||
|
||||
- name: Build baseline
|
||||
run: cmake --build build_pgo_baseline --target $PGO_BENCHMARK_TARGETS
|
||||
|
||||
- name: Configure profile-generation build
|
||||
run: |
|
||||
cmake -S brlcad -B build_pgo -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
|
||||
-DBUILD_TESTING=ON \
|
||||
-DBRLCAD_EXT_DIR=${{ runner.temp }}/bext_output \
|
||||
-DBRLCAD_ENABLE_QT=OFF \
|
||||
-DBRLCAD_ENABLE_STEP=ON \
|
||||
-DBRLCAD_ENABLE_STRICT=ON \
|
||||
-DBRLCAD_PGO=ON
|
||||
grep -q -- '-fprofile-generate=' build_pgo/compile_commands.json
|
||||
|
||||
- name: Build profile-generation binaries
|
||||
run: cmake --build build_pgo --target $PGO_BENCHMARK_TARGETS $PGO_TRAINING_TARGETS
|
||||
|
||||
- name: Generate profile data
|
||||
run: |
|
||||
cmake \
|
||||
-DPGO_MODE=TRAIN \
|
||||
-DPGO_BUILD_DIR="$GITHUB_WORKSPACE/build_pgo" \
|
||||
-DPGO_RESULTS_DIR="$GITHUB_WORKSPACE/pgo-training" \
|
||||
-DPGO_PROFILE_DIR="$PGO_PATH" \
|
||||
-DPGO_TIMEFRAME="$PGO_BENCHMARK_TIMEFRAME" \
|
||||
-DPGO_MAXTIME="$PGO_BENCHMARK_MAXTIME" \
|
||||
-DPGO_AVERAGE="$PGO_BENCHMARK_AVERAGE" \
|
||||
-DPGO_DEVIATION="$PGO_BENCHMARK_DEVIATION" \
|
||||
-DPGO_TRAINING_SIZE="$PGO_TRAINING_SIZE" \
|
||||
-DPGO_TRAINING_AZIMUTH="$PGO_TRAINING_AZIMUTH" \
|
||||
-DPGO_TRAINING_ELEVATION="$PGO_TRAINING_ELEVATION" \
|
||||
-P brlcad/misc/CMake/scripts/pgo_benchmark.cmake
|
||||
|
||||
- name: Configure profile-use build
|
||||
run: |
|
||||
# GCC incorporates the build path into profile filenames, so both
|
||||
# compilation passes must use the same build directory.
|
||||
cmake -E rm -rf build_pgo
|
||||
cmake -S brlcad -B build_pgo -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
|
||||
-DBUILD_TESTING=ON \
|
||||
-DBRLCAD_EXT_DIR=${{ runner.temp }}/bext_output \
|
||||
-DBRLCAD_ENABLE_QT=OFF \
|
||||
-DBRLCAD_ENABLE_STEP=ON \
|
||||
-DBRLCAD_ENABLE_STRICT=ON \
|
||||
-DBRLCAD_PGO=ON
|
||||
grep -q -- '-fprofile-use=' build_pgo/compile_commands.json
|
||||
|
||||
- name: Build profile-guided binaries
|
||||
run: cmake --build build_pgo --target $PGO_BENCHMARK_TARGETS
|
||||
|
||||
- name: Benchmark and compare profile-guided build
|
||||
run: |
|
||||
cmake \
|
||||
-DPGO_MODE=COMPARE \
|
||||
-DPGO_BASELINE_BUILD_DIR="$GITHUB_WORKSPACE/build_pgo_baseline" \
|
||||
-DPGO_OPTIMIZED_BUILD_DIR="$GITHUB_WORKSPACE/build_pgo" \
|
||||
-DPGO_RESULTS_DIR="$GITHUB_WORKSPACE/pgo-results" \
|
||||
-DPGO_TIMEFRAME="$PGO_BENCHMARK_TIMEFRAME" \
|
||||
-DPGO_MAXTIME="$PGO_BENCHMARK_MAXTIME" \
|
||||
-DPGO_AVERAGE="$PGO_BENCHMARK_AVERAGE" \
|
||||
-DPGO_DEVIATION="$PGO_BENCHMARK_DEVIATION" \
|
||||
-DPGO_MIN_SPEEDUP="$PGO_MIN_SPEEDUP" \
|
||||
-P brlcad/misc/CMake/scripts/pgo_benchmark.cmake
|
||||
|
||||
windows_pgo:
|
||||
name: MSVC Profile-Guided Optimization
|
||||
needs: [windows_bext]
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 180
|
||||
strategy:
|
||||
fail-fast: true
|
||||
env:
|
||||
CMAKE_BUILD_PARALLEL_LEVEL: "2"
|
||||
PGO_BENCHMARK_TARGETS: >-
|
||||
rt pixcmp benchmark_cp BENCHMARK_PIX_cp PIX_LOG_cp
|
||||
bldg391.g m35.g moss.g sphflake.g star.g world.g
|
||||
PGO_TRAINING_TARGETS: >-
|
||||
havoc.g Generic_Twin.g NIST_MBE_PMI_2.g
|
||||
PGO_TRAINING_SIZE: "512"
|
||||
PGO_TRAINING_AZIMUTH: "35"
|
||||
PGO_TRAINING_ELEVATION: "25"
|
||||
PGO_BENCHMARK_TIMEFRAME: "8"
|
||||
PGO_BENCHMARK_MAXTIME: "60"
|
||||
PGO_BENCHMARK_AVERAGE: "3"
|
||||
PGO_BENCHMARK_DEVIATION: "3"
|
||||
PGO_MIN_SPEEDUP: "1.0"
|
||||
if: |
|
||||
!cancelled() &&
|
||||
(github.event_name == 'workflow_dispatch' && github.ref == inputs.branch ||
|
||||
github.event_name == 'schedule' && github.ref == 'refs/heads/main')
|
||||
steps:
|
||||
- name: Setup - CMake
|
||||
uses: lukka/get-cmake@latest
|
||||
|
||||
- name: Add msbuild to PATH
|
||||
uses: microsoft/setup-msbuild@v3
|
||||
|
||||
- name: Add cl.exe to PATH
|
||||
uses: ilammy/msvc-dev-cmd@v1
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
path: brlcad
|
||||
|
||||
- name: Prepare bext dependencies
|
||||
uses: ./brlcad/.github/actions/prepare-bext
|
||||
with:
|
||||
platform-channel: windows-2025-${{ runner.arch }}-release
|
||||
source-dir: ${{ github.workspace }}/brlcad
|
||||
output-dir: ${{ runner.temp }}/bext_output
|
||||
build-type: Release
|
||||
artifact-token: ${{ secrets.BEXT_ARTIFACT_TOKEN }}
|
||||
|
||||
- name: Set up PGO paths
|
||||
shell: pwsh
|
||||
run: |
|
||||
# Short build roots keep STEP-generated paths below Windows MAX_PATH.
|
||||
$paths = @{
|
||||
PGO_PATH = Join-Path $env:RUNNER_TEMP 'pgo-d'
|
||||
PGO_BASELINE_BUILD_DIR = Join-Path $env:RUNNER_TEMP 'pgo-b'
|
||||
PGO_GENERATE_BUILD_DIR = Join-Path $env:RUNNER_TEMP 'pgo-g'
|
||||
PGO_OPTIMIZED_BUILD_DIR = Join-Path $env:RUNNER_TEMP 'pgo-u'
|
||||
PGO_TRAINING_RESULTS_DIR = Join-Path $env:RUNNER_TEMP 'pgo-t'
|
||||
PGO_RESULTS_DIR = Join-Path $env:RUNNER_TEMP 'pgo-r'
|
||||
}
|
||||
if (Test-Path -LiteralPath $paths.PGO_PATH) {
|
||||
throw "PGO profile path unexpectedly exists before the generation build: $($paths.PGO_PATH)"
|
||||
}
|
||||
foreach ($path in $paths.GetEnumerator()) {
|
||||
Add-Content -Path $env:GITHUB_ENV -Value "$($path.Key)=$($path.Value)"
|
||||
}
|
||||
|
||||
- name: Configure baseline build
|
||||
shell: pwsh
|
||||
run: |
|
||||
cmake -S brlcad -B "$env:PGO_BASELINE_BUILD_DIR" -G Ninja `
|
||||
-DCMAKE_C_COMPILER=cl.exe `
|
||||
-DCMAKE_CXX_COMPILER=cl.exe `
|
||||
-DCMAKE_BUILD_TYPE=Release `
|
||||
-DBUILD_TESTING=ON `
|
||||
"-DBRLCAD_EXT_DIR=${{ runner.temp }}/bext_output" `
|
||||
-DBRLCAD_ENABLE_QT=OFF `
|
||||
-DBRLCAD_ENABLE_STEP=ON `
|
||||
-DBRLCAD_ENABLE_STRICT=ON
|
||||
|
||||
- name: Build baseline
|
||||
shell: pwsh
|
||||
run: |
|
||||
$targets = $env:PGO_BENCHMARK_TARGETS -split '\s+'
|
||||
$arguments = @('--build', $env:PGO_BASELINE_BUILD_DIR, '--target') + $targets
|
||||
cmake @arguments
|
||||
|
||||
- name: Configure profile-generation build
|
||||
shell: pwsh
|
||||
run: |
|
||||
cmake -S brlcad -B "$env:PGO_GENERATE_BUILD_DIR" -G Ninja `
|
||||
-DCMAKE_C_COMPILER=cl.exe `
|
||||
-DCMAKE_CXX_COMPILER=cl.exe `
|
||||
-DCMAKE_BUILD_TYPE=Release `
|
||||
-DBUILD_TESTING=ON `
|
||||
"-DBRLCAD_EXT_DIR=${{ runner.temp }}/bext_output" `
|
||||
-DBRLCAD_ENABLE_QT=OFF `
|
||||
-DBRLCAD_ENABLE_STEP=ON `
|
||||
-DBRLCAD_ENABLE_STRICT=ON `
|
||||
-DBRLCAD_PGO=ON
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
if (-not (Select-String -Path "$env:PGO_GENERATE_BUILD_DIR/build.ninja" -SimpleMatch '/GENPROFILE:PGD=' -Quiet)) {
|
||||
throw 'The profile-generation build does not contain /GENPROFILE.'
|
||||
}
|
||||
|
||||
- name: Build profile-generation binaries
|
||||
shell: pwsh
|
||||
run: |
|
||||
$targets = ($env:PGO_BENCHMARK_TARGETS + ' ' + $env:PGO_TRAINING_TARGETS) -split '\s+'
|
||||
$arguments = @('--build', $env:PGO_GENERATE_BUILD_DIR, '--target') + $targets
|
||||
cmake @arguments
|
||||
|
||||
- name: Generate profile data
|
||||
shell: pwsh
|
||||
run: |
|
||||
cmake `
|
||||
-DPGO_MODE=TRAIN `
|
||||
"-DPGO_BUILD_DIR=$env:PGO_GENERATE_BUILD_DIR" `
|
||||
"-DPGO_RESULTS_DIR=$env:PGO_TRAINING_RESULTS_DIR" `
|
||||
"-DPGO_PROFILE_DIR=$env:PGO_PATH" `
|
||||
"-DPGO_TIMEFRAME=$env:PGO_BENCHMARK_TIMEFRAME" `
|
||||
"-DPGO_MAXTIME=$env:PGO_BENCHMARK_MAXTIME" `
|
||||
"-DPGO_AVERAGE=$env:PGO_BENCHMARK_AVERAGE" `
|
||||
"-DPGO_DEVIATION=$env:PGO_BENCHMARK_DEVIATION" `
|
||||
"-DPGO_TRAINING_SIZE=$env:PGO_TRAINING_SIZE" `
|
||||
"-DPGO_TRAINING_AZIMUTH=$env:PGO_TRAINING_AZIMUTH" `
|
||||
"-DPGO_TRAINING_ELEVATION=$env:PGO_TRAINING_ELEVATION" `
|
||||
-P brlcad/misc/CMake/scripts/pgo_benchmark.cmake
|
||||
|
||||
- name: Configure profile-use build
|
||||
shell: pwsh
|
||||
run: |
|
||||
cmake -S brlcad -B "$env:PGO_OPTIMIZED_BUILD_DIR" -G Ninja `
|
||||
-DCMAKE_C_COMPILER=cl.exe `
|
||||
-DCMAKE_CXX_COMPILER=cl.exe `
|
||||
-DCMAKE_BUILD_TYPE=Release `
|
||||
-DBUILD_TESTING=ON `
|
||||
"-DBRLCAD_EXT_DIR=${{ runner.temp }}/bext_output" `
|
||||
-DBRLCAD_ENABLE_QT=OFF `
|
||||
-DBRLCAD_ENABLE_STEP=ON `
|
||||
-DBRLCAD_ENABLE_STRICT=ON `
|
||||
-DBRLCAD_PGO=ON
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
if (-not (Select-String -Path "$env:PGO_OPTIMIZED_BUILD_DIR/build.ninja" -SimpleMatch '/USEPROFILE:PGD=' -Quiet)) {
|
||||
throw 'The profile-use build does not contain /USEPROFILE.'
|
||||
}
|
||||
|
||||
- name: Build profile-guided binaries
|
||||
shell: pwsh
|
||||
run: |
|
||||
$targets = $env:PGO_BENCHMARK_TARGETS -split '\s+'
|
||||
$arguments = @('--build', $env:PGO_OPTIMIZED_BUILD_DIR, '--target') + $targets
|
||||
cmake @arguments
|
||||
|
||||
- name: Benchmark and compare profile-guided build
|
||||
shell: pwsh
|
||||
run: |
|
||||
cmake `
|
||||
-DPGO_MODE=COMPARE `
|
||||
"-DPGO_BASELINE_BUILD_DIR=$env:PGO_BASELINE_BUILD_DIR" `
|
||||
"-DPGO_OPTIMIZED_BUILD_DIR=$env:PGO_OPTIMIZED_BUILD_DIR" `
|
||||
"-DPGO_RESULTS_DIR=$env:PGO_RESULTS_DIR" `
|
||||
"-DPGO_TIMEFRAME=$env:PGO_BENCHMARK_TIMEFRAME" `
|
||||
"-DPGO_MAXTIME=$env:PGO_BENCHMARK_MAXTIME" `
|
||||
"-DPGO_AVERAGE=$env:PGO_BENCHMARK_AVERAGE" `
|
||||
"-DPGO_DEVIATION=$env:PGO_BENCHMARK_DEVIATION" `
|
||||
"-DPGO_MIN_SPEEDUP=$env:PGO_MIN_SPEEDUP" `
|
||||
-P brlcad/misc/CMake/scripts/pgo_benchmark.cmake
|
||||
|
||||
|
||||
# See https://app.codecov.io/gh/BRL-CAD/brlcad for analysis results
|
||||
linux_cov:
|
||||
name: LCOV Coverage testing
|
||||
needs: [linux_bext]
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: *ubuntu_runner # Use the ubuntu runner alias
|
||||
strategy:
|
||||
fail-fast: true
|
||||
env:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
if: ${{ !cancelled() && github.ref == 'refs/heads/main' }}
|
||||
if: |
|
||||
!cancelled() &&
|
||||
(github.event_name == 'workflow_dispatch' && github.ref == inputs.branch ||
|
||||
github.event_name == 'schedule' && github.ref == 'refs/heads/main')
|
||||
steps:
|
||||
- name: Setup - CMake
|
||||
uses: lukka/get-cmake@latest
|
||||
@@ -331,7 +662,7 @@ jobs:
|
||||
- name: Prepare bext dependencies
|
||||
uses: ./brlcad/.github/actions/prepare-bext
|
||||
with:
|
||||
platform-channel: ubuntu-24.04-${{ runner.arch }}-release
|
||||
platform-channel: ubuntu-*ubuntu_version-${{ runner.arch }}-release
|
||||
source-dir: ${{ github.workspace }}/brlcad
|
||||
output-dir: ${{ runner.temp }}/bext_output
|
||||
build-type: Release
|
||||
|
||||
@@ -231,7 +231,7 @@ set(CMAKE_FIND_USE_PACKAGE_REGISTRY FALSE)
|
||||
# Prepare to manage and build BRL-CAD's external dependencies. Specifies a
|
||||
# specific commit hash to use in the 'bext' repository.
|
||||
include(BRLCAD_ExternalDeps)
|
||||
brlcad_bext_init(12115e6c5922e2db27a9b88e00dd714af5315f69)
|
||||
brlcad_bext_init(5ccf9b577dbccef244e2fea7e2741027f0c83555)
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
# Define various utilities.
|
||||
@@ -352,9 +352,10 @@ set(CMAKE_SKIP_BUILD_RPATH FALSE)
|
||||
# it should only be set to the installation value when actually installed.
|
||||
set(CMAKE_BUILD_WITH_INSTALL_RPATH FALSE)
|
||||
|
||||
# Add the automatically determined parts of the RPATH which point to
|
||||
# directories outside the build tree to the install RPATH
|
||||
set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)
|
||||
# Installed runtime paths are defined explicitly below. Do not append
|
||||
# directories discovered on the build host, since those paths may not exist
|
||||
# or may resolve to unrelated libraries on the installed system.
|
||||
set(CMAKE_INSTALL_RPATH_USE_LINK_PATH FALSE)
|
||||
|
||||
# The following logic is what allows binaries to run successfully in the build
|
||||
# directory AND install directory.
|
||||
@@ -678,8 +679,13 @@ endif(MSVC)
|
||||
# before use so platforms without GNU/LLVM-style linker options are skipped.
|
||||
brlcad_check_linker_flag(C "LINKER:--no-undefined" BRLCAD_LINKER_NO_UNDEFINED_C_FLAG_FOUND)
|
||||
brlcad_check_linker_flag(CXX "LINKER:--no-undefined" BRLCAD_LINKER_NO_UNDEFINED_CXX_FLAG_FOUND)
|
||||
if(BRLCAD_LINKER_NO_UNDEFINED_C_FLAG_FOUND AND BRLCAD_LINKER_NO_UNDEFINED_CXX_FLAG_FOUND)
|
||||
if(BRLCAD_LINKER_NO_UNDEFINED_C_FLAG_FOUND AND BRLCAD_LINKER_NO_UNDEFINED_CXX_FLAG_FOUND
|
||||
AND NOT BRLCAD_ENABLE_ADDRESS_SANITIZER)
|
||||
set(BRLCAD_LINKER_NO_UNDEFINED_FLAG "LINKER:--no-undefined" CACHE STRING "Linker flag requiring resolved shared library symbols" FORCE)
|
||||
else()
|
||||
# Clang links the AddressSanitizer runtime into executables, leaving its
|
||||
# shared-library instrumentation symbols unresolved until then.
|
||||
set(BRLCAD_LINKER_NO_UNDEFINED_FLAG "" CACHE STRING "Linker flag requiring resolved shared library symbols" FORCE)
|
||||
endif()
|
||||
brlcad_check_linker_flags(C BRLCAD_LINKER_WHOLE_ARCHIVE_GROUP_C_FLAG_FOUND "LINKER:--whole-archive" "LINKER:--no-whole-archive")
|
||||
brlcad_check_linker_flags(CXX BRLCAD_LINKER_WHOLE_ARCHIVE_GROUP_CXX_FLAG_FOUND "LINKER:--whole-archive" "LINKER:--no-whole-archive")
|
||||
@@ -1332,6 +1338,90 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
brlcad_deferred_define("BRLCAD_HAS_BIG_STACK 1")
|
||||
endif()
|
||||
|
||||
# Profile-Guided Optimization uses a shared profile location for both compiler
|
||||
# families. A path that does not exist selects the instrumentation pass; an
|
||||
# existing path selects the profile-use pass in a fresh build tree.
|
||||
if("$ENV{PGO_PATH}" STREQUAL "")
|
||||
set(BRLCAD_PGO_PATH "${CMAKE_BINARY_DIR}/profiling")
|
||||
else()
|
||||
file(TO_CMAKE_PATH "$ENV{PGO_PATH}" BRLCAD_PGO_PATH)
|
||||
endif()
|
||||
|
||||
# MSVC creates one profile database per linked image. The target-dependent
|
||||
# generator expression gives every executable and DLL a distinct database,
|
||||
# while /GL also covers code contributed through object and static libraries.
|
||||
if(MSVC AND BRLCAD_PGO)
|
||||
if(DEFINED BRLCAD_MSVC_PGO_PHASE)
|
||||
if(NOT "${BRLCAD_MSVC_PGO_PATH}" STREQUAL "${BRLCAD_PGO_PATH}")
|
||||
message(
|
||||
FATAL_ERROR
|
||||
"PGO_PATH changed from '${BRLCAD_MSVC_PGO_PATH}' to '${BRLCAD_PGO_PATH}'. "
|
||||
"Use a fresh build directory when changing PGO phases or profile locations."
|
||||
)
|
||||
endif()
|
||||
elseif(EXISTS "${BRLCAD_PGO_PATH}")
|
||||
set(BRLCAD_MSVC_PGO_PHASE USE)
|
||||
else()
|
||||
set(BRLCAD_MSVC_PGO_PHASE GENERATE)
|
||||
endif()
|
||||
|
||||
if(NOT BRLCAD_MSVC_PGO_PHASE MATCHES "^(GENERATE|USE)$")
|
||||
message(FATAL_ERROR "Invalid internal MSVC PGO phase '${BRLCAD_MSVC_PGO_PHASE}'")
|
||||
endif()
|
||||
set(BRLCAD_MSVC_PGO_PHASE "${BRLCAD_MSVC_PGO_PHASE}" CACHE INTERNAL "MSVC PGO build phase" FORCE)
|
||||
set(BRLCAD_MSVC_PGO_PATH "${BRLCAD_PGO_PATH}" CACHE INTERNAL "MSVC PGO profile path" FORCE)
|
||||
|
||||
if(BRLCAD_MSVC_PGO_PHASE STREQUAL "GENERATE")
|
||||
# link.exe requires the PGD parent directory to exist before it links the
|
||||
# instrumented image. Cache the phase above so an in-place CMake rerun
|
||||
# does not mistake this newly created directory for completed profiles.
|
||||
file(MAKE_DIRECTORY "${BRLCAD_PGO_PATH}")
|
||||
set(_brlcad_msvc_pgo_link_mode GENPROFILE)
|
||||
message(
|
||||
STATUS
|
||||
"MSVC PGO generation enabled; set VCPROFILE_PATH=${BRLCAD_PGO_PATH} while running training workloads"
|
||||
)
|
||||
distclean("${BRLCAD_PGO_PATH}")
|
||||
else()
|
||||
message(STATUS "MSVC PGO profile use enabled from ${BRLCAD_PGO_PATH}")
|
||||
endif()
|
||||
|
||||
add_compile_options("$<$<COMPILE_LANGUAGE:C,CXX>:/GL>")
|
||||
add_link_options(/LTCG)
|
||||
if(_brlcad_msvc_pgo_link_mode)
|
||||
add_link_options(
|
||||
"/${_brlcad_msvc_pgo_link_mode}:PGD=${BRLCAD_PGO_PATH}/$<TARGET_PROPERTY:NAME>.pgd"
|
||||
)
|
||||
endif()
|
||||
unset(_brlcad_msvc_pgo_link_mode)
|
||||
endif()
|
||||
|
||||
# Apply profile-use only to images exercised during training. link.exe emits
|
||||
# C4961 for an untrained PGD, and strict builds promote that warning to an
|
||||
# error during LTCG. The generation pass names PGC files after the target's
|
||||
# PGD, so this check also works when OUTPUT_NAME changes the binary name.
|
||||
function(_BRLCAD_MSVC_PGO_APPLY_PROFILES directory)
|
||||
get_property(_brlcad_pgo_targets DIRECTORY "${directory}" PROPERTY BUILDSYSTEM_TARGETS)
|
||||
foreach(_brlcad_pgo_target IN LISTS _brlcad_pgo_targets)
|
||||
get_target_property(_brlcad_pgo_target_type ${_brlcad_pgo_target} TYPE)
|
||||
if(_brlcad_pgo_target_type MATCHES "^(EXECUTABLE|SHARED_LIBRARY|MODULE_LIBRARY)$")
|
||||
set(_brlcad_pgo_database "${BRLCAD_PGO_PATH}/${_brlcad_pgo_target}.pgd")
|
||||
file(GLOB _brlcad_pgo_counts LIST_DIRECTORIES FALSE "${BRLCAD_PGO_PATH}/${_brlcad_pgo_target}![0-9]*.pgc")
|
||||
if(EXISTS "${_brlcad_pgo_database}" AND _brlcad_pgo_counts)
|
||||
target_link_options(${_brlcad_pgo_target} PRIVATE "/USEPROFILE:PGD=${_brlcad_pgo_database}")
|
||||
set_property(GLOBAL APPEND PROPERTY BRLCAD_MSVC_PGO_TRAINED_TARGETS ${_brlcad_pgo_target})
|
||||
endif()
|
||||
unset(_brlcad_pgo_counts)
|
||||
unset(_brlcad_pgo_database)
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
get_property(_brlcad_pgo_subdirectories DIRECTORY "${directory}" PROPERTY SUBDIRECTORIES)
|
||||
foreach(_brlcad_pgo_subdirectory IN LISTS _brlcad_pgo_subdirectories)
|
||||
_brlcad_msvc_pgo_apply_profiles("${_brlcad_pgo_subdirectory}")
|
||||
endforeach()
|
||||
endfunction()
|
||||
|
||||
# The following tests are almost entirely for gcc/llvm style flags, so
|
||||
# we don't unnecessarily slow down the Windows build.
|
||||
if(NOT MSVC)
|
||||
@@ -1394,11 +1484,6 @@ if(NOT MSVC)
|
||||
# variable set to a temp folder
|
||||
|
||||
set(pgo_flags_found "")
|
||||
if("$ENV{PGO_PATH}" STREQUAL "")
|
||||
set(BRLCAD_PGO_PATH "${CMAKE_BINARY_DIR}/profiling")
|
||||
else(NOT "$ENV{PGO_PATH}" STREQUAL "")
|
||||
set(BRLCAD_PGO_PATH "$ENV{PGO_PATH}")
|
||||
endif("$ENV{PGO_PATH}" STREQUAL "")
|
||||
|
||||
if(BRLCAD_PGO AND NOT EXISTS "${BRLCAD_PGO_PATH}")
|
||||
if("${CMAKE_C_FLAGS}" MATCHES ".*fprofile-use.*")
|
||||
@@ -1444,7 +1529,8 @@ if(NOT MSVC)
|
||||
# gcc-style USE
|
||||
if("${pgo_flags_found}" STREQUAL "")
|
||||
cmake_push_check_state()
|
||||
set(flags "fprofile-use=\"${BRLCAD_PGO_PATH}\" -Wno-error=coverage-mismatch -fprofile-correction")
|
||||
set(flags "fprofile-use=\"${BRLCAD_PGO_PATH}\" -Wno-error=coverage-mismatch")
|
||||
string(APPEND flags " -Wno-error=missing-profile -fprofile-correction")
|
||||
set(CMAKE_REQUIRED_FLAGS "-${flags}")
|
||||
check_c_flag("${flags}" GROUPS OPTIMIZE_C_FLAGS VARS pgo_flags_found)
|
||||
check_cxx_flag("${flags}" GROUPS OPTIMIZE_CXX_FLAGS)
|
||||
@@ -1642,21 +1728,31 @@ if(NOT MSVC)
|
||||
endif(BRLCAD_WARNINGS OR BRLCAD_ENABLE_STRICT)
|
||||
|
||||
if(BRLCAD_ENABLE_COVERAGE)
|
||||
# TODO: These seem to GCC specific flags - should probably set up for clang
|
||||
# as well if that's our compiler...
|
||||
# https://clang.llvm.org/docs/SourceBasedCodeCoverage.html
|
||||
# If we need different settings for different tools, should
|
||||
# encapsulate each tool's setup in a function
|
||||
check_c_flag(ftest-coverage)
|
||||
check_cxx_flag(ftest-coverage)
|
||||
if(FTEST_COVERAGE_C_FLAG_FOUND)
|
||||
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -ftest-coverage -fprofile-arcs -fprofile-update=atomic")
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -ftest-coverage -fprofile-arcs -fprofile-update=atomic")
|
||||
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -lgcov -ftest-coverage -fprofile-arcs -fprofile-update=atomic")
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -lgcov -ftest-coverage -fprofile-arcs -fprofile-update=atomic")
|
||||
else(FTEST_COVERAGE_C_FLAG_FOUND)
|
||||
message(SEND_ERROR "Building with coverage is not supported by BRL-CAD on this platform.")
|
||||
endif(FTEST_COVERAGE_C_FLAG_FOUND)
|
||||
if(CMAKE_C_COMPILER_ID MATCHES "Clang" AND CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
||||
check_c_flag(fprofile-instr-generate)
|
||||
check_cxx_flag(fprofile-instr-generate)
|
||||
check_c_flag(fcoverage-mapping)
|
||||
check_cxx_flag(fcoverage-mapping)
|
||||
if(NOT FPROFILE_INSTR_GENERATE_C_FLAG_FOUND OR
|
||||
NOT FPROFILE_INSTR_GENERATE_CXX_FLAG_FOUND OR
|
||||
NOT FCOVERAGE_MAPPING_C_FLAG_FOUND OR
|
||||
NOT FCOVERAGE_MAPPING_CXX_FLAG_FOUND)
|
||||
message(FATAL_ERROR "Clang source-based coverage is not supported by this toolchain.")
|
||||
endif()
|
||||
add_compile_options(-fprofile-instr-generate -fcoverage-mapping)
|
||||
add_link_options(-fprofile-instr-generate)
|
||||
else()
|
||||
check_c_flag(ftest-coverage)
|
||||
check_cxx_flag(ftest-coverage)
|
||||
if(FTEST_COVERAGE_C_FLAG_FOUND AND FTEST_COVERAGE_CXX_FLAG_FOUND)
|
||||
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -ftest-coverage -fprofile-arcs -fprofile-update=atomic")
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -ftest-coverage -fprofile-arcs -fprofile-update=atomic")
|
||||
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -lgcov -ftest-coverage -fprofile-arcs -fprofile-update=atomic")
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -lgcov -ftest-coverage -fprofile-arcs -fprofile-update=atomic")
|
||||
else()
|
||||
message(FATAL_ERROR "GCC coverage is not supported by this toolchain.")
|
||||
endif()
|
||||
endif()
|
||||
endif(BRLCAD_ENABLE_COVERAGE)
|
||||
|
||||
if(BRLCAD_ENABLE_STRICT)
|
||||
@@ -2957,6 +3053,24 @@ verbose_add_subdirectory("" doc)
|
||||
# NOTE: misc/tools is added separately (earlier)
|
||||
verbose_add_subdirectory("" misc)
|
||||
|
||||
if(MSVC AND BRLCAD_PGO AND BRLCAD_MSVC_PGO_PHASE STREQUAL "USE")
|
||||
set_property(GLOBAL PROPERTY BRLCAD_MSVC_PGO_TRAINED_TARGETS)
|
||||
_brlcad_msvc_pgo_apply_profiles("${CMAKE_SOURCE_DIR}")
|
||||
get_property(_brlcad_msvc_pgo_targets GLOBAL PROPERTY BRLCAD_MSVC_PGO_TRAINED_TARGETS)
|
||||
list(LENGTH _brlcad_msvc_pgo_targets _brlcad_msvc_pgo_target_count)
|
||||
if(_brlcad_msvc_pgo_target_count EQUAL 0)
|
||||
message(
|
||||
FATAL_ERROR
|
||||
"No MSVC PGO counter files were found in ${BRLCAD_PGO_PATH}. "
|
||||
"Set VCPROFILE_PATH to that directory while running the training workloads "
|
||||
"before configuring the profile-use build."
|
||||
)
|
||||
endif()
|
||||
message(STATUS "MSVC PGO will use training data for ${_brlcad_msvc_pgo_target_count} linked targets")
|
||||
unset(_brlcad_msvc_pgo_target_count)
|
||||
unset(_brlcad_msvc_pgo_targets)
|
||||
endif()
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# CMake package configuration files for modern find_package(BRLCAD CONFIG)
|
||||
#
|
||||
|
||||
@@ -10,78 +10,108 @@ descriptive text that emphasizes or further describes significant
|
||||
changes made. See document footer for additional details.
|
||||
|
||||
----------------------------------------------------------------------
|
||||
--- 20XX-XX-XX Release 7.46.0 ---
|
||||
--- 2026-10-0X Release 7.46.0 ---
|
||||
----------------------------------------------------------------------
|
||||
|
||||
* updated MGED and Archer for Tcl 8.6.18 and Itcl/Itk 4 - Cliff Yapp
|
||||
This release expands geometry conversion and improves modeling
|
||||
reliability. STEP support adds AP203e2, AP214, and AP242
|
||||
import/export, native CSG round trips, and AP242 product and
|
||||
manufacturing information (PMI) import. IGES and JT conversion is
|
||||
expanded, and new OpenSCAD CSG import/export tools are available.
|
||||
|
||||
Facetization is more reliable, NMG solid ray tracing is 10-20 times
|
||||
faster, CSG ray tracing is faster, and BoT, DSP, and point-cloud ray
|
||||
intersections are corrected. MGED gains numerous fixes to primitive,
|
||||
sketch, and combination editing, drawing, dialogs, and preferences.
|
||||
|
||||
New tools support object nesting with arrange, building generation
|
||||
with mkbuilding, annotation editing, and primitive repair. Geometry
|
||||
analysis, image utilities, Windows support, documentation, and bundled
|
||||
dependencies also receive substantial updates.
|
||||
|
||||
* corrected Archer logic for saving window size - Cliff Yapp
|
||||
* fixed inside BoT rays, DSP surface normals, PNTS bounds - Cliff Yapp
|
||||
* improved mged fallback HTML manual viewer - Cliff Yapp
|
||||
* fixed gqa undefined densities and exit status - Chris McGregor
|
||||
* added ARBN support to the mged inside command - Cliff Yapp
|
||||
* improved HYP neck-ratio labels and BoT face selection - Cliff Yapp
|
||||
* fixed mged view panning, edit rotation, & ray queries - Cliff Yapp
|
||||
* preserved ARB, CLINE, TGC, & extrusion geometry edits - Cliff Yapp
|
||||
* restored mged command prompt focus at GUI startup - Cliff Yapp
|
||||
* new documentation on geometry representation schemes - Sean Morrison
|
||||
* improved pixscale & bwscale scaling and image piping - Sean Morrison
|
||||
* restored pixdiff behavior on .pix, added -b for .bw - Sean Morrison
|
||||
* improved facetize fallback methods and reliability - Cliff Yapp
|
||||
* updated mged and Archer for Tcl 8.6.18 and Itcl/Itk 4 - Cliff Yapp
|
||||
* fixed writes to fileless geometry databases - Cliff Yapp
|
||||
* fixed nastran-g handling of large property IDs - Rupesh, Cliff Yapp
|
||||
* fixed dxf-g invalid polyface index crashes - Meghna45-code, Cliff Yapp
|
||||
* fixed Archer primitive editors under Itcl 4 - Cliff Yapp
|
||||
* fixed Archer shutdown menu cleanup errors - Cliff Yapp
|
||||
* fixed MGED Build Pattern dialog under Itcl 4 - Cliff Yapp
|
||||
* fixed dxf-g invalid polyface idx crash - Meghna45-code, Cliff Yapp
|
||||
* fixed TclCAD framebuffer listing and deletion - Cliff Yapp
|
||||
* fixed MGED Build Pattern progress updates - Cliff Yapp
|
||||
* fixed MGED search -exec reentry and Ctrl-C crashes - Cliff Yapp
|
||||
* fixed MGED asynchronous commands and clone progress - Cliff Yapp
|
||||
* accelerated large and repeated MGED path drawing - Cliff Yapp
|
||||
* fixed mged Build Pattern progress updates - Cliff Yapp
|
||||
* fixed mged search -exec reentry and Ctrl-C crashes - Cliff Yapp
|
||||
* fixed mged asynchronous commands and clone progress - Cliff Yapp
|
||||
* accelerated large and repeated mged path drawing - Cliff Yapp
|
||||
* fixed ARB7 inside solid construction - Cliff Yapp
|
||||
* fixed direct ARB face and edge-direction editing - Cliff Yapp
|
||||
* fixed rtwizard startup with framebuffer contention - Cliff Yapp
|
||||
* fixed Archer annotation line display updates - Cliff Yapp
|
||||
* fixed Archer sketch scale drift in non-mm databases - Cliff Yapp
|
||||
* fixed toggle arrow callbacks during setup - Cliff Yapp
|
||||
* fixed Tcl dialogs with multi-word titles and messages - Cliff Yapp
|
||||
* fixed Tcl dialogs with multi-word titles and msgs - Cliff Yapp
|
||||
* fixed Archer Tank Wizard gun generation - Cliff Yapp
|
||||
* fixed MGED and Archer sketch arc tangency editing - Cliff Yapp
|
||||
* fixed mged and Archer sketch arc tangency editing - Cliff Yapp
|
||||
* fixed Archer ARB4 point 3 editing - Cliff Yapp
|
||||
* fixed ARB4 storage from make and mk_arb4 - Cliff Yapp
|
||||
* fixed ARB4 and ARB6 center point calculations - Cliff Yapp
|
||||
* fixed concurrent MGED and rtwizard framebuffer use - Cliff Yapp
|
||||
* fixed MGED ShotVis editing and reload failures - Cliff Yapp
|
||||
* fixed concurrent mged and rtwizard framebuffer use - Cliff Yapp
|
||||
* fixed mged ShotVis editing and reload failures - Cliff Yapp
|
||||
* fixed Tcl widget geometry and selection defects - Cliff Yapp
|
||||
* fixed MGED BoT editor property handling - Cliff Yapp
|
||||
* fixed MGED LOD dialog and adaptive BoT wireframes - Cliff Yapp
|
||||
* fixed MGED Plot, PostScript, and manual launch tools - Cliff Yapp
|
||||
* fixed MGED Overlap Tool multi-object result handling - Cliff Yapp
|
||||
* fixed mged BoT editor property handling - Cliff Yapp
|
||||
* fixed mged LOD dialog and adaptive BoT wireframes - Cliff Yapp
|
||||
* fixed mged Plot, PostScript, and manual launch tools - Cliff Yapp
|
||||
* fixed mged Overlap Tool multi-object result handling - Cliff Yapp
|
||||
* eliminated 16-fold redundant gchecker grid analysis - Cliff Yapp
|
||||
* fixed Archer tree handling for object names with spaces - Cliff Yapp
|
||||
* fixed submodel transforms & relative database paths - Sean Morrison
|
||||
* fixed Archer tree handling for names with spaces - Cliff Yapp
|
||||
* fixed RtWizard multi-object edge occlusion handling - Cliff Yapp
|
||||
* fixed RtWizard title, About, and preference handling - Cliff Yapp
|
||||
* fixed MGED startup file false legacy-syntax warnings - Cliff Yapp
|
||||
* fixed MGED saved-view deletion and collaboration sync - Cliff Yapp
|
||||
* fixed MGED font preferences apply and persistence - Cliff Yapp
|
||||
* fixed mged startup file false legacy-syntax warnings - Cliff Yapp
|
||||
* fixed mged saved-view deletion and collaboration sync - Cliff Yapp
|
||||
* fixed mged font preferences apply and persistence - Cliff Yapp
|
||||
* fixed Archer combination edits changing Boolean trees - Cliff Yapp
|
||||
* fixed Archer command-line help startup crash - Cliff Yapp
|
||||
* fixed MGED Geometry Browser menu and close behavior - Cliff Yapp
|
||||
* fixed MGED collaboration view unshare crash - Cliff Yapp
|
||||
* added an override for Archer's preference file location - Cliff Yapp
|
||||
* fixed mged Geometry Browser menu and close behavior - Cliff Yapp
|
||||
* fixed mged collaboration view unshare crash - Cliff Yapp
|
||||
* added override for Archer's preference file location - Cliff Yapp
|
||||
* fixed embedded Archer display and mode menus - Cliff Yapp
|
||||
* added Archer -h and --help command-line options - Cliff Yapp
|
||||
* fixed Archer Help opening the documentation menu - Sean Morrison
|
||||
* added arrange nest command for packing database objects - Sean Morrison
|
||||
* new mkbuilding tool for parametric/OSM/CityJSON models - Sean Morrison
|
||||
* added arrange nest command for packing db objects - Sean Morrison
|
||||
* added mkbuilding for parametric/OSM/CityJSON models - Sean Morrison
|
||||
* cross-platform BINUNIF and DSP byte-order handling - Sean Morrison
|
||||
* improved several cv default behaviors (s, 32, 64, n) - Sean Morrison
|
||||
* improved several cv default behaviors (s,32,64,n) - Sean Morrison
|
||||
* fixed invalid bot handling bug applying matrices - Sean Morrison
|
||||
* improved glTF import robustness via gcv - Sean Morrison
|
||||
* fixed bot dump glTF/GLB export; -F is now rejected - Cliff Yapp
|
||||
* faster NMG solid ray tracing (10-20x faster) - Sean Morrison
|
||||
* preserved primitive colors in shaded BoT and B-Rep drawing - Cliff Yapp
|
||||
* preserved B-Rep attributes through in-place geometry edits - Cliff Yapp
|
||||
* fixed gcv filter arguments containing one-character values - Cliff Yapp
|
||||
* preserved prim colors in shaded BoT and B-Rep drawing - Cliff Yapp
|
||||
* preserved B-Rep attrs through in-place geometry edits - Cliff Yapp
|
||||
* fixed gcv filter arguments w/ one-character values - Cliff Yapp
|
||||
* improved iges-g importer robustness - Cliff Yapp
|
||||
* expanded g-iges native CSG and B-Rep export support - Cliff Yapp
|
||||
* added IGES import and export support to gcv - Cliff Yapp
|
||||
* rt -b query option now supports a single value index - Sean Morrison
|
||||
* rt -b query option now supports a single value idx - Sean Morrison
|
||||
* fixed .dpix file format portability and icv support - Sean Morrison
|
||||
* expanded pixscale support for piped image data - Sean Morrison
|
||||
* fixed HYP volume and ARBN totals reported by analyze - Cliff Yapp
|
||||
* corrected rtarea sampling and exposed-area calculations - Cliff Yapp
|
||||
* fixed VOL data-file lookup and missing-data errors - Chris McGregor
|
||||
* corrected rtarea sampling/exposed-area calculations - Cliff Yapp
|
||||
* bot split now groups results and preserves mesh data - Cliff Yapp
|
||||
* bot decimate now preserves normals, UVs, and plate data - Cliff Yapp
|
||||
* bot decimate now preserves nrmls, UVs, and plate data - Cliff Yapp
|
||||
* added raytracing support for annotation objects - Cliff Yapp
|
||||
* fixed MGED X embedded framebuffer display manager crash - Cliff Yapp
|
||||
* fixed Windows subprocess quoting and empty arguments - Cliff Yapp
|
||||
* fixed mged X embedded framebuffer display manager crash - Cliff Yapp
|
||||
* fixed Windows drive-letter path handling - Cliff Yapp
|
||||
* added annotate command for managing annotation objects - Cliff Yapp
|
||||
* added repair command for supported invalid primitives - Cliff Yapp
|
||||
* changed default new-region material code from 1 to 0 - Keith Bowman
|
||||
@@ -103,26 +133,34 @@ changes made. See document footer for additional details.
|
||||
* added option validation to image & framebuffer tools - Sean Morrison
|
||||
* added contributor, API, command, & raytracing guides - Sean Morrison
|
||||
* added and updated command and library manual pages - Sean Morrison
|
||||
* added pnts write -f xyz export option - Chris McGregor
|
||||
* fixed mged ARB5 point editing - Cliff Yapp
|
||||
* fixed mged gqa and rtcheck stale overlap plots - Cliff Yapp
|
||||
* fixed procedural tree generator crashes - Sean Morrison, Cliff Yapp
|
||||
* added gaia Earth models with day/dusk/night textures - Sean Morrison
|
||||
* fixed fbcolor ramps & Windows input - Cliff Yapp, Sean Morrison
|
||||
* added parashot gravity trajectory path generator - Sean Morrison
|
||||
* added metaball point scaling (via metaball_move_pnt) - Sean Morrison
|
||||
* added metaball pnt scaling (via metaball_move_pnt) - Sean Morrison
|
||||
* all raytracing apps now report object prep failures - Sean Morrison
|
||||
* improved fast4-g robustness and error reporting - Cliff Yapp
|
||||
* improved robustness of g-vrml plate mode handling - Cliff Yapp
|
||||
* fixed MGED ARB5 point, pipe, and sketch editing - Cliff Yapp
|
||||
* added pnts write -f xyz export option - Chris McGregor
|
||||
* added support for tick drawing on MGED model axis - Cliff Yapp
|
||||
* added support for tick drawing on mged model axis - Cliff Yapp
|
||||
* added naca456 left and paired wing generation - Cliff Yapp
|
||||
* added patch-g -j option, specify object thicknesses - Sean Morrison
|
||||
* fixed fbcolor color ramp bug drawing the last color - Sean Morrison
|
||||
* corrected bwstat median and input error handling - Sean Morrison
|
||||
* fixed gist -Q quick preview option - Sean Morrison
|
||||
* rt preserves eye_pt setting in command scripts - Sean Morrison
|
||||
* removed trailing whitespace from nirt output - Sean Morrison
|
||||
* rt -l8 heat graphs now measure CPU time - Sean Morrison
|
||||
* documented gqa centroid & moments of inertia options - Sean Morrison
|
||||
* pixdiff now calculates pixel differences on bw files - Sean Morrison
|
||||
* fixed bwcrop single-axis crops and pixembed edge fill - Sean Morrison
|
||||
* fixed bwcrop single-axis crops & pixembed edge fill - Sean Morrison
|
||||
* fixed isize in non-mm databases - Josh Baker
|
||||
* fixed metaball adjust clearing points on errors - Sean Morrison
|
||||
* fixed benchmark+pixcmp correctness validation bug - Sean Morrison
|
||||
* fixed mged gqa option and value parsing - Sean Morrison
|
||||
* improved gist hierarchy overflow rendering - Chris McGregor
|
||||
* fixed rtxray -o grayscale/BW output - Sean Morrison
|
||||
* fbhelp writes help to stdout and rest to stderr - Sean Morrison
|
||||
* FB now supports /dev/stdout and /dev/stderr devices - Sean Morrison
|
||||
* fixed mged rt -C + -o background color to file bug - Sean Morrison
|
||||
@@ -131,7 +169,7 @@ changes made. See document footer for additional details.
|
||||
* added rtsil -o support for bw output - Sean Morrison
|
||||
* added cutting plane animation generation to rtwizard - Cliff Yapp
|
||||
* fixed pixdiff bug on Windows - Sean Morrison
|
||||
* fixed MGED startup window placement on Windows - Chris McGregor
|
||||
* fixed mged startup window placement on Windows - Chris McGregor
|
||||
* fixed pixborder utility right-side edge bug - Sean Morrison
|
||||
* fixed 'wavelet' image processing tool on Windows - Sean Morrison
|
||||
* quieted mged killtree spurious lookup failure output - Sean Morrison
|
||||
@@ -142,14 +180,16 @@ changes made. See document footer for additional details.
|
||||
* new doc guide, a visual overview of BRL-CAD objects - Sean Morrison
|
||||
* improved triangle quality produced by bot remesh - Cliff Yapp
|
||||
* updated numerous manpage command references - Josh Baker
|
||||
* fixed submodel paths relative to parent databases - Sean Morrison
|
||||
* improved performance making huge combinations - Sean Morrison
|
||||
* added PNTS get, adjust, form, & g2asc/asc2g support - Chris McGregor
|
||||
* fixed ANNOT get, adjust, & g2asc/asc2g support - Chris McGregor
|
||||
* added make -t primitive type listing - Chris McGregor
|
||||
* added gcv import conversion support for vrml/wrl 1.0 - Sean Morrison
|
||||
* fixed gcv appending to existing geometry databases - Sean Morrison
|
||||
* added CSS names, hex, HSL/HSV to libbu color parsing - Sean Morrison
|
||||
* improved OpenNURBS B-Rep import and adjustment safety - Cliff Yapp
|
||||
* improved Debian application menus and MIME associations - Jordi Sayol
|
||||
* improved Debian application menus/MIME associations - Jordi Sayol
|
||||
* updated Tcl/Tk dependency to v8.6.18 using bundled Itcl - Cliff Yapp
|
||||
* updated Clipper2 dependency to v1.5.4 - Cliff Yapp
|
||||
* updated Eigen dependency to v5.0.1 - Cliff Yapp
|
||||
* updated GDAL dependency to v3.13.3 - Cliff Yapp
|
||||
@@ -161,7 +201,6 @@ changes made. See document footer for additional details.
|
||||
* updated OpenNURBS dependency to v8.34.26223.11001 - Cliff Yapp
|
||||
* updated PROJ dependency to v9.8.1 - Cliff Yapp
|
||||
* updated PoissonRecon dependency to v18.75 - Cliff Yapp
|
||||
* updated Tcl/Tk dependency to v8.6.18, using bundled Itcl - Cliff Yapp
|
||||
* updated libjpeg-turbo dependency to v3.2.0 - Cliff Yapp
|
||||
* updated libosmesa dependency to SHA1 bfa90de - Cliff Yapp
|
||||
* updated libpng dependency to v1.6.58 - Cliff Yapp
|
||||
@@ -270,7 +309,7 @@ and extensively improving existing docs.
|
||||
* added metadata-aware image diff & nirt analysis - Cliff Yapp
|
||||
* added basic PNTS point-cloud ray tracing - Sean Morrison
|
||||
* improved nirt behavior in Windows bash consoles - Cliff Yapp
|
||||
* added g2asc/asc2g support for PNTS, DSP, and EBM - Sean Morrison
|
||||
* added g2asc/asc2g support for DSP and EBM - Sean Morrison
|
||||
* updated gct mesh decimation dependency to mmesh - Cliff Yapp
|
||||
* added procedural wing, propeller & tree generators - Cliff Yapp
|
||||
* added ISO tire dimensions & named tread presets - Cliff Yapp
|
||||
@@ -542,6 +581,7 @@ for more information.
|
||||
----------------------------------------------------------------------
|
||||
--- 2023-11-06 Release 7.38.0 ---
|
||||
----------------------------------------------------------------------
|
||||
|
||||
This major release of BRL-CAD introduces significant improvements to
|
||||
the robustness of the facetize command by leveraging the Manifold
|
||||
library for boolean evaluation of meshes. Facetization is an operation
|
||||
@@ -580,6 +620,7 @@ processing high level assemblies in BRL-CAD's example geometry files:
|
||||
----------------------------------------------------------------------
|
||||
--- 2023-07-26 Release 7.36.0 ---
|
||||
----------------------------------------------------------------------
|
||||
|
||||
This major release of BRL-CAD introduces significant updates to
|
||||
geometry conversion support. The update includes modernization for
|
||||
NURBS importing and interaction, BoT manipulation, and incorporates
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
BRL-CAD
|
||||
Release 7.44.2
|
||||
http://brlcad.org/
|
||||
BRL-CAD
|
||||
Release 7.46.0
|
||||
http://brlcad.org/
|
||||
|
||||
BRL-CAD is a powerful cross-platform open source combinatorial
|
||||
solid modeling system that includes an interactive 3D solid geometry
|
||||
|
||||
@@ -2668,15 +2668,6 @@ DOCUMENTATION
|
||||
Comparison of how BRL-CAD overlaps feature-wise with various
|
||||
major commercial CAD systems.
|
||||
|
||||
* Solid Geometry Representation Comparisons
|
||||
Describe the implications, tradeoffs, and conversion issues
|
||||
involved when converting between different geometry
|
||||
representations. Includes describing implicit geometry,
|
||||
explicit polygonal boundary representations, explicit spline
|
||||
surface representations, volumetric models, wireframes,
|
||||
boolean operations, feature edit operations, and parametric
|
||||
modeling.
|
||||
|
||||
* BRL-CAD Open Source
|
||||
An overview of how BRL-CAD is organized as an open source
|
||||
project and community describing the developer meritocracy,
|
||||
|
||||
@@ -20,6 +20,7 @@ set(
|
||||
procedural_geometry.adoc
|
||||
projection_shader.adoc
|
||||
renderer_showcase.adoc
|
||||
solid_geometry_comparison.adoc
|
||||
taxonomy.adoc
|
||||
tire.adoc
|
||||
)
|
||||
@@ -227,6 +228,17 @@ set(
|
||||
images/renderer_gallery_rthide.png
|
||||
images/renderer_gallery_rtdepth.png
|
||||
images/renderer_gallery_rtxray.png
|
||||
images/solid_rep_csg.png
|
||||
images/solid_rep_brep_boolean.png
|
||||
images/solid_rep_brep_evaluated.png
|
||||
images/solid_rep_polygonal_boolean.png
|
||||
images/solid_rep_nmg.png
|
||||
images/solid_rep_bot.png
|
||||
images/solid_rep_revolve.png
|
||||
images/solid_rep_voxel_cells.png
|
||||
images/solid_rep_volume.png
|
||||
images/solid_rep_point_cloud.png
|
||||
images/solid_rep_wireframe.png
|
||||
images/adrt_castle_path.png
|
||||
images/adrt_castle_rt.png
|
||||
images/adrt_castle_rt_ao.png
|
||||
@@ -409,6 +421,54 @@ if(
|
||||
set_target_properties(renderer-gallery-snapshot PROPERTIES EXCLUDE_FROM_DEFAULT_BUILD 1)
|
||||
endif()
|
||||
|
||||
# Solid geometry comparison -- opt-in regeneration of committed snapshots.
|
||||
#
|
||||
# The article and solid_rep_*.png images are normal committed documentation.
|
||||
# This target rebuilds the common pawn, derives each representation with the
|
||||
# built tools, and refreshes the images for review.
|
||||
option(
|
||||
BRLCAD_GENERATE_SOLID_REPRESENTATION_COMPARISON
|
||||
"Regenerate the solid geometry representation comparison snapshots"
|
||||
OFF
|
||||
)
|
||||
if(
|
||||
BRLCAD_GENERATE_SOLID_REPRESENTATION_COMPARISON
|
||||
AND TARGET mged
|
||||
AND TARGET rt
|
||||
AND TARGET g-voxel
|
||||
AND TARGET asc2pix
|
||||
AND TARGET fbclear
|
||||
AND TARGET plot3-fb
|
||||
AND TARGET fb-png
|
||||
)
|
||||
add_custom_target(
|
||||
solid-representation-comparison-snapshot
|
||||
COMMAND
|
||||
${CMAKE_COMMAND}
|
||||
-DMGED=$<TARGET_FILE:mged>
|
||||
-DRT=$<TARGET_FILE:rt>
|
||||
-DGVOXEL=$<TARGET_FILE:g-voxel>
|
||||
-DASC2PIX=$<TARGET_FILE:asc2pix>
|
||||
-DFBCLEAR=$<TARGET_FILE:fbclear>
|
||||
-DPLOT3FB=$<TARGET_FILE:plot3-fb>
|
||||
-DFBPNG=$<TARGET_FILE:fb-png>
|
||||
-DMODEL_SCRIPT=${CMAKE_CURRENT_SOURCE_DIR}/solid_geometry_comparison/create_pawn.tcl
|
||||
-DIMGDIR=${CMAKE_CURRENT_SOURCE_DIR}/images
|
||||
-DWORKDIR=${CMAKE_CURRENT_BINARY_DIR}/solid_representation_comparison_work
|
||||
-DSIZE=512
|
||||
-P ${CMAKE_CURRENT_SOURCE_DIR}/solid_geometry_comparison/generate_comparison.cmake
|
||||
DEPENDS mged rt g-voxel asc2pix fbclear plot3-fb fb-png
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
|
||||
COMMENT "Regenerating solid geometry representation comparison snapshots"
|
||||
VERBATIM
|
||||
USES_TERMINAL
|
||||
)
|
||||
set_target_properties(
|
||||
solid-representation-comparison-snapshot
|
||||
PROPERTIES EXCLUDE_FROM_DEFAULT_BUILD 1
|
||||
)
|
||||
endif()
|
||||
|
||||
# ADRT path-tracing gallery -- opt-in regeneration of the committed snapshot.
|
||||
#
|
||||
# Like the galleries above, the adrt_pathtrace.adoc article is hand-written and
|
||||
@@ -464,6 +524,8 @@ cmakefiles(
|
||||
gallery/generate_gallery.cmake
|
||||
renderer_gallery/renderer_gallery_manifest.cmake
|
||||
renderer_gallery/generate_renderer_gallery.cmake
|
||||
solid_geometry_comparison/create_pawn.tcl
|
||||
solid_geometry_comparison/generate_comparison.cmake
|
||||
adrt_gallery/generate_adrt_gallery.cmake
|
||||
gallery/inputs/pipe.stdin
|
||||
gallery/inputs/wire.stdin
|
||||
|
||||
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
@@ -0,0 +1,294 @@
|
||||
= Solid Geometry Representation Comparisons
|
||||
:doctype: article
|
||||
:toc:
|
||||
:toclevels: 2
|
||||
|
||||
The same shape can be stored in very different ways. This comparison uses a
|
||||
purpose-built chess pawn to show which information BRL-CAD retains, evaluates,
|
||||
or samples when moving among its principal geometry representations. It is a
|
||||
guide to semantics and conversion choices, not a claim that one representation
|
||||
is universally best.
|
||||
|
||||
== Common model and method
|
||||
|
||||
The reference pawn is a region containing seven overlapping ellipsoids. Their
|
||||
union is non-trivial enough to exercise curved surfaces, overlapping interiors,
|
||||
and several component boundaries while remaining reliable across the
|
||||
conversions shown here. The sketch-and-revolve version independently follows
|
||||
the same nominal profile; it is not an exact conversion of the seven-piece CSG
|
||||
model.
|
||||
|
||||
Every shaded image uses the same orthographic view, 512x512 image size,
|
||||
material, and single-threaded renderer. The wireframe uses the same view and
|
||||
image size. The faceting and sampling settings are intentionally coarse enough
|
||||
to remain visible. A similar silhouette therefore does not mean that two
|
||||
database objects have the same structure or numerical boundary.
|
||||
|
||||
NOTE: This article and its images are a committed snapshot. Developers can
|
||||
regenerate the figures from built BRL-CAD tools by configuring with
|
||||
`-DBRLCAD_GENERATE_SOLID_REPRESENTATION_COMPARISON=ON` and building the
|
||||
`solid-representation-comparison-snapshot` target.
|
||||
|
||||
== At a glance
|
||||
|
||||
[cols="2,2,1,2,4",options="header"]
|
||||
|===
|
||||
|Form
|
||||
|BRL-CAD storage
|
||||
|Solid?
|
||||
|Boolean state
|
||||
|Editing and conversion consequences
|
||||
|
||||
|Analytic CSG
|
||||
|Seven `ell` leaves in a combination
|
||||
|Yes
|
||||
|Retained
|
||||
|Compact primitive parameters and tree edits preserve construction intent.
|
||||
|
||||
|B-rep tree
|
||||
|Closed `brep` leaves in a combination
|
||||
|Yes
|
||||
|Retained
|
||||
|NURBS surfaces and trims replace analytic leaves, but the assembly tree remains.
|
||||
|
||||
|Evaluated B-rep
|
||||
|One closed `brep`
|
||||
|Yes
|
||||
|Evaluated
|
||||
|Direct face, edge, trim, and control-point editing is possible; original
|
||||
features and hierarchy are gone.
|
||||
|
||||
|Polygonal tree
|
||||
|Solid-mode `bot` leaves in a combination
|
||||
|Yes
|
||||
|Retained
|
||||
|Leaf tessellation is approximate, but components and their union tree remain
|
||||
independently replaceable.
|
||||
|
||||
|Evaluated NMG
|
||||
|One `nmg`
|
||||
|Yes
|
||||
|Evaluated
|
||||
|Stores explicit, topology-rich polygonal boundaries and can record
|
||||
non-manifold relationships.
|
||||
|
||||
|Evaluated triangle mesh
|
||||
|One solid-mode `bot`
|
||||
|Yes
|
||||
|Evaluated
|
||||
|Portable triangle connectivity, but curved geometry and construction history
|
||||
have been sampled away.
|
||||
|
||||
|Sketch and revolve
|
||||
|One `sketch` referenced by one `revolve`
|
||||
|Yes
|
||||
|Not needed here
|
||||
|The profile, axis, and sweep angle are compact, meaningful parameters.
|
||||
|
||||
|Voxel cells
|
||||
|A region containing many `arb8` boxes
|
||||
|Yes (sampled)
|
||||
|Cell union retained
|
||||
|Individual cells are ordinary CSG geometry; size grows rapidly as cells
|
||||
shrink.
|
||||
|
||||
|Sampled volume
|
||||
|A file-backed `vol` scalar grid
|
||||
|Yes (thresholded)
|
||||
|None
|
||||
|Compact regular-grid occupancy; resolution and threshold determine the boundary.
|
||||
|
||||
|Point cloud
|
||||
|One `pnts` object with positions and normals
|
||||
|No pawn interior
|
||||
|None
|
||||
|Useful as measured or sampled evidence, but it has no inherent connected
|
||||
boundary or inside/outside rule.
|
||||
|
||||
|Wireframe
|
||||
|Derived line display of the CSG object
|
||||
|No
|
||||
|Uses source tree
|
||||
|A view-dependent drawing aid, not an independent solid representation.
|
||||
|===
|
||||
|
||||
"Solid" in this table means that the stored object supplies usable
|
||||
inside/outside semantics for the pawn. An arbitrary B-rep or mesh is not
|
||||
automatically a solid: its boundary must be closed and consistently oriented.
|
||||
The point size used to render `pnts` makes each sample a small sphere, but those
|
||||
spheres do not reconstruct the pawn interior. A wireframe likewise does not
|
||||
become a solid merely because its curves appear closed in one view.
|
||||
|
||||
== Implicit and parametric forms
|
||||
|
||||
[cols="1,1"]
|
||||
[%noheader]
|
||||
|===
|
||||
|image:images/solid_rep_csg.png[Analytic CSG pawn,512]
|
||||
|image:images/solid_rep_revolve.png[Sketch and revolve pawn,512]
|
||||
|
||||
|*Analytic CSG.* Native primitives retain their equations and dimensions; the
|
||||
region evaluates their union during ray tracing.
|
||||
|*Sketch plus revolve.* A closed line-segment profile is swept through 360
|
||||
degrees. The stepped profile is intentional and remains editable in two
|
||||
dimensions.
|
||||
|===
|
||||
|
||||
Analytic CSG is an implicit representation: intersections with ideal primitive
|
||||
surfaces are computed as needed rather than read from a stored boundary mesh.
|
||||
It is usually the most compact form here and makes edits such as changing the
|
||||
head radius or stem proportions direct. Its Boolean hierarchy also records why the
|
||||
shape was assembled.
|
||||
|
||||
`sketch` plus `revolve` is another procedural, parameterized description. It
|
||||
works especially well for an axisymmetric part, although a sketch with only
|
||||
line segments produces conical and cylindrical bands rather than the pawn's
|
||||
original ellipsoidal surfaces. BRL-CAD supports primitive parameters, placement
|
||||
matrices, sketches, and sweeps; that should not be confused with a general
|
||||
constraint solver or an associative feature-history system.
|
||||
|
||||
== Spline boundary representations
|
||||
|
||||
[cols="1,1"]
|
||||
[%noheader]
|
||||
|===
|
||||
|image:images/solid_rep_brep_boolean.png[B-rep leaves with Boolean tree,512]
|
||||
|image:images/solid_rep_brep_evaluated.png[Single evaluated B-rep,512]
|
||||
|
||||
|*B-rep with Booleans.* Each analytic leaf becomes a closed B-rep whose
|
||||
boundaries are trimmed NURBS surfaces. The original union tree is retained.
|
||||
|*B-rep without Booleans.* Boolean evaluation produces one explicit boundary;
|
||||
operand identities and the construction tree no longer exist as editable
|
||||
features.
|
||||
|===
|
||||
|
||||
A B-rep stores faces, trims, edges, vertices, and their topology explicitly.
|
||||
Rational NURBS can represent the source quadrics exactly as surfaces, but that
|
||||
does not make every CSG-to-B-rep conversion exact: intersections, trimming,
|
||||
topology classification, and repair use numerical tolerances. Keeping a tree
|
||||
of B-rep leaves isolates conversion to each primitive and retains high-level
|
||||
editing. Evaluating the tree yields a more self-contained boundary for
|
||||
exchange and face-level work, at the cost of that history. This snapshot's
|
||||
evaluated result passes both `brep ... valid` and `brep ... solid`; conversion
|
||||
results should be checked rather than assumed to be closed.
|
||||
|
||||
== Polygonal boundaries
|
||||
|
||||
[cols="1,1,1"]
|
||||
[%noheader]
|
||||
|===
|
||||
|image:images/solid_rep_polygonal_boolean.png[BoT leaves with Boolean tree,512]
|
||||
|image:images/solid_rep_nmg.png[Evaluated NMG pawn,512]
|
||||
|image:images/solid_rep_bot.png[Evaluated BoT pawn,512]
|
||||
|
||||
|*Polygonal with Booleans.* Each leaf is independently tessellated to a BoT,
|
||||
then placed in the original union tree.
|
||||
|*Evaluated NMG.* One non-manifold geometry object records the evaluated
|
||||
polygonal boundary and its richer topology.
|
||||
|*Evaluated triangle mesh.* One solid, counter-clockwise BoT stores 982
|
||||
vertices and 1,960 triangular faces in this snapshot.
|
||||
|===
|
||||
|
||||
All three images expose the same coarse tessellation. Their similar appearance
|
||||
hides an important distinction: the first still has seven editable operands,
|
||||
whereas the NMG and single BoT are evaluated results. NMG can represent
|
||||
polygonal faces and non-manifold adjacency; BoT faces are triangles only.
|
||||
Neither recreates analytic curvature when converted back. Finer tessellation
|
||||
reduces visible error but increases storage and processing cost.
|
||||
|
||||
== Sampled forms
|
||||
|
||||
[cols="1,1,1"]
|
||||
[%noheader]
|
||||
|===
|
||||
|image:images/solid_rep_voxel_cells.png[Pawn represented by CSG voxel cells,512]
|
||||
|image:images/solid_rep_volume.png[Pawn represented by a VOL grid,512]
|
||||
|image:images/solid_rep_point_cloud.png[Pawn represented by surface points,512]
|
||||
|
||||
|*Explicit voxel cells.* `voxelize` tests 3 mm cells and creates a CSG union of
|
||||
699 boxes for cells exceeding the occupancy threshold.
|
||||
|*Sampled volume.* `g-voxel` samples a 2 mm grid; a file-backed `vol` interprets
|
||||
the resulting 17x17x30 byte array through an occupancy window.
|
||||
|*Point cloud.* `pnts gen` samples the analytic surface on a 2 mm grid. Small
|
||||
rendered spheres make the otherwise zero-area sample locations visible.
|
||||
|===
|
||||
|
||||
The two voxel images are deliberately separate. The `voxelize` command emits
|
||||
ordinary box primitives and a combination, while `vol` reads scalar samples on
|
||||
a regular grid and derives a threshold boundary at ray-trace time. Both are
|
||||
volumetric approximations whose accuracy, memory, and feature size depend on
|
||||
cell spacing. A point cloud samples only the surface and has no connectivity;
|
||||
surface reconstruction requires additional assumptions and may invent or omit
|
||||
geometry.
|
||||
|
||||
== Wireframe display
|
||||
|
||||
image::images/solid_rep_wireframe.png[Wireframe display of the analytic CSG pawn,512]
|
||||
|
||||
This wireframe is a view-plane plot of MGED's unevaluated display of the
|
||||
analytic CSG pawn; hidden lines are not removed. Wireframes are useful for
|
||||
selection, orientation, and seeing component curves, but they are ambiguous:
|
||||
curves alone do not say which side is material, which loops bound faces, or
|
||||
which surfaces enclose volume. Keep the solid that generated the display
|
||||
rather than treating the drawing as an equivalent conversion target.
|
||||
|
||||
== Conversion and editing consequences
|
||||
|
||||
A conversion command is not an equivalence guarantee. Before replacing source
|
||||
data, check at least these effects:
|
||||
|
||||
* *Tolerance and resolution:* B-rep Boolean evaluation uses intersection and
|
||||
topology tolerances; tessellation uses chord and angular tolerances; voxels
|
||||
and points use sample spacing and thresholds. Each can erase a feature
|
||||
smaller than its controlling tolerance.
|
||||
* *Construction intent:* evaluating a Boolean tree discards operand identity,
|
||||
primitive parameters, and ordering. Converting leaves while retaining the
|
||||
tree keeps more of that intent.
|
||||
* *Topology:* a visually plausible B-rep or mesh may contain gaps, duplicate
|
||||
faces, self-intersections, or inconsistent orientation. Validate closure
|
||||
and ray behavior, not only a shaded image.
|
||||
* *Attributes:* collapsing a hierarchy may change where region identifiers,
|
||||
materials, colors, and other attributes reside. Verify them separately
|
||||
from geometry.
|
||||
* *Round trips:* CSG to mesh to B-rep does not recover the original equations;
|
||||
points to a surface requires reconstruction; resampling a volume compounds
|
||||
discretization. Retain the highest-level trustworthy source.
|
||||
|
||||
For parameter changes and repeated design work, prefer analytic primitives,
|
||||
sketch sweeps, and preserved combination trees. Prefer a validated B-rep when
|
||||
trimmed-surface topology or exchange requires it. Use NMG for topology-rich
|
||||
polygonal processing and BoT for triangle-oriented interchange or algorithms.
|
||||
Use voxels for occupancy operations and point clouds for measured or sampled
|
||||
data, not as silent substitutes for a watertight solid.
|
||||
|
||||
BRL-CAD also supports specialized forms such as DSP height fields, EBM
|
||||
extrusions, ARS surfaces, metaballs, and legacy `poly` and `bspline` objects.
|
||||
They are not shown as interchangeable pawn encodings: DSP and EBM are
|
||||
domain-specific, metaballs change the modeling method, and BoT and B-rep are the
|
||||
preferred general successors to the legacy polygon and spline forms.
|
||||
|
||||
== Reproducing the conversions
|
||||
|
||||
The snapshot generator creates the database and runs these principal MGED
|
||||
operations (names shortened here only where noted):
|
||||
|
||||
[source]
|
||||
----
|
||||
brep comparison.csg.r brep --no-evaluation
|
||||
brep comparison.csg.r brep comparison.brep_evaluated.s
|
||||
facetize -n comparison.csg.r comparison.nmg.s
|
||||
facetize --methods NMG comparison.csg.r comparison.bot.s
|
||||
facetize --methods NMG base.s base.bot # repeated for all seven leaves
|
||||
voxelize -s "3 3 3" -d 4 -t 0.45 comparison.voxel_cells.r comparison.csg.r
|
||||
pnts gen -t 2 --surface --grid comparison.csg.r comparison.samples
|
||||
pnts write comparison.samples comparison.xyz
|
||||
pnts read -f xyzijk --size 0.45 comparison.xyz comparison.point_cloud.s
|
||||
----
|
||||
|
||||
The generator also runs `g-voxel` and expands its sparse occupied-cell report
|
||||
into the x-fastest byte array used by `vol`. It then renders every shaded
|
||||
object with `rt -B -M -P1` and the saved view; MGED's `plot -2d`, `plot3-fb`,
|
||||
and `fb-png` produce the wireframe image. See
|
||||
`solid_geometry_comparison/generate_comparison.cmake` and
|
||||
`solid_geometry_comparison/create_pawn.tcl` for the complete reproducible
|
||||
inputs.
|
||||
@@ -0,0 +1,24 @@
|
||||
# Create the shared pawn used by the solid-representation comparison.
|
||||
|
||||
title {Solid geometry representation comparison pawn}
|
||||
units mm
|
||||
|
||||
# Deliberate overlaps keep the pawn connected while exercising curved-surface
|
||||
# intersections during Boolean evaluation.
|
||||
db put base.s ell V {0 0 4.5} A {17 0 0} B {0 17 0} C {0 0 4.5}
|
||||
db put base_bead.s ell V {0 0 9.5} A {14 0 0} B {0 14 0} C {0 0 5}
|
||||
db put body.s ell V {0 0 18} A {12 0 0} B {0 12 0} C {0 0 9}
|
||||
db put stem.s ell V {0 0 29} A {7 0 0} B {0 7 0} C {0 0 13}
|
||||
db put collar.s ell V {0 0 39} A {9 0 0} B {0 9 0} C {0 0 3.5}
|
||||
db put neck.s ell V {0 0 43} A {6.5 0 0} B {0 6.5 0} C {0 0 5}
|
||||
db put head.s ell V {0 0 50} A {9 0 0} B {0 9 0} C {0 0 9}
|
||||
|
||||
db put comparison.csg.r comb region yes id 1000 los 100 GIFTmater 1 rgb {235 190 90} shader {plastic} tree {u {u {u {l base.s} {l base_bead.s}} {u {l body.s} {l stem.s}}} {u {u {l collar.s} {l neck.s}} {l head.s}}}
|
||||
|
||||
# The revolve uses a deliberately independent outline. Its single closed
|
||||
# sketch approximates the same pawn silhouette without copying the CSG tree.
|
||||
db put comparison.profile sketch V {0 0 0} A {1 0 0} B {0 0 1} VL {{0 0} {17 0} {17 2} {16.3 4} {15 6} {14 8} {15.4 7} {16 9} {15.4 11} {13.5 12.5} {11.2 14} {12 18} {11 21} {9.5 23} {8 26} {7.5 32} {6.5 36} {7.5 37} {9 39} {7.5 41} {6.5 42} {6.5 43} {7.5 44} {8.5 46} {9 50} {8.5 54} {6.7 56.5} {0 59}} SL {{line S 0 E 1} {line S 1 E 2} {line S 2 E 3} {line S 3 E 4} {line S 4 E 5} {line S 5 E 6} {line S 6 E 7} {line S 7 E 8} {line S 8 E 9} {line S 9 E 10} {line S 10 E 11} {line S 11 E 12} {line S 12 E 13} {line S 13 E 14} {line S 14 E 15} {line S 15 E 16} {line S 16 E 17} {line S 17 E 18} {line S 18 E 19} {line S 19 E 20} {line S 20 E 21} {line S 21 E 22} {line S 22 E 23} {line S 23 E 24} {line S 24 E 25} {line S 25 E 26} {line S 26 E 27} {line S 27 E 0}}
|
||||
db put comparison.revolve.s revolve V {0 0 0} axis {0 0 1} R {1 0 0} ang 6.283185307179586 sk_name comparison.profile
|
||||
db put comparison.revolve.r comb region yes id 1001 los 100 GIFTmater 1 rgb {235 190 90} shader {plastic} tree {l comparison.revolve.s}
|
||||
|
||||
q
|
||||
@@ -0,0 +1,372 @@
|
||||
# Regenerate the solid geometry representation comparison snapshots.
|
||||
#
|
||||
# The article is hand-written. This script builds its common pawn model,
|
||||
# derives each representation using BRL-CAD tools, and replaces the committed
|
||||
# images. All databases and sidecar data remain in WORKDIR.
|
||||
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
foreach(v MGED RT GVOXEL ASC2PIX FBCLEAR PLOT3FB FBPNG
|
||||
MODEL_SCRIPT IMGDIR WORKDIR)
|
||||
if(NOT DEFINED ${v})
|
||||
message(FATAL_ERROR "generate_comparison: -D${v}=... is required")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
if(NOT DEFINED SIZE)
|
||||
set(SIZE 512)
|
||||
endif()
|
||||
|
||||
set(RENDER_TIMEOUT 1800)
|
||||
set(PAWN_DB "${WORKDIR}/solid_representation_pawn.g")
|
||||
set(VIEW_SCRIPT "${WORKDIR}/pawn.view")
|
||||
set(MGED_SCRIPT "${WORKDIR}/assemble_variants.tcl")
|
||||
set(VOXELIZE_SCRIPT "${WORKDIR}/voxelize.tcl")
|
||||
set(VOXEL_LIST "${WORKDIR}/voxels1.txt")
|
||||
set(VOLUME_ASCII "${WORKDIR}/comparison_volume.asc")
|
||||
set(VOLUME_FILE "${WORKDIR}/comparison.vol")
|
||||
|
||||
file(REMOVE_RECURSE "${WORKDIR}")
|
||||
file(MAKE_DIRECTORY "${WORKDIR}")
|
||||
file(MAKE_DIRECTORY "${IMGDIR}")
|
||||
|
||||
function(run_process label)
|
||||
execute_process(
|
||||
COMMAND ${ARGN}
|
||||
WORKING_DIRECTORY "${WORKDIR}"
|
||||
TIMEOUT ${RENDER_TIMEOUT}
|
||||
RESULT_VARIABLE process_result
|
||||
OUTPUT_VARIABLE process_stdout
|
||||
ERROR_VARIABLE process_stderr
|
||||
)
|
||||
if(NOT process_result EQUAL 0)
|
||||
message(
|
||||
FATAL_ERROR
|
||||
"${label} failed (exit ${process_result})\n${process_stdout}${process_stderr}"
|
||||
)
|
||||
endif()
|
||||
message(STATUS "${label}")
|
||||
endfunction()
|
||||
|
||||
function(require_brep_property object property expected_output)
|
||||
execute_process(
|
||||
COMMAND "${MGED}" --no-rc -c "${PAWN_DB}" brep "${object}" "${property}"
|
||||
WORKING_DIRECTORY "${WORKDIR}"
|
||||
TIMEOUT ${RENDER_TIMEOUT}
|
||||
RESULT_VARIABLE property_result
|
||||
OUTPUT_VARIABLE property_stdout
|
||||
ERROR_VARIABLE property_stderr
|
||||
)
|
||||
set(property_output "${property_stdout}${property_stderr}")
|
||||
if(NOT property_result EQUAL 0 OR NOT property_output MATCHES "${expected_output}")
|
||||
message(
|
||||
FATAL_ERROR
|
||||
"B-rep ${object} is not ${property}\n${property_output}"
|
||||
)
|
||||
endif()
|
||||
message(STATUS "Validated B-rep ${object}: ${property}")
|
||||
endfunction()
|
||||
|
||||
execute_process(
|
||||
COMMAND "${MGED}" --no-rc -c "${PAWN_DB}"
|
||||
WORKING_DIRECTORY "${WORKDIR}"
|
||||
TIMEOUT ${RENDER_TIMEOUT}
|
||||
INPUT_FILE "${MODEL_SCRIPT}"
|
||||
RESULT_VARIABLE create_result
|
||||
OUTPUT_VARIABLE create_stdout
|
||||
ERROR_VARIABLE create_stderr
|
||||
)
|
||||
if(NOT create_result EQUAL 0 OR NOT EXISTS "${PAWN_DB}")
|
||||
message(
|
||||
FATAL_ERROR
|
||||
"Creating the comparison pawn failed (exit ${create_result})\n${create_stdout}${create_stderr}"
|
||||
)
|
||||
endif()
|
||||
|
||||
# Preserve one Boolean hierarchy as B-rep leaves, and evaluate another into a
|
||||
# single B-rep. The default suffix is intentional: supplying an explicit
|
||||
# suffix changes parsing in older MGED versions.
|
||||
set(PAWN_LEAVES base base_bead body stem collar neck head)
|
||||
run_process(
|
||||
"Created Boolean tree of B-rep leaves"
|
||||
"${MGED}" --no-rc -c "${PAWN_DB}" brep comparison.csg.r brep --no-evaluation
|
||||
)
|
||||
foreach(leaf IN LISTS PAWN_LEAVES)
|
||||
require_brep_property("${leaf}.s.brep" solid "brep is solid")
|
||||
endforeach()
|
||||
run_process(
|
||||
"Created evaluated B-rep"
|
||||
"${MGED}" --no-rc -c "${PAWN_DB}" brep comparison.csg.r brep comparison.brep_evaluated.s
|
||||
)
|
||||
require_brep_property(comparison.brep_evaluated.s valid "brep is valid")
|
||||
require_brep_property(comparison.brep_evaluated.s solid "brep is solid")
|
||||
|
||||
# Generate both the topology-rich NMG form and the triangle-only BoT form.
|
||||
run_process(
|
||||
"Created evaluated NMG"
|
||||
"${MGED}" --no-rc -c "${PAWN_DB}" facetize -n comparison.csg.r comparison.nmg.s
|
||||
)
|
||||
run_process(
|
||||
"Created evaluated BoT"
|
||||
"${MGED}" --no-rc -c "${PAWN_DB}" facetize --methods NMG comparison.csg.r comparison.bot.s
|
||||
)
|
||||
|
||||
# Tessellating leaves independently preserves the editable Boolean hierarchy.
|
||||
foreach(leaf IN LISTS PAWN_LEAVES)
|
||||
run_process(
|
||||
"Tessellated ${leaf}.s"
|
||||
"${MGED}" --no-rc -c "${PAWN_DB}" facetize --methods NMG "${leaf}.s" "${leaf}.bot"
|
||||
)
|
||||
endforeach()
|
||||
|
||||
file(WRITE "${VOXELIZE_SCRIPT}" "voxelize -s {3 3 3} -d 4 -t 0.45 comparison.voxel_cells.r comparison.csg.r\nq\n")
|
||||
execute_process(
|
||||
COMMAND "${MGED}" --no-rc -c "${PAWN_DB}"
|
||||
WORKING_DIRECTORY "${WORKDIR}"
|
||||
TIMEOUT ${RENDER_TIMEOUT}
|
||||
INPUT_FILE "${VOXELIZE_SCRIPT}"
|
||||
RESULT_VARIABLE voxelize_result
|
||||
OUTPUT_VARIABLE voxelize_stdout
|
||||
ERROR_VARIABLE voxelize_stderr
|
||||
)
|
||||
if(NOT voxelize_result EQUAL 0)
|
||||
message(
|
||||
FATAL_ERROR
|
||||
"Creating explicit voxel cells failed (exit ${voxelize_result})\n${voxelize_stdout}${voxelize_stderr}"
|
||||
)
|
||||
endif()
|
||||
message(STATUS "Created explicit voxel cells")
|
||||
|
||||
# PNTS stores point size at import time. The generator first writes all
|
||||
# sampled fields (xyzijk), then reads them back with a visible point diameter.
|
||||
run_process(
|
||||
"Sampled pawn surface"
|
||||
"${MGED}" --no-rc -c "${PAWN_DB}" pnts gen -t 2 --surface --grid
|
||||
comparison.csg.r comparison.samples
|
||||
)
|
||||
run_process(
|
||||
"Exported pawn point samples"
|
||||
"${MGED}" --no-rc -c "${PAWN_DB}" pnts write comparison.samples comparison.xyz
|
||||
)
|
||||
run_process(
|
||||
"Created visible point cloud"
|
||||
"${MGED}" --no-rc -c "${PAWN_DB}" pnts read -f xyzijk --size 0.45
|
||||
comparison.xyz comparison.point_cloud.s
|
||||
)
|
||||
|
||||
# g-voxel reports occupied cells. Expand that sparse report into the regular,
|
||||
# x-fastest byte array required by VOL. The pawn bounds are [-17,17] in X/Y
|
||||
# and [0,60] in Z; 2 mm cells therefore form a 17 x 17 x 30 grid.
|
||||
file(REMOVE "${VOXEL_LIST}")
|
||||
run_process(
|
||||
"Sampled VOL grid"
|
||||
"${GVOXEL}" -s "2 2 2" -d 4 -t 0.5 "${PAWN_DB}" comparison.csg.r
|
||||
)
|
||||
if(NOT EXISTS "${VOXEL_LIST}")
|
||||
message(FATAL_ERROR "g-voxel did not create ${VOXEL_LIST}")
|
||||
endif()
|
||||
|
||||
file(STRINGS "${VOXEL_LIST}" voxel_lines)
|
||||
list(LENGTH voxel_lines occupied_voxels)
|
||||
if(occupied_voxels EQUAL 0)
|
||||
message(FATAL_ERROR "g-voxel reported no occupied cells")
|
||||
endif()
|
||||
|
||||
foreach(line IN LISTS voxel_lines)
|
||||
string(
|
||||
REGEX MATCH
|
||||
"^\\((-?[0-9]+)\\.0+, (-?[0-9]+)\\.0+, (-?[0-9]+)\\.0+\\)[ \t]+[^ \t]+[ \t]+[0-9.]+$"
|
||||
parsed "${line}"
|
||||
)
|
||||
if(NOT parsed)
|
||||
message(FATAL_ERROR "Cannot parse g-voxel output: ${line}")
|
||||
endif()
|
||||
set(x_coord "${CMAKE_MATCH_1}")
|
||||
set(y_coord "${CMAKE_MATCH_2}")
|
||||
set(z_coord "${CMAKE_MATCH_3}")
|
||||
math(EXPR x_index "(${x_coord} + 16) / 2")
|
||||
math(EXPR y_index "(${y_coord} + 16) / 2")
|
||||
math(EXPR z_index "(${z_coord} - 1) / 2")
|
||||
if(x_index LESS 0 OR x_index GREATER 16 OR
|
||||
y_index LESS 0 OR y_index GREATER 16 OR
|
||||
z_index LESS 0 OR z_index GREATER 29)
|
||||
message(FATAL_ERROR "g-voxel coordinate outside the expected pawn grid: ${line}")
|
||||
endif()
|
||||
set("voxel_${x_index}_${y_index}_${z_index}" 255)
|
||||
endforeach()
|
||||
|
||||
set(volume_text "")
|
||||
foreach(z_index RANGE 0 29)
|
||||
foreach(y_index RANGE 0 16)
|
||||
foreach(x_index RANGE 0 16)
|
||||
if(DEFINED voxel_${x_index}_${y_index}_${z_index})
|
||||
set(sample ff)
|
||||
else()
|
||||
set(sample 00)
|
||||
endif()
|
||||
string(APPEND volume_text "${sample}\n")
|
||||
endforeach()
|
||||
endforeach()
|
||||
endforeach()
|
||||
file(WRITE "${VOLUME_ASCII}" "${volume_text}")
|
||||
|
||||
execute_process(
|
||||
COMMAND "${ASC2PIX}"
|
||||
WORKING_DIRECTORY "${WORKDIR}"
|
||||
TIMEOUT ${RENDER_TIMEOUT}
|
||||
INPUT_FILE "${VOLUME_ASCII}"
|
||||
OUTPUT_FILE "${VOLUME_FILE}"
|
||||
RESULT_VARIABLE asc2pix_result
|
||||
ERROR_VARIABLE asc2pix_error
|
||||
)
|
||||
if(NOT asc2pix_result EQUAL 0)
|
||||
message(FATAL_ERROR "asc2pix failed (exit ${asc2pix_result})\n${asc2pix_error}")
|
||||
endif()
|
||||
file(SIZE "${VOLUME_FILE}" volume_size)
|
||||
if(NOT volume_size EQUAL 8670)
|
||||
message(FATAL_ERROR "VOL sidecar is ${volume_size} bytes; expected 8670")
|
||||
endif()
|
||||
message(STATUS "Encoded ${occupied_voxels} occupied VOL cells")
|
||||
|
||||
# Wrap single primitives in regions so every shaded comparison uses the same
|
||||
# material. The polygonal tree repeats the original CSG union hierarchy with
|
||||
# BoT leaves. VOL references a sidecar basename resolved beside the database.
|
||||
file(WRITE "${MGED_SCRIPT}" [=[
|
||||
db put comparison.brep_evaluated.r comb region yes id 1010 los 100 GIFTmater 1 rgb {235 190 90} shader {plastic} tree {l comparison.brep_evaluated.s}
|
||||
db put comparison.nmg.r comb region yes id 1020 los 100 GIFTmater 1 rgb {235 190 90} shader {plastic} tree {l comparison.nmg.s}
|
||||
db put comparison.bot.r comb region yes id 1030 los 100 GIFTmater 1 rgb {235 190 90} shader {plastic} tree {l comparison.bot.s}
|
||||
db put comparison.polygonal_boolean.r comb region yes id 1040 los 100 GIFTmater 1 rgb {235 190 90} shader {plastic} tree {u {u {u {l base.bot} {l base_bead.bot}} {u {l body.bot} {l stem.bot}}} {u {u {l collar.bot} {l neck.bot}} {l head.bot}}}
|
||||
attr set comparison.voxel_cells.r region R region_id 1050 material_id 1 los 100 shader plastic color 235/190/90
|
||||
db put comparison.point_cloud.r comb region yes id 1060 los 100 GIFTmater 1 rgb {235 190 90} shader {plastic} tree {l comparison.point_cloud.s}
|
||||
db put comparison.volume.s vol file {comparison.vol} name {comparison.vol} src f w 17 n 17 d 30 lo 1 hi 255 size {2 2 2} mat {1 0 0 -17 0 1 0 -17 0 0 1 0 0 0 0 1}
|
||||
db put comparison.volume.r comb region yes id 1070 los 100 GIFTmater 1 rgb {235 190 90} shader {plastic} tree {l comparison.volume.s}
|
||||
q
|
||||
]=])
|
||||
|
||||
execute_process(
|
||||
COMMAND "${MGED}" --no-rc -c "${PAWN_DB}"
|
||||
WORKING_DIRECTORY "${WORKDIR}"
|
||||
TIMEOUT ${RENDER_TIMEOUT}
|
||||
INPUT_FILE "${MGED_SCRIPT}"
|
||||
RESULT_VARIABLE assemble_result
|
||||
OUTPUT_VARIABLE assemble_stdout
|
||||
ERROR_VARIABLE assemble_stderr
|
||||
)
|
||||
if(NOT assemble_result EQUAL 0)
|
||||
message(
|
||||
FATAL_ERROR
|
||||
"Assembling comparison regions failed (exit ${assemble_result})\n${assemble_stdout}${assemble_stderr}"
|
||||
)
|
||||
endif()
|
||||
|
||||
file(WRITE "${VIEW_SCRIPT}" "viewsize 8.000000000000000e+01;\n")
|
||||
file(APPEND "${VIEW_SCRIPT}" "orientation 2.480970000000000e-01 4.765910000000000e-01 7.480970000000000e-01 3.894350000000000e-01;\n")
|
||||
file(APPEND "${VIEW_SCRIPT}" "eye_pt 4.199070000000000e+01 2.940060000000000e+01 5.329140000000000e+01;\n")
|
||||
file(APPEND "${VIEW_SCRIPT}" "start 0; clean;\n")
|
||||
file(APPEND "${VIEW_SCRIPT}" "end;\n")
|
||||
|
||||
function(copy_snapshot local_image)
|
||||
if(NOT EXISTS "${local_image}")
|
||||
message(FATAL_ERROR "Expected image was not created: ${local_image}")
|
||||
endif()
|
||||
file(SIZE "${local_image}" image_size)
|
||||
if(image_size EQUAL 0)
|
||||
message(FATAL_ERROR "Rendered image is empty: ${local_image}")
|
||||
endif()
|
||||
file(COPY "${local_image}" DESTINATION "${IMGDIR}")
|
||||
get_filename_component(image_name "${local_image}" NAME)
|
||||
file(
|
||||
CHMOD "${IMGDIR}/${image_name}"
|
||||
PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ
|
||||
)
|
||||
endfunction()
|
||||
|
||||
function(render_snapshot name object)
|
||||
set(filename "solid_rep_${name}.png")
|
||||
set(local_image "${WORKDIR}/${filename}")
|
||||
file(REMOVE "${local_image}")
|
||||
execute_process(
|
||||
COMMAND "${RT}" -B -M -W "-s${SIZE}" -P1 -o "${filename}" "${PAWN_DB}" "${object}"
|
||||
WORKING_DIRECTORY "${WORKDIR}"
|
||||
TIMEOUT ${RENDER_TIMEOUT}
|
||||
INPUT_FILE "${VIEW_SCRIPT}"
|
||||
RESULT_VARIABLE render_result
|
||||
OUTPUT_VARIABLE render_stdout
|
||||
ERROR_VARIABLE render_stderr
|
||||
)
|
||||
if(NOT render_result EQUAL 0)
|
||||
message(
|
||||
FATAL_ERROR
|
||||
"Rendering ${name} failed (exit ${render_result})\n${render_stdout}${render_stderr}"
|
||||
)
|
||||
endif()
|
||||
copy_snapshot("${local_image}")
|
||||
message(STATUS "Rendered ${filename}")
|
||||
endfunction()
|
||||
|
||||
render_snapshot(csg comparison.csg.r)
|
||||
render_snapshot(brep_boolean comparison.csg.r.brep)
|
||||
render_snapshot(brep_evaluated comparison.brep_evaluated.r)
|
||||
render_snapshot(polygonal_boolean comparison.polygonal_boolean.r)
|
||||
render_snapshot(nmg comparison.nmg.r)
|
||||
render_snapshot(bot comparison.bot.r)
|
||||
render_snapshot(revolve comparison.revolve.r)
|
||||
render_snapshot(voxel_cells comparison.voxel_cells.r)
|
||||
render_snapshot(volume comparison.volume.r)
|
||||
render_snapshot(point_cloud comparison.point_cloud.r)
|
||||
|
||||
# MGED exports its current wireframe display as view-plane plot3. Rasterize
|
||||
# that plot through a disk framebuffer so regeneration needs no display.
|
||||
set(WIREFRAME_PLOT "${WORKDIR}/solid_rep_wireframe.plot3")
|
||||
set(WIREFRAME_PNG "${WORKDIR}/solid_rep_wireframe.png")
|
||||
set(WIREFRAME_FB_NAME "solid_rep_wireframe.fb")
|
||||
set(WIREFRAME_SCRIPT "${WORKDIR}/wireframe.tcl")
|
||||
file(REMOVE "${WIREFRAME_PLOT}" "${WORKDIR}/${WIREFRAME_FB_NAME}" "${WIREFRAME_PNG}")
|
||||
file(WRITE "${WIREFRAME_SCRIPT}" [=[
|
||||
draw comparison.csg.r
|
||||
size 80
|
||||
orientation 0.248097 0.476591 0.748097 0.389435
|
||||
eye_pt 41.9907 29.4006 53.2914
|
||||
plot -2d solid_rep_wireframe.plot3
|
||||
q
|
||||
]=])
|
||||
execute_process(
|
||||
COMMAND "${MGED}" --no-rc -c "${PAWN_DB}"
|
||||
WORKING_DIRECTORY "${WORKDIR}"
|
||||
TIMEOUT ${RENDER_TIMEOUT}
|
||||
INPUT_FILE "${WIREFRAME_SCRIPT}"
|
||||
RESULT_VARIABLE wireframe_result
|
||||
OUTPUT_VARIABLE wireframe_stdout
|
||||
ERROR_VARIABLE wireframe_stderr
|
||||
)
|
||||
if(NOT wireframe_result EQUAL 0 OR NOT EXISTS "${WIREFRAME_PLOT}")
|
||||
message(
|
||||
FATAL_ERROR
|
||||
"MGED wireframe export failed (exit ${wireframe_result})\n${wireframe_stdout}${wireframe_stderr}"
|
||||
)
|
||||
endif()
|
||||
message(STATUS "Exported wireframe plot")
|
||||
run_process(
|
||||
"Cleared wireframe framebuffer"
|
||||
"${FBCLEAR}" -c -F "${WIREFRAME_FB_NAME}" -w "${SIZE}" -n "${SIZE}" 24 24 24
|
||||
)
|
||||
execute_process(
|
||||
COMMAND "${PLOT3FB}" -F "${WIREFRAME_FB_NAME}" -o -w "${SIZE}" -n "${SIZE}"
|
||||
WORKING_DIRECTORY "${WORKDIR}"
|
||||
TIMEOUT ${RENDER_TIMEOUT}
|
||||
INPUT_FILE "${WIREFRAME_PLOT}"
|
||||
RESULT_VARIABLE plot_result
|
||||
OUTPUT_VARIABLE plot_stdout
|
||||
ERROR_VARIABLE plot_stderr
|
||||
)
|
||||
if(NOT plot_result EQUAL 0)
|
||||
message(FATAL_ERROR "plot3-fb failed (exit ${plot_result})\n${plot_stdout}${plot_stderr}")
|
||||
endif()
|
||||
run_process(
|
||||
"Rasterized wireframe snapshot"
|
||||
"${FBPNG}" -F "${WIREFRAME_FB_NAME}" -w "${SIZE}" -n "${SIZE}"
|
||||
"solid_rep_wireframe.png"
|
||||
)
|
||||
copy_snapshot("${WIREFRAME_PNG}")
|
||||
|
||||
message(STATUS "Solid representation comparison: 11 snapshots regenerated")
|
||||
@@ -4,38 +4,54 @@
|
||||
:manmanual: BRL-CAD
|
||||
|
||||
== NAME
|
||||
pixdiff - highlight samples which differ between two pix or bw files
|
||||
pixdiff - highlight differences between two pix or bw images
|
||||
|
||||
== SYNOPSIS
|
||||
|
||||
[source]
|
||||
----
|
||||
pixdiff _file1.pix_ _file2.pix_ > _out.pix_
|
||||
pixdiff _file1.bw_ _file2.bw_ > _out.bw_
|
||||
pixdiff -b _file1.bw_ _file2.bw_ > _out.bw_
|
||||
----
|
||||
|
||||
|
||||
[[description]]
|
||||
== DESCRIPTION
|
||||
|
||||
*pixdiff* compares two raw *pix*(5) or *bw*(5) images byte by byte and
|
||||
writes a difference image of the same size to standard output. Bytes that
|
||||
differ by +/-1 are written at intensity 192, while larger differences are
|
||||
written at full intensity 255. Matching bytes are written at half their
|
||||
original intensity, providing visual context for locating differences.
|
||||
By default, *pixdiff* compares raw *pix*(5) images as RGB pixels and writes
|
||||
a PIX difference image to standard output. When all three channels of a
|
||||
pixel match, it writes a dim grayscale version of that pixel to all three
|
||||
output channels. When any channel differs, each matching channel is written
|
||||
as zero, each channel differing by one is written as 192, and each channel
|
||||
differing by more than one is written as 255.
|
||||
|
||||
Because the comparison does not interpret pixel structure, no format option
|
||||
is needed and *pixdiff* is independent of image resolution. The two inputs
|
||||
must be the same size in bytes; otherwise the program reports the mismatch
|
||||
and exits. Either input file may be given as
|
||||
With *-b*, *pixdiff* compares raw *bw*(5) images one byte at a time and
|
||||
writes a BW difference image. Differing samples use the same 192 and 255
|
||||
intensities; matching samples are written at half their original intensity.
|
||||
|
||||
No image dimensions are needed. The two inputs must be the same size in
|
||||
bytes, and PIX input must contain complete three-byte pixels. Otherwise,
|
||||
*pixdiff* reports an error. Either input file may be given as
|
||||
*-* to read from standard input. Output must be redirected; *pixdiff*
|
||||
refuses to write a raw image stream to a terminal.
|
||||
|
||||
Upon completion, the count of bytes that matched, that were off by 1, and
|
||||
that were off by many is reported on standard error.
|
||||
Upon completion, *pixdiff* reports matching, off-by-one, and larger-difference
|
||||
channel or sample counts on standard error.
|
||||
|
||||
Also see *bwdiff*(1), which can also operate on *pix*(5) files and can
|
||||
produce other kinds of difference output.
|
||||
*bwdiff -n -b* produces a similar BW highlight image, but writes 255 rather
|
||||
than 192 for off-by-one differences. The default *bwdiff*(1) output instead
|
||||
shows signed differences around a neutral gray.
|
||||
|
||||
|
||||
[[options]]
|
||||
== OPTIONS
|
||||
|
||||
*-b*::
|
||||
Treat both inputs and the output as one-byte-per-pixel BW images. Without
|
||||
this option, use three-byte-per-pixel PIX images.
|
||||
|
||||
*-h*::
|
||||
Show usage information.
|
||||
|
||||
|
||||
[[examples]]
|
||||
@@ -59,10 +75,10 @@ on standard error give a quick quantitative sense of how much changed.
|
||||
====
|
||||
[source,bash]
|
||||
----
|
||||
pixdiff depth_a.bw depth_b.bw > depth_diff.bw
|
||||
pixdiff -b depth_a.bw depth_b.bw > depth_diff.bw
|
||||
----
|
||||
Writes a grayscale highlight image using the same byte comparison and
|
||||
statistics as the color example; no format option is needed.
|
||||
Writes a grayscale highlight image using byte-wise comparison. Matching
|
||||
samples retain half their input intensity as context.
|
||||
====
|
||||
|
||||
|
||||
|
||||
@@ -18,7 +18,8 @@ Link with *-lanalyze*.
|
||||
The public interface is organized across the headers included by
|
||||
_analyze.h_, including _analyze/diff.h_, _analyze/density.h_,
|
||||
_analyze/grid.h_, _analyze/heal.h_, _analyze/info.h_, _analyze/pnts.h_,
|
||||
_analyze/polygonize.h_, _analyze/nirt.h_, _analyze/voxelize.h_, and
|
||||
_analyze/contour.h_, _analyze/polygonize.h_, _analyze/nirt.h_,
|
||||
_analyze/voxelize.h_, and
|
||||
_analyze/worker.h_.
|
||||
|
||||
== DESCRIPTION
|
||||
@@ -32,7 +33,7 @@ to gather the information they need.
|
||||
Facilities provided by the library include model differencing, material
|
||||
density and mass property analysis, ray-based gridding and sampling,
|
||||
geometry healing, model information queries, point-cloud generation and
|
||||
processing, polygonization and voxelization of geometry, and the
|
||||
processing, manifold dual contouring and voxelization of geometry, and the
|
||||
interactive ray-tracing engine used by the *nirt*(1) command. A parallel
|
||||
worker interface supports distributing analysis work across multiple
|
||||
threads.
|
||||
|
||||
@@ -200,6 +200,56 @@ ____
|
||||
|
||||
The LIBRT_DEBUG environment variable may be set to the hexadecimal bit vector as described by the DEBUG_* flags listed in the raytrace.h header. Similarly, many LIBRT applications provide a "-x #" command-line option which will take precedence over the environment variable.
|
||||
|
||||
The RT_DEBUG_RTCMP flag (0x10000000) (set with LIBRT_DEBUG=10000000)
|
||||
sets up rtcmp capture, but recording starts disabled. Set
|
||||
LIBRT_RTCMP_FILE before enabling the flag, then call
|
||||
rt_rtcmp_capture_set_enabled(1) from the application or a debugger to
|
||||
record selected rays. Call rt_rtcmp_capture_set_enabled(0) to stop
|
||||
recording. Enabling returns -1 if the debug flag is unset or the output
|
||||
writer failed to initialize; disabling returns 0 even before setup. A
|
||||
ray must begin while recording is enabled, and leaving recording
|
||||
disabled when it finishes suppresses its record. The debug flag must be
|
||||
set before concurrent ray shooting; use the setter to change recording
|
||||
state while rays are running.
|
||||
|
||||
Captured hits append one high-precision rtcmp JSON record per ray with
|
||||
final partitions. Misses produce no record. librt never clears the
|
||||
output file; the caller must clear or rotate it before a new comparison
|
||||
as needed. Separate processes must not write to the same file
|
||||
concurrently. Pending records are flushed within two seconds under
|
||||
normal operation and during normal process shutdown; call
|
||||
rt_rtcmp_capture_flush() to wait for pending records when using a
|
||||
debugger. Disabling recording does not discard records already queued. A
|
||||
process terminated abnormally may lose pending records. rtcmp comparison
|
||||
files should use the same hit-only capture mode on both sides; its
|
||||
generation mode also includes misses. The output can be expanded to also
|
||||
include primitive segments if LIBRT_RTCMP_PRIMITIVES is set to 1 before
|
||||
capture is set up.
|
||||
|
||||
For example, run a single-threaded ray tracing application in a debugger
|
||||
and stop at `rt_shootray` for the suspect ray. Capture just that call,
|
||||
then compare it with a capture of the same ray from a reference build:
|
||||
|
||||
[source,shell]
|
||||
----
|
||||
$ LIBRT_DEBUG=10000000 LIBRT_RTCMP_FILE=trial.json \
|
||||
gdb --args ./ray_application model.g
|
||||
(gdb) break rt_shootray
|
||||
(gdb) run
|
||||
(gdb) call rt_rtcmp_capture_set_enabled(1)
|
||||
(gdb) finish
|
||||
(gdb) call rt_rtcmp_capture_set_enabled(0)
|
||||
(gdb) call rt_rtcmp_capture_flush()
|
||||
(gdb) quit
|
||||
$ rtcmp -c reference.json trial.json
|
||||
----
|
||||
|
||||
Continue to the suspect ray before enabling capture if the breakpoint
|
||||
first stops on another ray. Produce `reference.json` with the same ray
|
||||
and the same capture mode; set LIBRT_RTCMP_PRIMITIVES=1 for both runs
|
||||
when primitive segments are needed to locate a difference before Boolean
|
||||
evaluation.
|
||||
|
||||
The LIBRT_V4FLIP environment variable may be set to a true or false value to respectively force or disable endianness interpretation of v4 geometry database files. The default behavior is to automatically detect whether flipping the endian interpretation will help LIBRT read a binary-incompatible v4 geometry database file. Setting LIBRT_V4FLIP will override automatic detection.
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ facetize - Creates __new_object__ using __old_object__ as a guide, creating an e
|
||||
|
||||
[source]
|
||||
----
|
||||
facetize [-h] [-v] [-q] [-n] [-r] [-s suffix] [-p prefix] [--in-place] [--max-time #] [--max-pnts #] [--resume] [--methods m1,m2,...] [--method-opts METHOD opt1=val opt2=val...] [--no-empty] [--log-file filename] [--nmg-booleval] [--disable-fixup] [--perturb] [--no-perturb] [--perturb-sa-tol #] [--perturb-vol-tol #] [--tolerate-failures] [-B -t threshold] {old_object new_object}
|
||||
facetize [-h] [-v] [-q] [-n] [-r] [-j jobs] [-s suffix] [-p prefix] [--in-place] [--max-time #] [--max-pnts #] [--resume] [--methods m1,m2,...] [--method-opts METHOD opt1=val opt2=val...] [--no-empty] [--log-file filename] [--nmg-booleval] [--disable-fixup] [--perturb] [--no-perturb] [--perturb-sa-tol #] [--perturb-vol-tol #] [--tolerate-failures] [-B -t threshold] {old_object new_object}
|
||||
----
|
||||
|
||||
|
||||
@@ -45,6 +45,8 @@ By default, the Manifold BoT path can make use of a coplanarity-avoidance pertur
|
||||
|
||||
When the perturbation step is active, per-region *facetize* validates the initial Boolean output against the original CSG geometry using a Crofton stochastic raytrace. The default tolerance thresholds that trigger a perturb retry are 10% for both surface area and volume; these can be adjusted with *--perturb-sa-tol* and *--perturb-vol-tol*. At the end of a per-region facetization run *facetize* prints a concise validation summary listing how many regions passed, triggered a perturb retry, or encountered one of the following noteworthy conditions:
|
||||
|
||||
_Unbounded halfspace (validation unavailable)_: Crofton surface-area and volume comparisons require a finite sampling domain. A standalone halfspace, or another CSG result without finite bounds, is therefore not sampled and is counted as having validation unavailable. A bounded object intersected with or subtracted by a halfspace can still be validated because its finite operand supplies the sampling bounds.
|
||||
|
||||
_Sub-mm / few-hit bypass (accepted with note)_: When the Crofton sampler finds only a very small number of ray-surface intersections (below an internal threshold of 50 crossings), the sampling noise alone would exceed 14% relative error, making any SA comparison meaningless. Geometry in this category is typically sub-millimetre in cross-section (e.g., a TGC with a radius under 0.2 mm) but the stochastic sampler has confirmed that some geometry is present. The BoT conversion is accepted and the region is counted separately in the summary. Users should verify that such extreme geometry reflects actual modeling intent rather than an accidental near-degenerate primitive.
|
||||
|
||||
_Zero-hit replacement_: When the Crofton sampler fires rays but records zero intersections with the CSG geometry while the corresponding BoT is non-empty, *facetize* concludes that the Boolean evaluation most likely produces empty geometry (for example, a subtractor that fully engulfs the base object). In this case the non-empty BoT is discarded and replaced with an empty BoT to match the expected raytrace behavior. If this replacement appears unexpected for a given region, the *lint* command can be used to inspect the result.
|
||||
@@ -61,13 +63,21 @@ Different tolerance settings will create finer or coarser meshes. See the *tol*
|
||||
|
||||
Primitive tessellation is performed in a child process against a private working copy of the database in the BRL-CAD cache. The original database is not modified as individual primitive meshes are generated. Only after the conversion succeeds are the requested results transferred from the working database. This separation also permits an interrupted conversion to retain its intermediate state for use with *--resume*.
|
||||
|
||||
The parent command sends primitive names to a persistent tessellation process one at a time. The child reports a result for each name, so a timeout, conversion failure, or child-process crash that occurs before a database write can be associated directly with the affected primitive. The child is restarted when necessary and processing continues with the next name; the parent does not subdivide and retry the whole group to discover which object failed. Unless output is suppressed with *--quiet*, long tessellation batches also report their current object and completed-object count at approximately five-second intervals.
|
||||
The parent command sends primitive names to a small pool of persistent tessellation processes. Each child reports a result for every name, so a timeout, conversion failure, or child-process crash can be associated directly with the affected primitive. A child is restarted when necessary and processing continues with the next name; the parent does not subdivide and retry the whole group to discover which object failed. Unless output is suppressed with *--quiet*, long tessellation batches also report their completed-object count and active-worker state at approximately five-second intervals.
|
||||
|
||||
Database updates use a separate write handshake. After preparing a mesh in memory, the child announces that it is ready to write and reports the approximate mesh size, then waits. The parent acknowledges that it is waiting for the write before the child may update the working database. The child explicitly reports when the update is complete. Consequently, the normal per-object tessellation limit, including *--max-time* and method-specific limits, applies to mesh preparation and does not interrupt an acknowledged database write.
|
||||
By default, *facetize* uses at most two worker processes and only enables the second when both CPU and memory information indicate it is safe. It reserves the larger of one GiB or one quarter of total physical memory for the rest of the system. Starting another job also requires at least 512 MiB beyond that reserve, or the largest worker resident-memory measurement seen so far if that is greater. If the platform cannot report the required memory information, *facetize* falls back to one worker. The available CPU count is divided among worker processes and is also imposed as the thread limit inside each worker, preventing nested algorithms from independently using all CPUs. *--jobs* changes the maximum process count; it does not bypass the memory admission checks.
|
||||
|
||||
Per-region BoT conversion uses this worker policy for both independent CSG validation and initial Manifold Boolean evaluation. When more than one worker is available, persistent validation processes each open their own read-only handle to the original database and compute Crofton surface-area and volume references for different regions. Persistent Boolean processes then evaluate different region trees using independent read-only handles to one shared snapshot of the tessellated working database. That snapshot also serves as the rollback checkpoint, so increasing *--jobs* does not create additional full database copies. This process isolation avoids sharing mutable raytrace preparation or database state between concurrent evaluations. Each result remains associated with the requested region name. The parent commits completed BoTs through one writer and continues to perform hierarchy changes, validation decisions, and any perturb retries serially. A one-worker run retains the established in-process order and avoids the snapshot phase.
|
||||
|
||||
Validation and Boolean workers report their completed-region count and active-worker state approximately every five seconds. When *--max-time* is specified, it also bounds each parallel CSG reference or Boolean request; a timed-out process is terminated, and the failure is associated with that region. Failure or timeout of supplementary CSG validation does not cause unrelated regions to be retried.
|
||||
|
||||
Database updates use a separate write handshake. After preparing a mesh in memory, a child announces that it is ready to write and reports the approximate mesh size and its current resident memory, then waits. The parent grants write permission, the child acknowledges when it is starting the write, and the child explicitly reports when the update is complete. Consequently, the normal per-object tessellation limit, including *--max-time* and method-specific limits, applies to mesh preparation and does not interrupt an acknowledged write.
|
||||
|
||||
With multiple workers, primitive tessellation children write to small staging databases rather than modifying the shared working database. Region Boolean children read one immutable cache-local snapshot while completed results are committed to the working database. The snapshot doubles as the recovery checkpoint and is opened independently by each worker; it is not duplicated per worker. For very large files, snapshot preparation reports the amount copied and elapsed time at five-second intervals. A region result is first completed in memory and enters the write handshake only when it is ready to replace the prior staged result. After a child reports a completed staging write, the parent submits that result to a single persistent writer process. The writer announces that it is ready, waits for the parent's permission, acknowledges that it is starting the write, transfers the result through one persistent working-database handle, and reports completion. This permits tessellation and initial region Boolean evaluation to proceed in parallel without concurrent database allocation updates or repeated full directory scans, while allowing the parent to supervise the canonical database write itself. With one worker, the established direct path avoids the extra staging I/O and database snapshot.
|
||||
|
||||
Before the first primitive conversion, the parent measures write throughput on the same cache filesystem as the working database. It writes a temporary file in 1 MiB chunks until approximately one second has elapsed or 128 MiB has been written, then removes the file immediately. Successful mesh writes of at least 1 MiB subsequently refine this profile. For each database update, the parent allows ten times the projected write time, with a minimum of five seconds and a maximum of five minutes. This measured allowance protects legitimate long writes on slow storage while still detecting a genuinely stalled update.
|
||||
|
||||
If a child fails before write permission is granted, the working database is known to be unchanged and no rollback is needed. A failure, crash, or write-deadline expiration after permission is granted but before completion is ambiguous: the working database may contain a partial update. In that case *facetize* restores its cache-local checkpoint and replays only the primitive conversions already known to have succeeded. The original database remains untouched during this recovery.
|
||||
If a child fails before acknowledging that its write has started, the working database is known to be unchanged and no rollback is needed. In parallel mode, a failure during a child write affects only that worker's disposable staging database, so the failed object is reported and the worker can be restarted without replaying unrelated objects. A serial child failure during a direct write, or a writer-process failure after acknowledging write start, is ambiguous because the working database may contain a partial update. In those cases *facetize* restores its cache-local checkpoint and replays only the conversions not already tied to a specific failure. The original database remains untouched during this recovery.
|
||||
|
||||
|
||||
[[facetize_options]]
|
||||
@@ -94,6 +104,10 @@ Instructs *facetize* to produce NMG solids rather than BoT solids.
|
||||
Rather than converting all specified inputs to one top level object, walk specified trees and individually convert regions to facetized objects. This mode is useful when wanting to display models in shaded mode while preserving color and region ID properties. If no regions are present a normal conversion is performed instead.
|
||||
|
||||
|
||||
*-j, --jobs #*::
|
||||
Set the maximum number of facetize worker processes. A value of 1 forces serial processing. The default value of 0 selects a conservative automatic limit based on available CPUs and memory. Memory checks may delay or reduce parallel work even when a larger value is requested. In per-region BoT mode the same limit controls parallel CSG reference sampling and initial Manifold Boolean evaluation.
|
||||
|
||||
|
||||
*-s, --suffix*::
|
||||
User specified suffix to use avoiding naming conflicts (naming conflict avoidance is needed when creating objects in *-r* mode.)
|
||||
|
||||
@@ -119,7 +133,7 @@ Resume an incomplete facetization process (will utilize cached data from a previ
|
||||
|
||||
|
||||
*--methods*::
|
||||
Accepts a comma separated list of one or more facetization methods to use. The default is to try all algorithms available as needed, but if a user knows they want to try a specific methodology they can call it out using this option.
|
||||
Accepts a comma separated list of one or more facetization methods to use. The default tries NMG tessellation first, Manifold Dual Contouring (MDC) second, and SPSR as a final fallback. Other listed methods may be selected explicitly if a user wants to try a specific methodology.
|
||||
|
||||
|
||||
*--method-opts*::
|
||||
@@ -231,21 +245,15 @@ mged>**facetize --tolerate-failures large_model large_model.partial.bot**
|
||||
|
||||
====
|
||||
|
||||
.Increase Decimation in CM Mode
|
||||
.Refine a Manifold Dual Contouring conversion
|
||||
[example]
|
||||
====
|
||||
mged>**facetize --methods CM --method-opts feature_size=0.06 d_feature_size=2.0 ellipse.r ell.bot**
|
||||
This selects MDC directly and requests a finer cell size with a larger ray
|
||||
budget than the default:
|
||||
|
||||
The default settings on this object in CM mode will produce a little over 8000 faces in the final bot, but the addition of the *d_feature_size* option with a >1.5 value has dramatically reduced the count to 104.
|
||||
mged>**facetize --methods MDC --method-opts "MDC feature_size=0.1 max_rays=400000" ellipse.r ell.bot**
|
||||
|
||||
====
|
||||
|
||||
.Decrease Decimation in CM Mode
|
||||
[example]
|
||||
====
|
||||
mged>**facetize --methods CM --method-opts feature_size=0.06 d_feature_size=0.01 ellipse.r ell.bot**
|
||||
|
||||
If the intent is preserve a more detailed mesh, reducing *decimation-feature-size* will keep more of the original triangles - in the above example, almost 250 thousand of the original 6.9 million faces were retained. NOTE: use lower values here cautiously, as extremely large meshes can be very difficult for many tools to deal with.
|
||||
Smaller feature sizes increase mesh detail, ray count, memory use, and runtime.
|
||||
|
||||
====
|
||||
|
||||
|
||||
@@ -22,4 +22,5 @@ Task-focused guides and how-to articles.
|
||||
* link:articles/procedural_geometry.html[BRL-CAD Procedural Geometry Gallery]
|
||||
* link:articles/projection_shader.html[Using the Projection Shader]
|
||||
* link:articles/renderer_showcase.html[BRL-CAD Renderer Showcase]
|
||||
* link:articles/solid_geometry_comparison.html[Solid Geometry Representation Comparisons]
|
||||
* link:articles/tire.html[Vehicle Tire and Wheel Creation in BRL-CAD]
|
||||
|
||||
@@ -679,23 +679,21 @@ Convert CSG geometry to a 3D voxel grid by:
|
||||
fill fraction
|
||||
* Level-of-detail parameter controls sub-voxel sampling
|
||||
|
||||
==== 7.5 Implicit Surface Polygonizer (Marching Cubes / Tetrahedral)
|
||||
==== 7.5 Manifold Dual Contouring
|
||||
|
||||
An implicit surface polygonizer (Bloomenthal tetrahedral marching) for
|
||||
converting any inside/outside function to a triangle mesh.
|
||||
`contour.cpp` evaluates CSG geometry directly with `librt` and constructs
|
||||
closed, oriented triangle meshes:
|
||||
|
||||
`polygonizer.c` (based on Jules Bloomenthal’s "`An Implicit Surface
|
||||
Polygonizer,`" Graphics Gems IV, 1994) converts any implicit function
|
||||
(inside/outside test) to a triangle mesh:
|
||||
|
||||
* Maintains a hash table of evaluated lattice corners and edges
|
||||
* Expanding front: starts at a seed point and spreads outward, testing
|
||||
adjacent cube faces
|
||||
* Each transitive cube is optionally decomposed into 6 tetrahedra for
|
||||
sub-cube accuracy
|
||||
* Edge-surface crossing is found by bisection (`converge`) then the
|
||||
vertex normal is estimated from finite differences
|
||||
* Used in `libanalyze` for converting CSG regions to faceted form
|
||||
* Recursively refines an implicit octree using cached axis-aligned rays along
|
||||
node box edges and interior quarter- and center-lines
|
||||
* Reuses complete ray partitions at the conforming finest grid and retains
|
||||
evaluated boundary points with analytic surface normals
|
||||
* Resolves ambiguous faces consistently and permits multiple vertices in a
|
||||
cell to preserve local topology
|
||||
* Places vertices with bounded quadratic-error minimization
|
||||
* Validates the resulting indexed mesh before returning it
|
||||
* Backs the legacy `analyze_polygonize` interface without retaining the old
|
||||
seed-driven marching-tetrahedra implementation
|
||||
|
||||
==== 7.6 Geometry Differencing (raydiff)
|
||||
|
||||
@@ -1712,7 +1710,6 @@ import
|
||||
runtime
|
||||
* *MT19937*: Mersenne Twister PRNG
|
||||
* *pbrt-v3* (derived): HLBVH construction algorithm
|
||||
* *Bloomenthal polygonizer*: implicit surface polygonization
|
||||
* *Screened Poisson Reconstruction* (Michael Kazhdan): surface
|
||||
reconstruction from point clouds
|
||||
* *Ball Pivoting Algorithm*: surface reconstruction
|
||||
|
||||
@@ -55,7 +55,6 @@ brlcad_manage_files(pbrt.txt ${DOC_DIR}/legal/embedded TRIGGER
|
||||
brlcad_manage_files(picohash.txt ${DOC_DIR}/legal/embedded TRIGGER gist)
|
||||
brlcad_manage_files(pixcii.txt ${DOC_DIR}/legal/embedded TRIGGER libicv)
|
||||
brlcad_manage_files(point_in_polygon.txt ${DOC_DIR}/legal/embedded TRIGGER libbg)
|
||||
brlcad_manage_files(polygonizer.txt ${DOC_DIR}/legal/embedded TRIGGER libanalyze)
|
||||
brlcad_manage_files(predicates.txt ${DOC_DIR}/legal/embedded TRIGGER libbg)
|
||||
brlcad_manage_files(profont.txt ${DOC_DIR}/legal/embedded TRIGGER libbu)
|
||||
brlcad_manage_files(pstdint.txt ${DOC_DIR}/legal/embedded TRIGGER libbu)
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
https://www.bloomenthal.com/JBloom/code/polygonizer.c
|
||||
|
||||
Authored by Jules Bloomenthal, Xerox PARC.
|
||||
Copyright (c) Xerox Corporation, 1991. All rights reserved.
|
||||
Permission is granted to reproduce, use and distribute this code for
|
||||
any and all purposes, provided that this notice appears in all copies.
|
||||
|
||||
file:/src/libanalyze/polygonizer.c
|
||||
|
||||
@@ -61,5 +61,4 @@ Author: Paul Mackerras (paulus@cs.anu.edu.au),
|
||||
|
||||
file:/src/libtclcad/tkImgFmtPIX.c
|
||||
file:/src/tclscripts/cad_clrpick.tcl
|
||||
file:/src/tclscripts/html_library.tcl
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
#include "common.h"
|
||||
|
||||
#include "analyze/defines.h"
|
||||
#include "analyze/contour.h"
|
||||
#include "analyze/debug.h"
|
||||
#include "analyze/diff.h"
|
||||
#include "analyze/density.h"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
set(
|
||||
analyze_headers
|
||||
contour.h
|
||||
debug.h
|
||||
defines.h
|
||||
density.h
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
/* C O N T O U R . H
|
||||
* BRL-CAD
|
||||
*
|
||||
* Copyright (c) 2026 United States Government as represented by
|
||||
* the U.S. Army Research Laboratory.
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public License
|
||||
* version 2.1 as published by the Free Software Foundation.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this file; see the file named COPYING for more
|
||||
* information.
|
||||
*/
|
||||
/** @addtogroup libanalyze
|
||||
* @{ */
|
||||
/** @file analyze/contour.h
|
||||
*
|
||||
* Ray-driven manifold dual contouring of evaluated BRL-CAD geometry.
|
||||
*/
|
||||
|
||||
#ifndef ANALYZE_CONTOUR_H
|
||||
#define ANALYZE_CONTOUR_H
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#include "vmath.h"
|
||||
#include "analyze/defines.h"
|
||||
|
||||
__BEGIN_DECLS
|
||||
|
||||
#define ANALYZE_MDC_DEFAULT_MAX_RAYS 0
|
||||
#define ANALYZE_MDC_DEFAULT_MINIMUM_FREE_MEM 150000000
|
||||
#define ANALYZE_MDC_DEFAULT_MIN_DEPTH 2
|
||||
#define ANALYZE_MDC_DEFAULT_MAX_DEPTH 10
|
||||
#define ANALYZE_MDC_MAX_DEPTH 20
|
||||
#define ANALYZE_MDC_DEFAULT_MAX_TIME 600
|
||||
|
||||
struct db_i;
|
||||
/** Resource and accuracy controls for analyze_mdc(). */
|
||||
struct analyze_mdc_params {
|
||||
/** Requested maximum finest-cell edge length. Zero selects an
|
||||
* implementation default based on the model bounds. */
|
||||
fastf_t feature_size;
|
||||
|
||||
/** Optional user ceiling for the automatically derived ray budget.
|
||||
* Zero applies only geometry, depth, time, and memory limits. */
|
||||
size_t max_rays;
|
||||
|
||||
/** Abort before available memory drops below this many bytes. */
|
||||
size_t minimum_free_mem;
|
||||
|
||||
/** Minimum and maximum power-of-two grid depths. */
|
||||
int min_depth;
|
||||
int max_depth;
|
||||
|
||||
/** Maximum wall-clock processing time in seconds. Zero is unlimited. */
|
||||
int max_time;
|
||||
|
||||
/** Emit progress diagnostics when non-zero. */
|
||||
int verbosity;
|
||||
};
|
||||
|
||||
#define ANALYZE_MDC_PARAMS_DEFAULT { \
|
||||
0.0, \
|
||||
ANALYZE_MDC_DEFAULT_MAX_RAYS, \
|
||||
ANALYZE_MDC_DEFAULT_MINIMUM_FREE_MEM, \
|
||||
ANALYZE_MDC_DEFAULT_MIN_DEPTH, \
|
||||
ANALYZE_MDC_DEFAULT_MAX_DEPTH, \
|
||||
ANALYZE_MDC_DEFAULT_MAX_TIME, \
|
||||
0 \
|
||||
}
|
||||
|
||||
enum analyze_mdc_status {
|
||||
ANALYZE_MDC_OK = 0,
|
||||
ANALYZE_MDC_INVALID_INPUT = 1,
|
||||
ANALYZE_MDC_PREP_FAILED = 2,
|
||||
ANALYZE_MDC_NO_SURFACE = 3,
|
||||
ANALYZE_MDC_RAY_LIMIT = 4,
|
||||
ANALYZE_MDC_TIMEOUT = 5,
|
||||
ANALYZE_MDC_MEMORY_LIMIT = 6,
|
||||
ANALYZE_MDC_AMBIGUOUS = 7,
|
||||
ANALYZE_MDC_NOT_MANIFOLD = 8
|
||||
};
|
||||
|
||||
/**
|
||||
* Evaluate @p object with librt and construct an indexed triangle mesh.
|
||||
*
|
||||
* On success, the caller owns @p faces and @p vertices and must release them
|
||||
* with bu_free(). Failure leaves all outputs empty.
|
||||
*/
|
||||
ANALYZE_EXPORT extern enum analyze_mdc_status
|
||||
analyze_mdc(int **faces, size_t *num_faces, point_t **vertices,
|
||||
size_t *num_vertices, const char *object, struct db_i *dbip,
|
||||
const struct analyze_mdc_params *params);
|
||||
|
||||
__END_DECLS
|
||||
|
||||
#endif /* ANALYZE_CONTOUR_H */
|
||||
|
||||
/** @} */
|
||||
|
||||
/*
|
||||
* Local Variables:
|
||||
* mode: C
|
||||
* tab-width: 8
|
||||
* indent-tabs-mode: t
|
||||
* c-file-style: "stroustrup"
|
||||
* End:
|
||||
* ex: shiftwidth=4 tabstop=8
|
||||
*/
|
||||
@@ -43,9 +43,29 @@ struct analyze_polygonize_params {
|
||||
size_t minimum_free_mem;
|
||||
int verbosity;
|
||||
};
|
||||
#define ANALYZE_POLYGONIZE_PARAMS_DEFAULT { 0, 0, 30, 150000000, 0 }
|
||||
#define ANALYZE_POLYGONIZE_DEFAULT_MAX_CYCLE_TIME 30
|
||||
#define ANALYZE_POLYGONIZE_DEFAULT_MINIMUM_FREE_MEM 150000000
|
||||
#define ANALYZE_POLYGONIZE_PARAMS_DEFAULT { 0, 0, \
|
||||
ANALYZE_POLYGONIZE_DEFAULT_MAX_CYCLE_TIME, \
|
||||
ANALYZE_POLYGONIZE_DEFAULT_MINIMUM_FREE_MEM, 0 }
|
||||
|
||||
/**
|
||||
* Legacy polygonization interface, implemented by analyze_mdc().
|
||||
*
|
||||
* @p size is the requested maximum cell edge length. @p p_s is retained for
|
||||
* source and binary compatibility but is no longer used as a starting seed.
|
||||
* The caller owns successful output arrays and must release them with
|
||||
* bu_free(). Failure leaves all outputs empty.
|
||||
*
|
||||
* @return 0 on success
|
||||
* @return -1 on invalid input or polygonization failure
|
||||
* @return 2 when a time limit is reached
|
||||
* @return 3 when the minimum-free-memory limit is reached
|
||||
*/
|
||||
ANALYZE_EXPORT extern int
|
||||
analyze_polygonize(int **faces, int *num_faces, point_t **vertices, int *num_vertices, fastf_t size, point_t p_s, const char *obj, struct db_i *dbip, struct analyze_polygonize_params *p);
|
||||
analyze_polygonize(int **faces, int *num_faces, point_t **vertices,
|
||||
int *num_vertices, fastf_t size, point_t p_s, const char *obj,
|
||||
struct db_i *dbip, struct analyze_polygonize_params *p);
|
||||
|
||||
__END_DECLS
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
#include "bg/clip.h"
|
||||
#include "bg/lseg.h"
|
||||
#include "bg/obr.h"
|
||||
#include "bg/pca.h"
|
||||
#include "bg/plane.h"
|
||||
#include "bg/polygon.h"
|
||||
#include "bg/sat.h"
|
||||
|
||||
@@ -50,12 +50,12 @@ BG_EXPORT extern int bg_lseg_clip(fastf_t *xp1, fastf_t *yp1, fastf_t *xp2, fast
|
||||
* The RPP has faces parallel to the coordinate planes and is defined
|
||||
* by a minimum point and a maximum point.
|
||||
*
|
||||
* FIXME: the function name implies this takes a point,dir for a,b but
|
||||
* it actually takes a line segment going from points a to b!
|
||||
* Despite its historical name, this function clips the finite line segment
|
||||
* from a to b.
|
||||
*
|
||||
* Returns -
|
||||
* 0 if ray does not hit RPP,
|
||||
* !0 if ray hits RPP.
|
||||
* 0 if the segment does not hit the RPP,
|
||||
* !0 if the segment hits the RPP.
|
||||
*
|
||||
* Implicit Return -
|
||||
* if !0 was returned, "a" and "b" have been clipped to the RPP.
|
||||
|
||||
@@ -126,9 +126,12 @@ BG_EXPORT extern int bg_3d_coplanar_obr(point_t *center,
|
||||
* \endverbatim
|
||||
*
|
||||
*
|
||||
* @param[out] pnts eight points of oriented bounding box
|
||||
* @param points_3d array of 3D points
|
||||
* @param pnt_cnt number of points in pnts array
|
||||
* @param[out] pnts eight non-NULL, caller-provided point_t storage
|
||||
* locations for the oriented bounding box vertices
|
||||
* @param[in] points_3d flat array containing 3 * pnt_cnt coordinate values
|
||||
* @param[in] pnt_cnt number of points in points_3d
|
||||
*
|
||||
* @return 0 on success and -1 for invalid input or when no box can be fit
|
||||
*/
|
||||
BG_EXPORT extern int bg_3d_obb(point_t **pnts,
|
||||
const fastf_t *points_3d,
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
/*----------------------------------------------------------------------*/
|
||||
/** @addtogroup bg_pca
|
||||
*
|
||||
* Principle Component Analysis
|
||||
* Principal Component Analysis
|
||||
*
|
||||
* Calculates an XYZ coordinate system such that it aligns with the largest
|
||||
* variations in the supplied data. Intuitively, it "aligns" with the
|
||||
@@ -50,7 +50,7 @@
|
||||
* point_t p;
|
||||
* for (size_t i = 0; i < pntcnt; i++) {
|
||||
* MAT4X3PNT(p, RT, pnts[i]);
|
||||
* VMOVE(pnts[i], v);
|
||||
* VMOVE(pnts[i], p);
|
||||
* }
|
||||
* @endcode
|
||||
*/
|
||||
@@ -69,7 +69,7 @@ __BEGIN_DECLS
|
||||
|
||||
/**
|
||||
* @brief
|
||||
* Perform a Principle Component Analysis on a set of points.
|
||||
* Perform a Principal Component Analysis on a set of points.
|
||||
*
|
||||
* Outputs are a center point and XYZ vectors for the coordinate system.
|
||||
*
|
||||
@@ -88,7 +88,7 @@ BG_EXPORT extern int bg_pca(point_t *c, vect_t *xaxis, vect_t *yaxis, vect_t *za
|
||||
|
||||
__END_DECLS
|
||||
|
||||
#endif /* BG_PLANE_H */
|
||||
#endif /* BG_PCA_H */
|
||||
/** @} */
|
||||
/*
|
||||
* Local Variables:
|
||||
|
||||
@@ -1171,9 +1171,10 @@ BG_EXPORT extern int bg_isect_planes(point_t pt,
|
||||
|
||||
/**
|
||||
* @brief
|
||||
* Given an origin and a normal, create a plane_t.
|
||||
* Given an origin and a normal, create a plane_t with a unit normal.
|
||||
* The input point and normal are not modified. A zero-length normal fails.
|
||||
*/
|
||||
BG_EXPORT extern int bg_plane_pt_nrml(plane_t *p, point_t pt, vect_t nrml);
|
||||
BG_EXPORT extern int bg_plane_pt_nrml(plane_t *p, const point_t pt, const vect_t nrml);
|
||||
|
||||
/**
|
||||
* @brief
|
||||
@@ -1188,13 +1189,15 @@ BG_EXPORT extern int bg_fit_plane(point_t *c, vect_t *n, size_t npnts, point_t *
|
||||
|
||||
/**
|
||||
* @brief
|
||||
* Find the closest U,V point on the plane p to 3d point pt.
|
||||
* Find the closest U,V point on the plane p to 3d point pt. The plane normal
|
||||
* need not be unit length.
|
||||
*/
|
||||
BG_EXPORT extern int bg_plane_closest_pt(fastf_t *u, fastf_t *v, plane_t *p, point_t *pt);
|
||||
|
||||
/**
|
||||
* @brief
|
||||
* Return the 3D point on the plane at parametric coordinates u, v.
|
||||
* Return the 3D point on the plane at parametric coordinates u, v. The plane
|
||||
* normal need not be unit length.
|
||||
*/
|
||||
BG_EXPORT extern int bg_plane_pt_at(point_t *pt, plane_t *p, fastf_t u, fastf_t v);
|
||||
|
||||
|
||||
@@ -293,6 +293,7 @@ BG_EXPORT extern int bg_3d_polygon_area(fastf_t *area, size_t npts, const point_
|
||||
* @param[in] npts Number of point_ts, stored in pts
|
||||
* @param[in] pts all points of the polygon, sorted counter-clockwise.
|
||||
* The array mustn't contain duplicated points or non-coplanar points.
|
||||
* A zero-area polygon fails.
|
||||
*
|
||||
* @return 0 if calculation was successful
|
||||
* @return 1 if calculation failed, e.g. because one in-parameter is a NULL-pointer
|
||||
@@ -307,7 +308,7 @@ BG_EXPORT extern int bg_3d_polygon_centroid(point_t *cent, size_t npts, const po
|
||||
*@param[in] npts Number of points, pts contains
|
||||
*@param pts Array of point_ts, building a convex polygon. Duplicated points
|
||||
*aren't allowed. The points in the array will be sorted counter-clockwise.
|
||||
*@param[in] cmp Plane equation of the polygon
|
||||
*@param[in] cmp Plane equation of the polygon. Its normal must be nonzero.
|
||||
*
|
||||
*@return 0 if calculation was successful
|
||||
*@return 1 if calculation failed, e.g. because pts is a NULL-pointer
|
||||
|
||||
@@ -39,7 +39,9 @@
|
||||
__BEGIN_DECLS
|
||||
|
||||
/**
|
||||
* Test for an intersection between a line and an Axis-Aligned Bounding Box (AABB).
|
||||
* Test for an intersection between an infinite line and an Axis-Aligned
|
||||
* Bounding Box (AABB). ldir need not be unit length, but must be nonzero.
|
||||
* aabb_extent supplies the positive half extents along the coordinate axes.
|
||||
*
|
||||
* Returns 1 if they intersect, 0 otherwise.
|
||||
*/
|
||||
@@ -50,7 +52,10 @@ bg_sat_line_aabb(
|
||||
);
|
||||
|
||||
/**
|
||||
* Test for an intersection between a line and an Oriented Bounding Box (OBB).
|
||||
* Test for an intersection between an infinite line and an Oriented Bounding
|
||||
* Box (OBB). ldir need not be unit length, but must be nonzero. Each OBB
|
||||
* extent vector points from the center to a face center; nonzero vectors must
|
||||
* be mutually perpendicular, and a zero vector represents a degenerate axis.
|
||||
*
|
||||
* Returns 1 if they intersect, 0 otherwise.
|
||||
*/
|
||||
@@ -61,7 +66,9 @@ bg_sat_line_obb(
|
||||
);
|
||||
|
||||
/**
|
||||
* Test for an intersection between a triangle and an Axis-Aligned Bounding Box (AABB).
|
||||
* Test for an intersection between a triangle and an Axis-Aligned Bounding
|
||||
* Box (AABB). extent supplies the positive half extents along the coordinate
|
||||
* axes.
|
||||
*
|
||||
* Returns 1 if they intersect, 0 otherwise.
|
||||
*/
|
||||
@@ -72,7 +79,10 @@ bg_sat_tri_aabb(
|
||||
);
|
||||
|
||||
/**
|
||||
* Test for an intersection between a triangle and an Oriented Bounding Box (OBB).
|
||||
* Test for an intersection between a triangle and an Oriented Bounding Box
|
||||
* (OBB). Each extent vector points from the center to a face center; nonzero
|
||||
* vectors must be mutually perpendicular, and a zero vector represents a
|
||||
* degenerate axis.
|
||||
*
|
||||
* Returns 1 if they intersect, 0 otherwise.
|
||||
*/
|
||||
@@ -85,8 +95,9 @@ bg_sat_tri_obb(
|
||||
/**
|
||||
* Test for an intersection between an Axis-Aligned Bounding Box (AABB) and an
|
||||
* Oriented Bounding Box (OBB). The latter is defined by a center point and
|
||||
* three perpendicular vectors from the center to the centers of the various
|
||||
* faces.
|
||||
* three extent vectors from the center to the centers of the various faces.
|
||||
* Nonzero extent vectors must be mutually perpendicular; a zero vector
|
||||
* represents a degenerate axis.
|
||||
*
|
||||
* Returns 1 if they intersect, 0 otherwise.
|
||||
*/
|
||||
@@ -98,8 +109,9 @@ bg_sat_aabb_obb(
|
||||
|
||||
/**
|
||||
* Test for an intersection between two Oriented Bounding Boxes (OBBs). The
|
||||
* boxes are defined by a center point and three perpendicular vectors from the
|
||||
* center to the centers of the various faces.
|
||||
* boxes are defined by a center point and three extent vectors from the center
|
||||
* to the centers of the various faces. Nonzero extent vectors must be
|
||||
* mutually perpendicular; a zero vector represents a degenerate axis.
|
||||
*
|
||||
* Returns 1 if they intersect, 0 otherwise.
|
||||
*/
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
#define BG_SPSR_H
|
||||
|
||||
#include "common.h"
|
||||
#include <stdint.h>
|
||||
#include "vmath.h"
|
||||
#include "bg/defines.h"
|
||||
|
||||
@@ -109,7 +110,7 @@ struct bg_3d_spsr_opts {
|
||||
#define BG_3D_SPSR_BOUNDARY_DIRICHLET 3
|
||||
|
||||
#define BG_3D_SPSR_DEFAULT_DEGREE 1 /* DEFAULT_FEM_DEGREE */
|
||||
#define BG_3D_SPSR_DEFAULT_DEPTH 11
|
||||
#define BG_3D_SPSR_DEFAULT_DEPTH 8
|
||||
#define BG_3D_SPSR_DEFAULT_KERNELDEPTH 0
|
||||
#define BG_3D_SPSR_DEFAULT_ITERATIONS 0
|
||||
#define BG_3D_SPSR_DEFAULT_FULL_DEPTH 0
|
||||
@@ -117,13 +118,13 @@ struct bg_3d_spsr_opts {
|
||||
#define BG_3D_SPSR_DEFAULT_BASEVCYCLES 0
|
||||
#define BG_3D_SPSR_DEFAULT_MAX_MEM 0
|
||||
#define BG_3D_SPSR_DEFAULT_THREADS 0
|
||||
#define BG_3D_SPSR_DEFAULT_SAMPLES_PER_NODE 1.1
|
||||
#define BG_3D_SPSR_DEFAULT_SCALE 1.0
|
||||
#define BG_3D_SPSR_DEFAULT_SAMPLES_PER_NODE 1.5
|
||||
#define BG_3D_SPSR_DEFAULT_SCALE 1.1
|
||||
#define BG_3D_SPSR_DEFAULT_WIDTH 0.0
|
||||
#define BG_3D_SPSR_DEFAULT_CONFIDENCE 0.0
|
||||
#define BG_3D_SPSR_DEFAULT_CONFIDENCE_BIAS 0.0
|
||||
#define BG_3D_SPSR_DEFAULT_CGSOLVER_ACCURACY 1.0e-3
|
||||
#define BG_3D_SPSR_DEFAULT_POINT_WEIGHT 8.0 /* DefaultPointWeightMultiplier * Degree */
|
||||
#define BG_3D_SPSR_DEFAULT_POINT_WEIGHT 2.0
|
||||
#define BG_3D_SPSR_DEFAULT_NONMANIFOLD 0
|
||||
#define BG_3D_SPSR_DEFAULT_LINEARFIT 0
|
||||
#define BG_3D_SPSR_DEFAULT_EXACT 1
|
||||
@@ -167,10 +168,131 @@ struct bg_3d_spsr_opts {
|
||||
* @return 0 if successful, else error
|
||||
*
|
||||
*/
|
||||
BG_EXPORT int bg_3d_spsr(int **faces, int *num_faces, point_t **vertices, int *num_vertices,
|
||||
DEPRECATED BG_EXPORT int bg_3d_spsr(int **faces, int *num_faces, point_t **vertices, int *num_vertices,
|
||||
const point_t *input_points_3d, const vect_t *input_normals_3d,
|
||||
int num_input_pnts, struct bg_3d_spsr_opts *opts);
|
||||
|
||||
/** Reasons the SPSR solver may request additional source samples. */
|
||||
#define BG_3D_SPSR_REFINE_RESIDUAL 0x01u
|
||||
#define BG_3D_SPSR_REFINE_NORMAL 0x02u
|
||||
#define BG_3D_SPSR_REFINE_DENSITY 0x04u
|
||||
#define BG_3D_SPSR_REFINE_SURFACE_VARIATION 0x08u
|
||||
|
||||
/** Adaptive SPSR termination states. */
|
||||
#define BG_3D_SPSR_COMPLETE 0
|
||||
#define BG_3D_SPSR_CALLBACK_STOP 1
|
||||
#define BG_3D_SPSR_POINT_LIMIT 2
|
||||
#define BG_3D_SPSR_TIME_LIMIT 3
|
||||
#define BG_3D_SPSR_NO_NEW_SAMPLES 4
|
||||
#define BG_3D_SPSR_SOLVER_ERROR 5
|
||||
|
||||
/**
|
||||
* One oriented source sample. A positive @p thickness records the length of
|
||||
* the solid ray partition that supplied the sample. Non-zero equal pair IDs
|
||||
* identify the entry and exit samples of the same partition.
|
||||
*/
|
||||
struct bg_3d_spsr_sample {
|
||||
point_t point;
|
||||
vect_t normal;
|
||||
fastf_t thickness;
|
||||
uint64_t pair_id;
|
||||
};
|
||||
|
||||
/** A spatial location at which the solver would benefit from more samples. */
|
||||
struct bg_3d_spsr_refinement_hint {
|
||||
point_t point;
|
||||
vect_t normal;
|
||||
fastf_t error;
|
||||
unsigned int reasons;
|
||||
};
|
||||
|
||||
/** Source-to-candidate measurements supplied by an adaptive callback. */
|
||||
struct bg_3d_spsr_validation {
|
||||
size_t ray_count;
|
||||
size_t source_hit_rays;
|
||||
size_t partition_mismatch_rays;
|
||||
size_t thin_partition_count;
|
||||
size_t thin_partition_mismatches;
|
||||
fastf_t endpoint_error_p95;
|
||||
fastf_t endpoint_error_p99;
|
||||
fastf_t chord_error_fraction;
|
||||
fastf_t surface_area_error_fraction;
|
||||
fastf_t volume_error_fraction;
|
||||
int passed;
|
||||
};
|
||||
|
||||
/** Information passed to an adaptive source-sampling callback. */
|
||||
struct bg_3d_spsr_refinement_request {
|
||||
size_t pass;
|
||||
fastf_t target_feature_size;
|
||||
double remaining_time;
|
||||
const point_t *vertices;
|
||||
size_t vertex_count;
|
||||
const int *faces;
|
||||
size_t face_count;
|
||||
const struct bg_3d_spsr_refinement_hint *hints;
|
||||
size_t hint_count;
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback response. Sample storage remains owned by the callback and need
|
||||
* only remain valid until the callback returns; libbg copies it immediately.
|
||||
*/
|
||||
struct bg_3d_spsr_refinement_response {
|
||||
const struct bg_3d_spsr_sample *samples;
|
||||
size_t sample_count;
|
||||
struct bg_3d_spsr_validation validation;
|
||||
/** Non-zero asks the adaptive driver to stop after this candidate. */
|
||||
int stop_refinement;
|
||||
};
|
||||
|
||||
typedef int (*bg_3d_spsr_refinement_func_t)(
|
||||
struct bg_3d_spsr_refinement_response *response,
|
||||
const struct bg_3d_spsr_refinement_request *request,
|
||||
void *client_data);
|
||||
|
||||
/** Options controlling a bounded adaptive SPSR reconstruction. */
|
||||
struct bg_3d_spsr_adaptive_opts {
|
||||
struct bg_3d_spsr_opts solver;
|
||||
size_t max_refinement_passes;
|
||||
size_t max_points;
|
||||
fastf_t target_feature_size;
|
||||
double max_time;
|
||||
};
|
||||
|
||||
/** Last-resort solve budget; callbacks may stop earlier when progress stalls. */
|
||||
#define BG_3D_SPSR_DEFAULT_REFINEMENT_PASSES 8
|
||||
#define BG_3D_SPSR_ADAPTIVE_OPTS_DEFAULT { \
|
||||
BG_3D_SPSR_OPTS_DEFAULT, \
|
||||
BG_3D_SPSR_DEFAULT_REFINEMENT_PASSES, \
|
||||
0, \
|
||||
0.0, \
|
||||
0.0 }
|
||||
|
||||
/** Summary of an adaptive SPSR attempt. */
|
||||
struct bg_3d_spsr_report {
|
||||
size_t solve_count;
|
||||
size_t initial_sample_count;
|
||||
size_t final_sample_count;
|
||||
size_t requested_sample_count;
|
||||
size_t accepted_sample_count;
|
||||
double elapsed_time;
|
||||
int termination;
|
||||
struct bg_3d_spsr_validation validation;
|
||||
};
|
||||
|
||||
/**
|
||||
* Reconstruct a surface and, when requested, obtain targeted additional
|
||||
* samples from @p refine. A callback is required when refinement passes are
|
||||
* enabled. The final candidate must be marked as passed by the callback.
|
||||
*/
|
||||
BG_EXPORT int bg_3d_spsr_adaptive(int **faces, int *num_faces,
|
||||
point_t **vertices, int *num_vertices,
|
||||
const struct bg_3d_spsr_sample *input_samples, size_t sample_count,
|
||||
const struct bg_3d_spsr_adaptive_opts *opts,
|
||||
bg_3d_spsr_refinement_func_t refine, void *client_data,
|
||||
struct bg_3d_spsr_report *report);
|
||||
|
||||
__END_DECLS
|
||||
|
||||
#endif /* BG_SPSR_H */
|
||||
|
||||
@@ -52,7 +52,7 @@ BG_EXPORT extern double bg_tri_closest_pt(
|
||||
|
||||
__END_DECLS
|
||||
|
||||
#endif /* BG_TRI_TRI_H */
|
||||
#endif /* BG_TRI_PT_H */
|
||||
/** @} */
|
||||
/*
|
||||
* Local Variables:
|
||||
|
||||
@@ -54,7 +54,7 @@ BG_EXPORT extern int bg_isect_tri_line(point_t *isect,
|
||||
* If isect is non-NULL, it is written only when an intersection is found.
|
||||
*/
|
||||
BG_EXPORT extern int bg_isect_tri_ray(const point_t orig,
|
||||
const point_t dir,
|
||||
const vect_t dir,
|
||||
const point_t vert0,
|
||||
const point_t vert1,
|
||||
const point_t vert2,
|
||||
|
||||
@@ -87,7 +87,11 @@ BG_EXPORT extern int bg_trimesh_oriented(int vcnt, int fcnt, fastf_t *v, int *f)
|
||||
/**
|
||||
* Check if a mesh is topologically solid. Returns 1 if the mesh is NOT SOLID
|
||||
* and 0 if the mesh is SOLID. A SOLID (0) outcome indicates the mesh satisfies
|
||||
* all three criteria: Closed, Manifold, Oriented
|
||||
* all three criteria: Closed, Manifold, Oriented. If @p bedges is non-NULL,
|
||||
* it is set to NULL when no edge diagnostics are present, or to a bu_calloc()
|
||||
* allocated array of vertex-index pairs for unmatched, misoriented, and excess
|
||||
* edges. The caller must bu_free() a non-NULL result. Use
|
||||
* bg_trimesh_solid2() when structured diagnostics and their counts are needed.
|
||||
*/
|
||||
BG_EXPORT extern int bg_trimesh_solid(int vcnt, int fcnt, fastf_t *v, int *f, int **bedges);
|
||||
|
||||
|
||||
@@ -214,8 +214,8 @@ BN_EXPORT extern void bn_mat_ae(mat_t m,
|
||||
double elev);
|
||||
|
||||
/**
|
||||
* Find the azimuth and elevation angles that correspond to the
|
||||
* direction (not including twist) given by a direction vector.
|
||||
* Find the azimuth and elevation angles, in degrees, that correspond
|
||||
* to the direction (not including twist) given by a direction vector.
|
||||
*/
|
||||
BN_EXPORT extern void bn_ae_vec(fastf_t *azp,
|
||||
fastf_t *elp,
|
||||
@@ -236,14 +236,16 @@ BN_EXPORT extern void bn_aet_vec(fastf_t *az,
|
||||
fastf_t accuracy);
|
||||
|
||||
/**
|
||||
* Find a unit vector from the origin given azimuth and elevation.
|
||||
* Find a unit vector from the origin given azimuth and elevation in
|
||||
* radians.
|
||||
*/
|
||||
BN_EXPORT extern void bn_vec_ae(vect_t vec,
|
||||
fastf_t az,
|
||||
fastf_t el);
|
||||
|
||||
/**
|
||||
* Find a vector from the origin given azimuth, elevation, and distance.
|
||||
* Find a vector from the origin given azimuth and elevation in radians,
|
||||
* and distance.
|
||||
*/
|
||||
BN_EXPORT extern void bn_vec_aed(vect_t vec,
|
||||
fastf_t az,
|
||||
@@ -310,8 +312,10 @@ BN_EXPORT extern void bn_vec_perp(vect_t new_vec,
|
||||
* space by the angle between the two. There are many candidate
|
||||
* matrices.
|
||||
*
|
||||
* The input 'from' and 'to' vectors need not be unit length.
|
||||
* MAT4X3VEC(to, m, from) is the identity that is created.
|
||||
* The input 'from' and 'to' vectors need not be unit length. They are
|
||||
* normalized internally. Near-parallel inputs are handled using the
|
||||
* angular tolerance in 'tol'. If either input cannot be normalized,
|
||||
* the result is the identity matrix.
|
||||
*
|
||||
*/
|
||||
BN_EXPORT extern void bn_mat_fromto(mat_t m,
|
||||
@@ -344,12 +348,11 @@ BN_EXPORT extern void bn_mat_zrot(mat_t m,
|
||||
double cosz);
|
||||
|
||||
/**
|
||||
* Given a direction vector D of unit length, product a matrix which
|
||||
* rotates that vector D onto the -Z axis. This matrix will be
|
||||
* suitable for use as a "model2view" matrix.
|
||||
*
|
||||
* XXX This routine will fail if the vector is already more or less
|
||||
* aligned with the Z axis.
|
||||
* Given a nonzero direction vector D, produce a matrix which
|
||||
* rotates D onto the -Z axis. D is normalized internally. Z-axis
|
||||
* directions use a deterministic roll, and a zero direction produces
|
||||
* the identity matrix. This matrix will be suitable for use as a
|
||||
* model2view matrix.
|
||||
*
|
||||
* This is done in several steps.
|
||||
*
|
||||
|
||||
@@ -37,11 +37,42 @@ __BEGIN_DECLS
|
||||
|
||||
typedef struct bn_multipoly {
|
||||
uint32_t magic;
|
||||
/**< Number of coefficients in the first polynomial variable. */
|
||||
int dgrs;
|
||||
/**< Number of coefficients in the second polynomial variable. */
|
||||
int dgrt;
|
||||
/**< Coefficient of S^s * T^t is cf[s][t]. */
|
||||
double **cf;
|
||||
} bn_multipoly_t;
|
||||
|
||||
/**
|
||||
* Allocate a zero-valued bivariate polynomial with the specified
|
||||
* coefficient counts. Both counts must be positive.
|
||||
*/
|
||||
BN_EXPORT extern struct bn_multipoly *bn_multipoly_new(int dgrs, int dgrt);
|
||||
|
||||
/** Release a bivariate polynomial allocated by bn_multipoly_new(). */
|
||||
BN_EXPORT extern void bn_multipoly_free(struct bn_multipoly *poly);
|
||||
|
||||
/** Grow a polynomial to at least the specified coefficient counts. */
|
||||
BN_EXPORT extern struct bn_multipoly *bn_multipoly_grow(struct bn_multipoly *poly,
|
||||
int dgrs,
|
||||
int dgrt);
|
||||
|
||||
/** Set the coefficient of S^s * T^t, growing the polynomial as needed. */
|
||||
BN_EXPORT extern struct bn_multipoly *bn_multipoly_set(struct bn_multipoly *poly,
|
||||
int s,
|
||||
int t,
|
||||
double value);
|
||||
|
||||
/** Return the sum of two bivariate polynomials. */
|
||||
BN_EXPORT extern struct bn_multipoly *bn_multipoly_add(const struct bn_multipoly *p1,
|
||||
const struct bn_multipoly *p2);
|
||||
|
||||
/** Return the product of two bivariate polynomials. */
|
||||
BN_EXPORT extern struct bn_multipoly *bn_multipoly_mul(const struct bn_multipoly *p1,
|
||||
const struct bn_multipoly *p2);
|
||||
|
||||
__END_DECLS
|
||||
|
||||
#endif /* BN_MULTIPOLY_H */
|
||||
|
||||
@@ -45,7 +45,10 @@ __BEGIN_DECLS
|
||||
|
||||
/**
|
||||
* Polynomial data type
|
||||
* bn_poly->cf[n] corresponds to X^n
|
||||
*
|
||||
* Coefficients are stored from the highest to the lowest degree:
|
||||
* `cf[0]` is the coefficient of `X^dgr` and `cf[dgr]` is the
|
||||
* constant coefficient.
|
||||
*/
|
||||
typedef struct bn_poly {
|
||||
uint32_t magic;
|
||||
@@ -63,6 +66,9 @@ typedef struct bn_poly {
|
||||
*
|
||||
* @brief
|
||||
* multiply two polynomials
|
||||
*
|
||||
* The output may alias either input. If the result exceeds
|
||||
* BN_MAX_POLY_DEGREE, returns BN_POLY_NULL without modifying product.
|
||||
*/
|
||||
BN_EXPORT extern struct bn_poly *bn_poly_mul(struct bn_poly *product,
|
||||
const struct bn_poly *m1,
|
||||
@@ -81,6 +87,8 @@ BN_EXPORT extern struct bn_poly *bn_poly_scale(struct bn_poly *eqn,
|
||||
* bn_poly_add
|
||||
* @brief
|
||||
* add two polynomials
|
||||
*
|
||||
* The output may alias either input.
|
||||
*/
|
||||
BN_EXPORT extern struct bn_poly *bn_poly_add(struct bn_poly *sum,
|
||||
const struct bn_poly *poly1,
|
||||
@@ -90,6 +98,8 @@ BN_EXPORT extern struct bn_poly *bn_poly_add(struct bn_poly *sum,
|
||||
* bn_poly_sub
|
||||
* @brief
|
||||
* subtract two polynomials
|
||||
*
|
||||
* The output may alias either input.
|
||||
*/
|
||||
BN_EXPORT extern struct bn_poly *bn_poly_sub(struct bn_poly *diff,
|
||||
const struct bn_poly *poly1,
|
||||
@@ -98,7 +108,10 @@ BN_EXPORT extern struct bn_poly *bn_poly_sub(struct bn_poly *diff,
|
||||
/**
|
||||
* @brief
|
||||
* Divides any polynomial into any other polynomial using synthetic
|
||||
* division. Both polynomials must have real coefficients.
|
||||
* division. Both polynomials must have real coefficients and the
|
||||
* divisor's leading coefficient must be nonzero. The quotient and
|
||||
* remainder outputs must be distinct; the quotient may alias the
|
||||
* dividend. All other output/input pairs must be distinct.
|
||||
*/
|
||||
BN_EXPORT extern void bn_poly_synthetic_division(struct bn_poly *quo,
|
||||
struct bn_poly *rem,
|
||||
@@ -108,7 +121,8 @@ BN_EXPORT extern void bn_poly_synthetic_division(struct bn_poly *quo,
|
||||
/**
|
||||
*@brief
|
||||
* Uses the quadratic formula to find the roots (in `complex' form) of
|
||||
* any quadratic equation with real coefficients.
|
||||
* any quadratic equation with real coefficients. A nonzero leading
|
||||
* coefficient need not be one.
|
||||
*
|
||||
* @return 1 for success
|
||||
* @return 0 for fail.
|
||||
@@ -144,6 +158,7 @@ BN_EXPORT extern int bn_poly_quadratic_roots(struct bn_complex roots[],
|
||||
* If D < 0, there will be three unequal real roots.
|
||||
*
|
||||
* Returns 1 for success, 0 for fail.
|
||||
* A nonzero leading coefficient need not be one.
|
||||
*/
|
||||
BN_EXPORT extern int bn_poly_cubic_roots(struct bn_complex roots[],
|
||||
const struct bn_poly *eqn);
|
||||
@@ -151,7 +166,8 @@ BN_EXPORT extern int bn_poly_cubic_roots(struct bn_complex roots[],
|
||||
/**
|
||||
*@brief
|
||||
* Uses the quartic formula to find the roots (in `complex' form)
|
||||
* of any quartic equation with real coefficients.
|
||||
* of any quartic equation with real coefficients. A nonzero leading
|
||||
* coefficient need not be one.
|
||||
*
|
||||
* @return 1 for success
|
||||
* @return 0 for fail.
|
||||
|
||||
@@ -100,8 +100,8 @@ BN_EXPORT extern void quat_double(quat_t qout,
|
||||
/**
|
||||
*@brief Gives the bisector of quaternions q1 and q2.
|
||||
*
|
||||
* (Could be done with quat_slerp and factor 0.5)
|
||||
* [I believe they must be unit quaternions this to work]
|
||||
* Inputs must be unit quaternions. Antipodal inputs use a valid
|
||||
* great-circle midpoint.
|
||||
*/
|
||||
BN_EXPORT extern void quat_bisect(quat_t qout,
|
||||
const quat_t q1,
|
||||
@@ -161,7 +161,8 @@ BN_EXPORT extern void quat_exp(quat_t out,
|
||||
|
||||
/**
|
||||
*@brief
|
||||
* Take the natural logarithm of a unit quaternion.
|
||||
* Take the natural logarithm of a unit quaternion. The identity
|
||||
* quaternion maps to the zero quaternion.
|
||||
* Code by Ken Shoemake.
|
||||
*/
|
||||
BN_EXPORT extern void quat_log(quat_t out,
|
||||
|
||||
@@ -205,23 +205,30 @@ BU_EXPORT extern int bu_sscanf(const char *src, const char *fmt, ...) _BU_ATTR_S
|
||||
/** Routines for scanning certain kinds of data. */
|
||||
|
||||
/**
|
||||
* Scans a sequence of fastf_t numbers from a string or stdin
|
||||
* Scans a sequence of fastf_t numbers from a string or stdin.
|
||||
*
|
||||
* Scanning fastf_t numbers with bu_sscanf() is difficult, because
|
||||
* doing so requires scanning to some intermediate type like double
|
||||
* and then assigning to the fastf_t variable to convert the value to
|
||||
* whatever type fastf_t really is. This function makes it possible
|
||||
* to scan a series of fastf_t numbers separated by some character(s)
|
||||
* easily, by doing the required conversion internally to the
|
||||
* functions. As series of delimiter characters will be skipped,
|
||||
* empty scan fields are not supported (e.g., "0.0,,0.0,1.0" will scan
|
||||
* as 3 fields, not 4 with the 2nd skipped).
|
||||
* to scan a series of fastf_t numbers easily, by doing the required
|
||||
* conversion internally. Delimiters form a non-empty character set:
|
||||
* one or more delimiter characters must separate values, and repeated
|
||||
* delimiters are skipped. Empty scan fields are therefore not
|
||||
* supported (e.g., "0.0,,0.0,1.0" scans as 3 fields, not 4 with the
|
||||
* second skipped). A trailing delimiter is not consumed after the
|
||||
* final requested value.
|
||||
*
|
||||
* @param[out] c Returns number of characters scanned by the function
|
||||
* @param[out] c Optional destination for the number of input characters
|
||||
* consumed. It is set to zero before scanning and remains zero for
|
||||
* invalid arguments or when no value is scanned.
|
||||
* @param[in] src A source string to scan from, or NULL to read from stdin
|
||||
* @param[in] delim Any delimiter character(s) to skip between scan values
|
||||
* @param[in] delim Non-empty set of delimiter characters to skip between
|
||||
* scan values
|
||||
* @param[in] n Number of fastf_t values to scan from the src input string
|
||||
* @param[out] ... Pointers to fastf_t for storing scanned values (optional)
|
||||
* @param[out] ... Exactly n fastf_t pointers for storing scanned values;
|
||||
* individual pointers may be NULL
|
||||
* @return Number of values scanned
|
||||
*
|
||||
*/
|
||||
BU_EXPORT extern int bu_scan_fastf_t(int *c, const char *src, const char *delim, size_t n, ...);
|
||||
|
||||
@@ -103,10 +103,23 @@ BU_EXPORT extern void bu_nice_set(int newnice);
|
||||
|
||||
/**
|
||||
* Return the maximum number of physical CPUs that are considered to
|
||||
* be available to this process now.
|
||||
* be available to this process now, subject to any process-local limit set by
|
||||
* bu_avail_cpus_set().
|
||||
*/
|
||||
BU_EXPORT extern size_t bu_avail_cpus(void);
|
||||
|
||||
/**
|
||||
* Limit the CPU count reported by bu_avail_cpus() and honored by subsequent
|
||||
* bu_parallel() calls in this process. A value of zero removes the limit.
|
||||
* Call this before starting worker threads; changing the limit does not resize
|
||||
* work that is already running.
|
||||
*
|
||||
* This is useful when several independent processes share one machine and
|
||||
* each process contains code that selects its own parallelism with
|
||||
* bu_avail_cpus().
|
||||
*/
|
||||
BU_EXPORT extern void bu_avail_cpus_set(size_t ncpu);
|
||||
|
||||
|
||||
/**
|
||||
* Create parallel threads of execution.
|
||||
|
||||
@@ -55,7 +55,7 @@ __BEGIN_DECLS
|
||||
* version 5 UUID suitable for repeatable identifier hashing.
|
||||
*/
|
||||
BU_EXPORT int
|
||||
bu_uuid_create(uint8_t uuid[STATIC_ARRAY(16)], size_t nbytes, const uint8_t *bytes, const uint8_t namespace_uuid[STATIC_ARRAY(16)]);
|
||||
bu_uuid_create(uint8_t uuid[STATIC_ARRAY(16)], size_t nbytes, const uint8_t *bytes, const uint8_t *namespace_uuid);
|
||||
|
||||
/**
|
||||
* This is a convenience UUID comparison routine compatible with
|
||||
|
||||
@@ -84,7 +84,7 @@ __BEGIN_DECLS
|
||||
#define RT_DEBUG_HF 0x04000000 /**< @brief 27 Height Field solids */
|
||||
|
||||
#define RT_DEBUG_MESHING 0x08000000 /**< @brief 28 Print meshing/triangulation details */
|
||||
#define RT_DEBUG_UNUSED_3 0x10000000 /**< @brief 29 Unassigned */
|
||||
#define RT_DEBUG_RTCMP 0x10000000 /**< @brief 29 Set up rtcmp capture; recording uses a runtime toggle. If LIBRT_RTCMP_PRIMITIVES=1 includes primitive segments. */
|
||||
#define RT_DEBUG_UNUSED_4 0x20000000 /**< @brief 30 Unassigned */
|
||||
|
||||
/* Options which will cause the library to write binary debugging output */
|
||||
@@ -96,7 +96,7 @@ __BEGIN_DECLS
|
||||
"\040PL_BOX" \
|
||||
"\037PL_SOLIDS" \
|
||||
"\036UNUSED_4" \
|
||||
"\035UNUSED_3" \
|
||||
"\035RTCMP" \
|
||||
"\034UNUSED_2" \
|
||||
"\033HF" \
|
||||
"\032EBM" \
|
||||
|
||||
@@ -465,6 +465,17 @@ rt_get_solid_keypoint(struct rt_edit *s, point_t *pt, const char **strp, fastf_t
|
||||
RT_EXPORT extern void
|
||||
rt_edit_set_edflag(struct rt_edit *s, int edflag);
|
||||
|
||||
/**
|
||||
* Set a parameter-edit translation target expressed in model base units.
|
||||
*
|
||||
* rt_edit's parameter-edit handlers consume e_para in database local units,
|
||||
* while keypoints and edit axes are maintained in model base units. This
|
||||
* helper performs the boundary conversion for callers that have computed a
|
||||
* target from a keypoint, axis, or view transform.
|
||||
*/
|
||||
RT_EXPORT extern void
|
||||
rt_edit_set_translation_target(struct rt_edit *s, const point_t target);
|
||||
|
||||
RT_EXPORT extern int
|
||||
rt_edit_knob_cmd_process(
|
||||
struct rt_edit *s,
|
||||
@@ -499,8 +510,10 @@ rt_knob_edit_sca(
|
||||
int matrix_edit);
|
||||
|
||||
/* Equivalent to sedit - run editing logic after input data is set in
|
||||
* rt_edit container */
|
||||
RT_EXPORT extern void
|
||||
* rt_edit container. Transient input flags are consumed whether the edit
|
||||
* succeeds or fails; parameter values remain available to the caller.
|
||||
* Returns BRLCAD_ERROR if the primitive edit fails. */
|
||||
RT_EXPORT extern int
|
||||
rt_edit_process(struct rt_edit *s);
|
||||
|
||||
/**
|
||||
@@ -521,7 +534,7 @@ rt_edit_snap_point(point2d_t pt, const struct rt_edit *s);
|
||||
* restored later with rt_edit_revert().
|
||||
*
|
||||
* The snapshot is stored inside the rt_edit struct. Calling this
|
||||
* function again overwrites any previous snapshot (single-level undo).
|
||||
* function again replaces the previous snapshot only on success.
|
||||
*
|
||||
* @return BRLCAD_OK on success, BRLCAD_ERROR if the export failed.
|
||||
*/
|
||||
@@ -532,8 +545,8 @@ rt_edit_checkpoint(struct rt_edit *s);
|
||||
* Restore primitive parameters from the snapshot saved by
|
||||
* rt_edit_checkpoint().
|
||||
*
|
||||
* If no snapshot has been saved (or the last snapshot was already
|
||||
* consumed) this function logs a message and returns BRLCAD_ERROR.
|
||||
* The snapshot remains available for repeated restores. If none has
|
||||
* been saved, this function logs a message and returns BRLCAD_ERROR.
|
||||
*
|
||||
* @return BRLCAD_OK on success, BRLCAD_ERROR otherwise.
|
||||
*/
|
||||
@@ -697,7 +710,11 @@ enum rt_constraint_edit_op_kind {
|
||||
RT_CONSTRAINT_EDIT_OP_SET_BEND,
|
||||
RT_CONSTRAINT_EDIT_OP_SCALE_OD,
|
||||
RT_CONSTRAINT_EDIT_OP_SCALE_ID,
|
||||
RT_CONSTRAINT_EDIT_OP_SCALE_BEND
|
||||
RT_CONSTRAINT_EDIT_OP_SCALE_BEND,
|
||||
/* Set every point when an all-zero dimension has no scale reference. */
|
||||
RT_CONSTRAINT_EDIT_OP_SET_ALL_OD,
|
||||
RT_CONSTRAINT_EDIT_OP_SET_ALL_ID,
|
||||
RT_CONSTRAINT_EDIT_OP_SET_ALL_BEND
|
||||
};
|
||||
|
||||
struct rt_constraint_edit_param_ref {
|
||||
|
||||
@@ -227,6 +227,32 @@ struct rt_crofton_params {
|
||||
double time_ms; /**< wall-clock time budget (ms); 0 = disabled */
|
||||
};
|
||||
|
||||
/** One solid partition observed by a Crofton ray. */
|
||||
struct rt_crofton_segment {
|
||||
point_t in_point;
|
||||
vect_t in_normal;
|
||||
point_t out_point;
|
||||
vect_t out_normal;
|
||||
fastf_t thickness;
|
||||
size_t ray_id;
|
||||
};
|
||||
|
||||
/**
|
||||
* Structured samples and estimates produced by rt_crofton_collect().
|
||||
* Release @p segments with rt_crofton_result_free().
|
||||
*/
|
||||
struct rt_crofton_result {
|
||||
struct rt_crofton_segment *segments;
|
||||
size_t segment_count;
|
||||
size_t ray_count;
|
||||
size_t crossing_count;
|
||||
double surface_area;
|
||||
double volume;
|
||||
};
|
||||
|
||||
/** Initialize an rt_crofton_result before first use. */
|
||||
#define RT_CROFTON_RESULT_INIT { NULL, 0, 0, 0, 0.0, 0.0 }
|
||||
|
||||
|
||||
/**
|
||||
* Run the Cauchy-Crofton ray-sampling estimator on an already-prepared
|
||||
@@ -337,6 +363,26 @@ struct rt_crofton_params {
|
||||
*/
|
||||
RT_EXPORT extern int rt_crofton_shoot(double *out_surf_area, double *out_volume, point_t *out_aabb_min, point_t *out_aabb_max, point_t out_obb[8], point_t **out_points, size_t *out_point_count, struct rt_i *rtip, const struct rt_crofton_params *params, const fastf_t *bbox_min, const fastf_t *bbox_max);
|
||||
|
||||
/**
|
||||
* Run Crofton sampling while preserving oriented entry/exit pairs.
|
||||
*
|
||||
* @p result must be initialized with RT_CROFTON_RESULT_INIT. On success it
|
||||
* owns the returned segment array. @p rtip must already be prepared.
|
||||
* @p ray_offset skips that many samples in the deterministic sequence and
|
||||
* labels the returned rays starting at that value. This lets successive
|
||||
* calls use disjoint validation streams.
|
||||
*/
|
||||
RT_EXPORT extern int rt_crofton_collect(struct rt_crofton_result *result,
|
||||
struct rt_i *rtip,
|
||||
const struct rt_crofton_params *params,
|
||||
size_t ray_offset,
|
||||
const fastf_t *bbox_min,
|
||||
const fastf_t *bbox_max);
|
||||
|
||||
/** Release storage owned by a structured Crofton result. */
|
||||
RT_EXPORT extern void rt_crofton_result_free(
|
||||
struct rt_crofton_result *result);
|
||||
|
||||
|
||||
/**
|
||||
* Cauchy-Crofton surface-area and/or volume estimator for a primitive.
|
||||
|
||||
@@ -55,14 +55,11 @@ struct clt_tri_specific {
|
||||
};
|
||||
#endif
|
||||
|
||||
#define RT_BOT_SURFNO_INTERIOR (-1)
|
||||
|
||||
/* Shared between bot and ars at the moment */
|
||||
struct bot_specific {
|
||||
unsigned char bot_mode;
|
||||
unsigned char bot_orientation;
|
||||
unsigned char bot_flags;
|
||||
unsigned char bot_parity_shot;
|
||||
size_t bot_ntri;
|
||||
fastf_t *bot_thickness;
|
||||
struct bu_bitv *bot_facemode;
|
||||
|
||||
@@ -41,6 +41,7 @@ struct rt_nmg_edit {
|
||||
struct loopuse *lu_copy; /* copy of loop to be extruded */
|
||||
plane_t lu_pl; /* plane equation for loop to be extruded */
|
||||
struct shell *es_s; /* Shell where extrusion is to end up */
|
||||
int es_s_from_extrusion; /* Replace this edit's prior preview */
|
||||
point_t lu_keypoint; /* keypoint of lu_copy for extrusion */
|
||||
struct vertex *es_v; /* Currently selected NMG vertex (ECMD_NMG_VPICK) */
|
||||
struct faceuse *es_fu; /* Currently selected NMG faceuse (ECMD_NMG_FPICK) */
|
||||
|
||||
@@ -55,6 +55,18 @@
|
||||
|
||||
__BEGIN_DECLS
|
||||
|
||||
#define RT_RTCMP_CAPTURE_RUNTIME_TOGGLE 1
|
||||
|
||||
/** Set RT_DEBUG_RTCMP recording: nonzero enables, zero disables.
|
||||
* Enabling requires the debug flag and a usable output writer.
|
||||
* Return 0 on success and -1 on failure. Disabling succeeds even
|
||||
* before setup.
|
||||
*/
|
||||
RT_EXPORT extern int rt_rtcmp_capture_set_enabled(int enabled);
|
||||
|
||||
/** Wait until pending RT_DEBUG_RTCMP records have reached the output file. */
|
||||
RT_EXPORT extern int rt_rtcmp_capture_flush(void);
|
||||
|
||||
/**
|
||||
* @brief
|
||||
* Shoot a ray
|
||||
|
||||
@@ -1108,9 +1108,9 @@ function(brlcad_install_strclear_replace stamp_dir strclear source install_file
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
function(brlcad_install_binary_postprocess stamp_dir strclear source install_file install_type mode rpath_tool install_rpath build_lib_path rel_rpath use_selective_rpath verbose)
|
||||
function(brlcad_install_binary_postprocess stamp_dir strclear source install_file install_type mode rpath_tool install_rpath build_lib_path rel_rpath verbose)
|
||||
set(_brlcad_physical_file "$ENV{DESTDIR}${install_file}")
|
||||
set(_brlcad_signature "binary-postprocess|${install_type}|${mode}|${rpath_tool}|${install_rpath}|${build_lib_path}|${rel_rpath}|${use_selective_rpath}|${strclear}")
|
||||
set(_brlcad_signature "binary-postprocess|${install_type}|${mode}|${rpath_tool}|${install_rpath}|${build_lib_path}|${rel_rpath}|${strclear}")
|
||||
_brlcad_postprocess_needed(_brlcad_needed "${stamp_dir}" "${source}" "${_brlcad_physical_file}" "${_brlcad_signature}")
|
||||
if(NOT _brlcad_needed)
|
||||
return()
|
||||
@@ -1121,32 +1121,41 @@ function(brlcad_install_binary_postprocess stamp_dir strclear source install_fil
|
||||
message(FATAL_ERROR "Post-install copy did not create ${_brlcad_physical_file}")
|
||||
endif()
|
||||
set(_brlcad_result 0)
|
||||
set(_brlcad_rpath_output)
|
||||
set(_brlcad_rpath_error)
|
||||
if("${mode}" STREQUAL "RPATH_TOOL")
|
||||
set(_brlcad_selective_rpath_args)
|
||||
if(use_selective_rpath)
|
||||
set(_brlcad_selective_rpath_args --set-rpath-if-needed --set-rpath-if-needed-prepend --stale-rpath-prefix "${build_lib_path}")
|
||||
endif()
|
||||
# Install the exact project-defined search order. A selective update can
|
||||
# leave an existing relative-only RPATH without the canonical install path.
|
||||
execute_process(
|
||||
COMMAND "${rpath_tool}" --set-rpath "${install_rpath}" ${_brlcad_selective_rpath_args} "${_brlcad_physical_file}"
|
||||
COMMAND "${rpath_tool}" --set-rpath "${install_rpath}" "${_brlcad_physical_file}"
|
||||
RESULT_VARIABLE _brlcad_result
|
||||
OUTPUT_VARIABLE _brlcad_rpath_output
|
||||
ERROR_VARIABLE _brlcad_rpath_error
|
||||
)
|
||||
elseif("${mode}" STREQUAL "APPLE")
|
||||
execute_process(
|
||||
COMMAND install_name_tool -delete_rpath "${build_lib_path}" "${_brlcad_physical_file}"
|
||||
RESULT_VARIABLE _brlcad_result
|
||||
OUTPUT_VARIABLE _brlcad_output
|
||||
ERROR_VARIABLE _brlcad_error
|
||||
OUTPUT_VARIABLE _brlcad_rpath_output
|
||||
ERROR_VARIABLE _brlcad_rpath_error
|
||||
)
|
||||
if(_brlcad_result EQUAL 0)
|
||||
execute_process(
|
||||
COMMAND install_name_tool -add_rpath "${rel_rpath}" "${_brlcad_physical_file}"
|
||||
RESULT_VARIABLE _brlcad_result
|
||||
OUTPUT_VARIABLE _brlcad_rpath_output
|
||||
ERROR_VARIABLE _brlcad_rpath_error
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(NOT _brlcad_result EQUAL 0)
|
||||
message(WARNING "Post-install RPATH update failed for ${_brlcad_physical_file}")
|
||||
message(
|
||||
WARNING
|
||||
"Post-install RPATH update failed for ${_brlcad_physical_file}\n"
|
||||
"stdout: ${_brlcad_rpath_output}\n"
|
||||
"stderr: ${_brlcad_rpath_error}"
|
||||
)
|
||||
return()
|
||||
endif()
|
||||
|
||||
@@ -1840,13 +1849,9 @@ endfunction()
|
||||
elseif(APPLE)
|
||||
set(_brlcad_install_postprocess_mode "APPLE")
|
||||
endif(P_RPATH_EXECUTABLE)
|
||||
set(_brlcad_install_use_selective_rpath FALSE)
|
||||
if(P_RPATH_SUPPORTS_SET_IF_NEEDED_PREPEND)
|
||||
set(_brlcad_install_use_selective_rpath TRUE)
|
||||
endif(P_RPATH_SUPPORTS_SET_IF_NEEDED_PREPEND)
|
||||
install(
|
||||
CODE
|
||||
"include(\"${BRLCAD_EXT_INSTALL_POSTPROCESS_SCRIPT}\")\nbrlcad_install_binary_postprocess(\"${BRLCAD_EXT_INSTALL_POSTPROCESS_STAMP_DIR}\" \"${STRCLEAR_EXECUTABLE}\" \"${CMAKE_BINARY_DIR}/${tf}\" \"\${CMAKE_INSTALL_PREFIX}/${tf}\" \"PROGRAM\" \"${_brlcad_install_postprocess_mode}\" \"${_brlcad_install_postprocess_tool}\" \"${_brlcad_install_postprocess_rpath}\" \"${CMAKE_BINARY_DIR}/${LIB_DIR}\" \"${REL_RPATH}\" \"${_brlcad_install_use_selective_rpath}\" \"${BRLCAD_VERBOSE}\")"
|
||||
"include(\"${BRLCAD_EXT_INSTALL_POSTPROCESS_SCRIPT}\")\nbrlcad_install_binary_postprocess(\"${BRLCAD_EXT_INSTALL_POSTPROCESS_STAMP_DIR}\" \"${STRCLEAR_EXECUTABLE}\" \"${CMAKE_BINARY_DIR}/${tf}\" \"\${CMAKE_INSTALL_PREFIX}/${tf}\" \"PROGRAM\" \"${_brlcad_install_postprocess_mode}\" \"${_brlcad_install_postprocess_tool}\" \"${_brlcad_install_postprocess_rpath}\" \"${CMAKE_BINARY_DIR}/${LIB_DIR}\" \"${REL_RPATH}\" \"${BRLCAD_VERBOSE}\")"
|
||||
)
|
||||
continue()
|
||||
endif("${tf}" IN_LIST ALL_BINARY_FILES)
|
||||
@@ -2200,7 +2205,11 @@ macro(find_package_opencv)
|
||||
set(OpenCV_DIR_TMP "${OpenCV_DIR}")
|
||||
set(OpenCV_DIR "${CMAKE_BINARY_DIR}/${LIB_DIR}/cmake/opencv4")
|
||||
set(OpenCV_ROOT ${CMAKE_BINARY_DIR})
|
||||
find_package(OpenCV COMPONENTS core features2d imgproc highgui)
|
||||
find_package(OpenCV CONFIG COMPONENTS core imgproc imgcodecs highgui)
|
||||
if(OpenCV_FOUND AND OpenCV_VERSION_MAJOR GREATER_EQUAL 5)
|
||||
# OpenCV 5 moved boundingRect from imgproc to geometry.
|
||||
find_package(OpenCV CONFIG COMPONENTS geometry)
|
||||
endif()
|
||||
unset(OpenCV_ROOT)
|
||||
|
||||
# If no bundled copy, see what the system has
|
||||
|
||||
@@ -471,11 +471,13 @@ if(BRLCAD_VERBOSE)
|
||||
set(CMAKE_VERBOSE_MAKEFILE ON)
|
||||
endif(BRLCAD_VERBOSE)
|
||||
|
||||
# Build with profile-guided optimization support. this requires a two-pass
|
||||
# compile, once with BRLCAD_PGO=ON on a location that did not exist beforehand
|
||||
# (specified via the PGO_PATH environment variable), and again to use profiling
|
||||
# metrics captured on "typical" operations and data. By default, path is
|
||||
# BUILDDIR/profiling
|
||||
# Build with profile-guided optimization support. This requires a two-pass
|
||||
# compile, once with BRLCAD_PGO=ON and a profile location that did not exist
|
||||
# beforehand (specified via the PGO_PATH environment variable), and again in a
|
||||
# fresh build directory to use metrics captured on "typical" operations and
|
||||
# data. By default, the path is BUILDDIR/profiling. For MSVC, set
|
||||
# VCPROFILE_PATH to PGO_PATH while running the instrumented programs so their
|
||||
# per-binary .pgc files are available beside the corresponding .pgd files.
|
||||
|
||||
option(BRLCAD_PGO "Enable profile-guided optimization (set PGO_PATH environment variable)")
|
||||
mark_as_advanced(BRLCAD_PGO)
|
||||
|
||||
@@ -108,6 +108,7 @@ set(
|
||||
probe_validation/README.md
|
||||
probe_validation/CMakeLists.txt
|
||||
probe_validation/probe_validation.cpp
|
||||
scripts/pgo_benchmark.cmake
|
||||
scripts/printtime.cmake
|
||||
scripts/tcl_mkindex.cmake
|
||||
scripts/timestamp.cmake
|
||||
|
||||
@@ -0,0 +1,435 @@
|
||||
# P G O _ B E N C H M A R K . C M A K E
|
||||
# BRL-CAD
|
||||
#
|
||||
# Copyright (c) 2026 United States Government as represented by
|
||||
# the U.S. Army Research Laboratory.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions
|
||||
# are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
#
|
||||
# 2. Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. The name of the author may not be used to endorse or promote
|
||||
# products derived from this software without specific prior written
|
||||
# permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
||||
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||
# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
|
||||
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
cmake_minimum_required(VERSION 3.22)
|
||||
|
||||
# TRAIN runs the standard benchmark and additional geometry, then verifies
|
||||
# that compiler profile counters were produced. COMPARE runs the standard
|
||||
# benchmark from baseline and profile-use build trees and requires its
|
||||
# equal-weight mean speedup to exceed PGO_MIN_SPEEDUP. The training model
|
||||
# list accepts build-relative or absolute paths, so external model sets can be
|
||||
# supplied without changing this script.
|
||||
#
|
||||
# Required inputs:
|
||||
# TRAIN: PGO_BUILD_DIR, PGO_RESULTS_DIR, PGO_PROFILE_DIR
|
||||
# COMPARE: PGO_BASELINE_BUILD_DIR, PGO_OPTIMIZED_BUILD_DIR,
|
||||
# PGO_RESULTS_DIR
|
||||
|
||||
set(_pgo_valid_modes TRAIN COMPARE)
|
||||
if(NOT DEFINED PGO_MODE)
|
||||
message(FATAL_ERROR "PGO_MODE must be TRAIN or COMPARE")
|
||||
endif()
|
||||
|
||||
string(TOUPPER "${PGO_MODE}" PGO_MODE)
|
||||
if(NOT PGO_MODE IN_LIST _pgo_valid_modes)
|
||||
message(FATAL_ERROR "PGO_MODE must be TRAIN or COMPARE, not '${PGO_MODE}'")
|
||||
endif()
|
||||
|
||||
set(_pgo_default_models moss world star bldg391 m35 sphflake)
|
||||
set(
|
||||
_pgo_default_training_models
|
||||
share/db/havoc.g
|
||||
share/db/faa/Generic_Twin.g
|
||||
share/db/nist/NIST_MBE_PMI_2.g
|
||||
)
|
||||
|
||||
if(NOT DEFINED PGO_MODELS)
|
||||
set(PGO_MODELS ${_pgo_default_models})
|
||||
endif()
|
||||
if(NOT DEFINED PGO_TRAINING_MODELS)
|
||||
set(PGO_TRAINING_MODELS ${_pgo_default_training_models})
|
||||
endif()
|
||||
if(NOT DEFINED PGO_TIMEFRAME)
|
||||
set(PGO_TIMEFRAME 8)
|
||||
endif()
|
||||
if(NOT DEFINED PGO_MAXTIME)
|
||||
set(PGO_MAXTIME 60)
|
||||
endif()
|
||||
if(NOT DEFINED PGO_AVERAGE)
|
||||
set(PGO_AVERAGE 3)
|
||||
endif()
|
||||
if(NOT DEFINED PGO_DEVIATION)
|
||||
set(PGO_DEVIATION 3)
|
||||
endif()
|
||||
if(NOT DEFINED PGO_TRAINING_SIZE)
|
||||
set(PGO_TRAINING_SIZE 512)
|
||||
endif()
|
||||
if(NOT DEFINED PGO_TRAINING_AZIMUTH)
|
||||
set(PGO_TRAINING_AZIMUTH 35)
|
||||
endif()
|
||||
if(NOT DEFINED PGO_TRAINING_ELEVATION)
|
||||
set(PGO_TRAINING_ELEVATION 25)
|
||||
endif()
|
||||
if(NOT DEFINED PGO_MIN_SPEEDUP)
|
||||
set(PGO_MIN_SPEEDUP 1.0)
|
||||
endif()
|
||||
|
||||
function(_pgo_require variable_name)
|
||||
if(NOT DEFINED "${variable_name}" OR "${${variable_name}}" STREQUAL "")
|
||||
message(FATAL_ERROR "${variable_name} is required for PGO_MODE=${PGO_MODE}")
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
function(_pgo_absolute_path input_path output_variable)
|
||||
get_filename_component(_pgo_path "${input_path}" ABSOLUTE)
|
||||
file(TO_CMAKE_PATH "${_pgo_path}" _pgo_path)
|
||||
set("${output_variable}" "${_pgo_path}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
function(_pgo_cache_value build_directory variable_name output_variable)
|
||||
set(_pgo_cache_file "${build_directory}/CMakeCache.txt")
|
||||
if(NOT EXISTS "${_pgo_cache_file}")
|
||||
message(FATAL_ERROR "CMake cache does not exist: ${_pgo_cache_file}")
|
||||
endif()
|
||||
file(
|
||||
STRINGS
|
||||
"${_pgo_cache_file}"
|
||||
_pgo_cache_entry
|
||||
LIMIT_COUNT 1
|
||||
REGEX "^${variable_name}(:[^=]*)?="
|
||||
)
|
||||
if(_pgo_cache_entry)
|
||||
string(REGEX REPLACE "^[^=]*=" "" _pgo_value "${_pgo_cache_entry}")
|
||||
else()
|
||||
set(_pgo_value "")
|
||||
endif()
|
||||
set("${output_variable}" "${_pgo_value}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
function(_pgo_validate_build build_directory expected_pgo expected_msvc_phase)
|
||||
_pgo_cache_value("${build_directory}" BRLCAD_PGO _pgo_enabled)
|
||||
if(expected_pgo AND NOT _pgo_enabled)
|
||||
message(FATAL_ERROR "BRLCAD_PGO is not enabled in ${build_directory}")
|
||||
elseif(NOT expected_pgo AND _pgo_enabled)
|
||||
message(FATAL_ERROR "The baseline build unexpectedly enables BRLCAD_PGO: ${build_directory}")
|
||||
endif()
|
||||
|
||||
_pgo_cache_value("${build_directory}" BRLCAD_MSVC_PGO_PHASE _pgo_msvc_phase)
|
||||
if(_pgo_msvc_phase AND NOT _pgo_msvc_phase STREQUAL expected_msvc_phase)
|
||||
message(
|
||||
FATAL_ERROR
|
||||
"Expected MSVC PGO phase ${expected_msvc_phase} in ${build_directory}, found ${_pgo_msvc_phase}"
|
||||
)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
function(_pgo_find_build_file build_directory file_name output_variable)
|
||||
set(_pgo_candidates "${build_directory}/bin/${file_name}")
|
||||
if(DEFINED PGO_CONFIG AND NOT "${PGO_CONFIG}" STREQUAL "")
|
||||
list(
|
||||
APPEND
|
||||
_pgo_candidates
|
||||
"${build_directory}/bin/${PGO_CONFIG}/${file_name}"
|
||||
"${build_directory}/${PGO_CONFIG}/bin/${file_name}"
|
||||
)
|
||||
endif()
|
||||
if(CMAKE_HOST_WIN32)
|
||||
set(_pgo_executable_candidates "")
|
||||
foreach(_pgo_candidate IN LISTS _pgo_candidates)
|
||||
list(APPEND _pgo_executable_candidates "${_pgo_candidate}.exe")
|
||||
endforeach()
|
||||
list(APPEND _pgo_candidates ${_pgo_executable_candidates})
|
||||
endif()
|
||||
|
||||
foreach(_pgo_candidate IN LISTS _pgo_candidates)
|
||||
if(EXISTS "${_pgo_candidate}")
|
||||
set("${output_variable}" "${_pgo_candidate}" PARENT_SCOPE)
|
||||
return()
|
||||
endif()
|
||||
endforeach()
|
||||
message(FATAL_ERROR "Could not find ${file_name} under ${build_directory}/bin")
|
||||
endfunction()
|
||||
|
||||
function(_pgo_find_shell build_directory output_variable)
|
||||
if(DEFINED PGO_SHELL AND NOT "${PGO_SHELL}" STREQUAL "")
|
||||
set(_pgo_shell "${PGO_SHELL}")
|
||||
elseif(EXISTS "${build_directory}/CMakeCache.txt")
|
||||
file(
|
||||
STRINGS
|
||||
"${build_directory}/CMakeCache.txt"
|
||||
_pgo_shell_cache_entry
|
||||
LIMIT_COUNT 1
|
||||
REGEX "^SH_EXEC(:[^=]*)?="
|
||||
)
|
||||
if(_pgo_shell_cache_entry)
|
||||
string(REGEX REPLACE "^[^=]*=" "" _pgo_shell "${_pgo_shell_cache_entry}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(NOT DEFINED _pgo_shell OR "${_pgo_shell}" STREQUAL "" OR NOT EXISTS "${_pgo_shell}")
|
||||
unset(_pgo_shell)
|
||||
unset(_pgo_shell CACHE)
|
||||
find_program(_pgo_shell NAMES sh bash)
|
||||
endif()
|
||||
if(NOT _pgo_shell)
|
||||
message(FATAL_ERROR "The BRL-CAD benchmark requires a POSIX shell; set PGO_SHELL to its path")
|
||||
endif()
|
||||
set("${output_variable}" "${_pgo_shell}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
function(_pgo_run_benchmark build_directory result_directory)
|
||||
_pgo_find_build_file("${build_directory}" benchmark _pgo_benchmark)
|
||||
_pgo_find_build_file("${build_directory}" rt _pgo_rt)
|
||||
_pgo_find_build_file("${build_directory}" pixcmp _pgo_pixcmp)
|
||||
_pgo_find_shell("${build_directory}" _pgo_shell)
|
||||
file(MAKE_DIRECTORY "${result_directory}")
|
||||
|
||||
message(STATUS "Running the BRL-CAD benchmark from ${build_directory}")
|
||||
execute_process(
|
||||
COMMAND
|
||||
"${_pgo_shell}" "${_pgo_benchmark}" run
|
||||
"TIMEFRAME=${PGO_TIMEFRAME}"
|
||||
"MAXTIME=${PGO_MAXTIME}"
|
||||
"AVERAGE=${PGO_AVERAGE}"
|
||||
"DEVIATION=${PGO_DEVIATION}"
|
||||
"RT=${_pgo_rt}"
|
||||
"CMP=${_pgo_pixcmp}"
|
||||
-P1
|
||||
WORKING_DIRECTORY "${result_directory}"
|
||||
RESULT_VARIABLE _pgo_benchmark_result
|
||||
)
|
||||
if(NOT "${_pgo_benchmark_result}" STREQUAL "0")
|
||||
message(FATAL_ERROR "The benchmark from ${build_directory} failed with status ${_pgo_benchmark_result}")
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
function(_pgo_extract_rate log_file output_text output_integer)
|
||||
if(NOT EXISTS "${log_file}")
|
||||
message(FATAL_ERROR "Benchmark log does not exist: ${log_file}")
|
||||
endif()
|
||||
file(READ "${log_file}" _pgo_log)
|
||||
string(REGEX MATCHALL "[0-9]+(\\.[0-9]+)? rays/sec \\(RTFM\\)" _pgo_rate_lines "${_pgo_log}")
|
||||
if(NOT _pgo_rate_lines)
|
||||
message(FATAL_ERROR "No RTFM result was found in ${log_file}")
|
||||
endif()
|
||||
list(GET _pgo_rate_lines -1 _pgo_rate_line)
|
||||
string(REGEX REPLACE " rays/sec.*$" "" _pgo_rate "${_pgo_rate_line}")
|
||||
|
||||
if(NOT _pgo_rate MATCHES "^([0-9]+)(\\.([0-9]+))?$")
|
||||
message(FATAL_ERROR "Invalid RTFM result '${_pgo_rate}' in ${log_file}")
|
||||
endif()
|
||||
set(_pgo_whole "${CMAKE_MATCH_1}")
|
||||
set(_pgo_fraction "${CMAKE_MATCH_3}00")
|
||||
string(SUBSTRING "${_pgo_fraction}" 0 2 _pgo_fraction)
|
||||
string(REGEX REPLACE "^0+" "" _pgo_whole "${_pgo_whole}")
|
||||
string(REGEX REPLACE "^0+" "" _pgo_fraction "${_pgo_fraction}")
|
||||
if("${_pgo_whole}" STREQUAL "")
|
||||
set(_pgo_whole 0)
|
||||
endif()
|
||||
if("${_pgo_fraction}" STREQUAL "")
|
||||
set(_pgo_fraction 0)
|
||||
endif()
|
||||
math(EXPR _pgo_rate_integer "${_pgo_whole} * 100 + ${_pgo_fraction}")
|
||||
if(_pgo_rate_integer LESS_EQUAL 0)
|
||||
message(FATAL_ERROR "Non-positive RTFM result '${_pgo_rate}' in ${log_file}")
|
||||
endif()
|
||||
|
||||
set("${output_text}" "${_pgo_rate}" PARENT_SCOPE)
|
||||
set("${output_integer}" "${_pgo_rate_integer}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
function(_pgo_decimal_to_thousandths value output_variable)
|
||||
if(NOT "${value}" MATCHES "^([0-9]+)(\\.([0-9]+))?$")
|
||||
message(FATAL_ERROR "Expected a non-negative decimal number, not '${value}'")
|
||||
endif()
|
||||
set(_pgo_whole "${CMAKE_MATCH_1}")
|
||||
set(_pgo_fraction "${CMAKE_MATCH_3}000")
|
||||
string(SUBSTRING "${_pgo_fraction}" 0 3 _pgo_fraction)
|
||||
string(REGEX REPLACE "^0+" "" _pgo_whole "${_pgo_whole}")
|
||||
string(REGEX REPLACE "^0+" "" _pgo_fraction "${_pgo_fraction}")
|
||||
if("${_pgo_whole}" STREQUAL "")
|
||||
set(_pgo_whole 0)
|
||||
endif()
|
||||
if("${_pgo_fraction}" STREQUAL "")
|
||||
set(_pgo_fraction 0)
|
||||
endif()
|
||||
math(EXPR _pgo_value "${_pgo_whole} * 1000 + ${_pgo_fraction}")
|
||||
set("${output_variable}" "${_pgo_value}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
function(_pgo_format_thousandths value output_variable)
|
||||
math(EXPR _pgo_whole "${value} / 1000")
|
||||
math(EXPR _pgo_fraction "${value} % 1000")
|
||||
set(_pgo_fraction "00${_pgo_fraction}")
|
||||
string(LENGTH "${_pgo_fraction}" _pgo_fraction_length)
|
||||
math(EXPR _pgo_fraction_offset "${_pgo_fraction_length} - 3")
|
||||
string(SUBSTRING "${_pgo_fraction}" ${_pgo_fraction_offset} 3 _pgo_fraction)
|
||||
set("${output_variable}" "${_pgo_whole}.${_pgo_fraction}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
if(PGO_MODE STREQUAL "TRAIN")
|
||||
_pgo_require(PGO_BUILD_DIR)
|
||||
_pgo_require(PGO_RESULTS_DIR)
|
||||
_pgo_require(PGO_PROFILE_DIR)
|
||||
_pgo_absolute_path("${PGO_BUILD_DIR}" PGO_BUILD_DIR)
|
||||
_pgo_absolute_path("${PGO_RESULTS_DIR}" PGO_RESULTS_DIR)
|
||||
_pgo_absolute_path("${PGO_PROFILE_DIR}" PGO_PROFILE_DIR)
|
||||
if(NOT IS_DIRECTORY "${PGO_BUILD_DIR}")
|
||||
message(FATAL_ERROR "PGO_BUILD_DIR does not exist: ${PGO_BUILD_DIR}")
|
||||
endif()
|
||||
_pgo_validate_build("${PGO_BUILD_DIR}" TRUE GENERATE)
|
||||
|
||||
# MSVC's profile runtime uses this location for each image's PGC files.
|
||||
# GCC ignores it and writes its GCDA files to the compiled-in PGO path.
|
||||
file(MAKE_DIRECTORY "${PGO_PROFILE_DIR}")
|
||||
set(ENV{VCPROFILE_PATH} "${PGO_PROFILE_DIR}")
|
||||
_pgo_run_benchmark("${PGO_BUILD_DIR}" "${PGO_RESULTS_DIR}/benchmark")
|
||||
|
||||
_pgo_find_build_file("${PGO_BUILD_DIR}" rt _pgo_rt)
|
||||
if(CMAKE_HOST_WIN32)
|
||||
set(_pgo_null_device NUL)
|
||||
else()
|
||||
set(_pgo_null_device /dev/null)
|
||||
endif()
|
||||
|
||||
set(_pgo_training_index 0)
|
||||
foreach(_pgo_model IN LISTS PGO_TRAINING_MODELS)
|
||||
math(EXPR _pgo_training_index "${_pgo_training_index} + 1")
|
||||
if(IS_ABSOLUTE "${_pgo_model}")
|
||||
set(_pgo_model_path "${_pgo_model}")
|
||||
else()
|
||||
set(_pgo_model_path "${PGO_BUILD_DIR}/${_pgo_model}")
|
||||
endif()
|
||||
if(NOT EXISTS "${_pgo_model_path}")
|
||||
message(FATAL_ERROR "Training model does not exist: ${_pgo_model_path}")
|
||||
endif()
|
||||
|
||||
get_filename_component(_pgo_log_name "${_pgo_model_path}" NAME_WE)
|
||||
string(REGEX REPLACE "[^A-Za-z0-9_.-]" "_" _pgo_log_name "${_pgo_log_name}")
|
||||
set(_pgo_log_file "${PGO_RESULTS_DIR}/${_pgo_training_index}-${_pgo_log_name}.log")
|
||||
message(STATUS "Training with ${_pgo_model_path}")
|
||||
execute_process(
|
||||
COMMAND
|
||||
"${_pgo_rt}" -B -P1 "-s${PGO_TRAINING_SIZE}"
|
||||
"-a${PGO_TRAINING_AZIMUTH}" "-e${PGO_TRAINING_ELEVATION}"
|
||||
-o "${_pgo_null_device}" "${_pgo_model_path}"
|
||||
RESULT_VARIABLE _pgo_rt_result
|
||||
OUTPUT_VARIABLE _pgo_rt_output
|
||||
ERROR_VARIABLE _pgo_rt_error
|
||||
)
|
||||
file(WRITE "${_pgo_log_file}" "${_pgo_rt_output}${_pgo_rt_error}")
|
||||
if(NOT "${_pgo_rt_result}" STREQUAL "0")
|
||||
message(FATAL_ERROR "Training ${_pgo_model} failed with status ${_pgo_rt_result}; see ${_pgo_log_file}")
|
||||
endif()
|
||||
string(REGEX MATCH "[^\r\n]*rays/sec \\(RTFM\\)" _pgo_rtfm_line "${_pgo_rt_output}${_pgo_rt_error}")
|
||||
if(NOT _pgo_rtfm_line)
|
||||
message(FATAL_ERROR "Training ${_pgo_model} did not complete a raytrace; see ${_pgo_log_file}")
|
||||
endif()
|
||||
message(STATUS "${_pgo_model}: ${_pgo_rtfm_line}")
|
||||
endforeach()
|
||||
|
||||
file(GLOB_RECURSE _pgo_gcda_files LIST_DIRECTORIES FALSE "${PGO_PROFILE_DIR}/*.gcda")
|
||||
file(GLOB_RECURSE _pgo_pgc_files LIST_DIRECTORIES FALSE "${PGO_PROFILE_DIR}/*.pgc")
|
||||
list(LENGTH _pgo_gcda_files _pgo_gcda_count)
|
||||
list(LENGTH _pgo_pgc_files _pgo_pgc_count)
|
||||
math(EXPR _pgo_profile_count "${_pgo_gcda_count} + ${_pgo_pgc_count}")
|
||||
if(_pgo_profile_count EQUAL 0)
|
||||
message(FATAL_ERROR "Training did not generate GCC GCDA or MSVC PGC data under ${PGO_PROFILE_DIR}")
|
||||
endif()
|
||||
message(STATUS "Training generated ${_pgo_profile_count} profile counter files under ${PGO_PROFILE_DIR}")
|
||||
else()
|
||||
_pgo_require(PGO_BASELINE_BUILD_DIR)
|
||||
_pgo_require(PGO_OPTIMIZED_BUILD_DIR)
|
||||
_pgo_require(PGO_RESULTS_DIR)
|
||||
_pgo_absolute_path("${PGO_BASELINE_BUILD_DIR}" PGO_BASELINE_BUILD_DIR)
|
||||
_pgo_absolute_path("${PGO_OPTIMIZED_BUILD_DIR}" PGO_OPTIMIZED_BUILD_DIR)
|
||||
_pgo_absolute_path("${PGO_RESULTS_DIR}" PGO_RESULTS_DIR)
|
||||
_pgo_validate_build("${PGO_BASELINE_BUILD_DIR}" FALSE "")
|
||||
_pgo_validate_build("${PGO_OPTIMIZED_BUILD_DIR}" TRUE USE)
|
||||
|
||||
_pgo_run_benchmark("${PGO_BASELINE_BUILD_DIR}" "${PGO_RESULTS_DIR}/baseline")
|
||||
_pgo_run_benchmark("${PGO_OPTIMIZED_BUILD_DIR}" "${PGO_RESULTS_DIR}/optimized")
|
||||
|
||||
set(_pgo_report "### Profile-guided optimization results\n\n")
|
||||
string(APPEND _pgo_report "| Model | Baseline rays/sec | PGO rays/sec | Speedup |\n")
|
||||
string(APPEND _pgo_report "| --- | ---: | ---: | ---: |\n")
|
||||
set(_pgo_speedup_sum 0)
|
||||
set(_pgo_model_count 0)
|
||||
set(_pgo_tsv "model\tbaseline_rays_per_second\tpgo_rays_per_second\tspeedup\n")
|
||||
|
||||
foreach(_pgo_model IN LISTS PGO_MODELS)
|
||||
_pgo_extract_rate("${PGO_RESULTS_DIR}/baseline/${_pgo_model}.log" _pgo_baseline_rate _pgo_baseline_integer)
|
||||
_pgo_extract_rate("${PGO_RESULTS_DIR}/optimized/${_pgo_model}.log" _pgo_optimized_rate _pgo_optimized_integer)
|
||||
math(
|
||||
EXPR
|
||||
_pgo_speedup
|
||||
"(${_pgo_optimized_integer} * 1000 + ${_pgo_baseline_integer} / 2) / ${_pgo_baseline_integer}"
|
||||
)
|
||||
_pgo_format_thousandths("${_pgo_speedup}" _pgo_speedup_text)
|
||||
math(EXPR _pgo_speedup_sum "${_pgo_speedup_sum} + ${_pgo_speedup}")
|
||||
math(EXPR _pgo_model_count "${_pgo_model_count} + 1")
|
||||
string(
|
||||
APPEND
|
||||
_pgo_report
|
||||
"| `${_pgo_model}` | ${_pgo_baseline_rate} | ${_pgo_optimized_rate} | ${_pgo_speedup_text}x |\n"
|
||||
)
|
||||
string(
|
||||
APPEND
|
||||
_pgo_tsv
|
||||
"${_pgo_model}\t${_pgo_baseline_rate}\t${_pgo_optimized_rate}\t${_pgo_speedup_text}\n"
|
||||
)
|
||||
endforeach()
|
||||
|
||||
if(_pgo_model_count EQUAL 0)
|
||||
message(FATAL_ERROR "PGO_MODELS must name at least one benchmark model")
|
||||
endif()
|
||||
math(EXPR _pgo_mean_speedup "(${_pgo_speedup_sum} + ${_pgo_model_count} / 2) / ${_pgo_model_count}")
|
||||
_pgo_format_thousandths("${_pgo_mean_speedup}" _pgo_mean_speedup_text)
|
||||
string(APPEND _pgo_report "\nArithmetic mean speedup: ${_pgo_mean_speedup_text}x\n")
|
||||
file(MAKE_DIRECTORY "${PGO_RESULTS_DIR}")
|
||||
file(WRITE "${PGO_RESULTS_DIR}/results.tsv" "${_pgo_tsv}")
|
||||
|
||||
if(DEFINED PGO_SUMMARY_FILE AND NOT "${PGO_SUMMARY_FILE}" STREQUAL "")
|
||||
_pgo_absolute_path("${PGO_SUMMARY_FILE}" _pgo_summary_file)
|
||||
elseif(NOT "$ENV{GITHUB_STEP_SUMMARY}" STREQUAL "")
|
||||
set(_pgo_summary_file "$ENV{GITHUB_STEP_SUMMARY}")
|
||||
endif()
|
||||
if(DEFINED _pgo_summary_file)
|
||||
file(APPEND "${_pgo_summary_file}" "${_pgo_report}")
|
||||
endif()
|
||||
message("${_pgo_report}")
|
||||
|
||||
_pgo_decimal_to_thousandths("${PGO_MIN_SPEEDUP}" _pgo_min_speedup)
|
||||
if(NOT _pgo_mean_speedup GREATER _pgo_min_speedup)
|
||||
message(
|
||||
FATAL_ERROR
|
||||
"PGO raytracing speedup ${_pgo_mean_speedup_text}x did not exceed ${PGO_MIN_SPEEDUP}x"
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Local Variables:
|
||||
# tab-width: 8
|
||||
# mode: cmake
|
||||
# indent-tabs-mode: t
|
||||
# End:
|
||||
# ex: shiftwidth=2 tabstop=8
|
||||
@@ -7,14 +7,25 @@
|
||||
set -e
|
||||
set -o pipefail
|
||||
|
||||
# Note: we set these variables to tell the ccc-analyzer script which compilers
|
||||
# to use, but it is not always enough - when some of the sub-builds are
|
||||
# triggered, they do not seem to inherit these settings and revert to the
|
||||
# default compiler. The only way so far to avoid this issue reliably seems to
|
||||
# be to edit the ccc-analyzer script to use the compiler we want as the default
|
||||
# compiler.
|
||||
export CCC_CC=clang
|
||||
export CCC_CXX=clang++
|
||||
# CLANG_VERSION should be set by User or CI environments to avoid hard-coding
|
||||
# a particular version into the script
|
||||
if [ -z "$CLANG_VERSION" ]; then
|
||||
echo "Error: CLANG_VERSION environment variable is not set." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Derive tool paths from the version
|
||||
SCAN_BUILD_DIR="/usr/share/clang/scan-build-${CLANG_VERSION}"
|
||||
SCAN_BUILD_BIN="${SCAN_BUILD_DIR}/bin"
|
||||
SCAN_BUILD_LIBEXEC="${SCAN_BUILD_DIR}/libexec"
|
||||
CLANG_ANALYZER_BIN="/usr/bin/clang-${CLANG_VERSION}"
|
||||
|
||||
# Add scan-build to the PATH
|
||||
export PATH="${SCAN_BUILD_BIN}:${SCAN_BUILD_LIBEXEC}:${PATH}"
|
||||
|
||||
# Set compilers for the analyzer scripts
|
||||
export CCC_CC="clang-${CLANG_VERSION}"
|
||||
export CCC_CXX="clang++-${CLANG_VERSION}"
|
||||
|
||||
# This appears to be a workable way to enable the new Z3 static analyzer
|
||||
# support, but at least as of 2017-12 it greatly slows the testing (by orders
|
||||
@@ -45,19 +56,24 @@ fi
|
||||
|
||||
# Encapsulate the logic to do a scan build
|
||||
function runtest {
|
||||
echo "$1"
|
||||
scan-build --use-analyzer=/usr/bin/$CCC_CC -o ./scan-reports-$1 make -j12 $1
|
||||
if [ "$(ls -A ./scan-reports-$1)" ]; then
|
||||
report_cnt=$(ls -l ./scan-reports-$1/*/report* | wc -l)
|
||||
failure=$(($failure + $report_cnt))
|
||||
else
|
||||
rm -rf ./scan-reports-$1
|
||||
fi
|
||||
echo "$1"
|
||||
scan-build --use-analyzer="${CLANG_ANALYZER_BIN}" -o "./scan-reports-$1" make -j12 "$1"
|
||||
if [ -d "./scan-reports-$1" ] && [ -n "$(ls -A "./scan-reports-$1")" ]; then
|
||||
report_cnt=$(find "./scan-reports-$1" -name 'report-*.html' | wc -l)
|
||||
failure=$((failure + report_cnt))
|
||||
else
|
||||
rm -rf "./scan-reports-$1"
|
||||
fi
|
||||
}
|
||||
|
||||
# configure using the correct compiler and values. We don't
|
||||
# need this report, so clear it after configure is done
|
||||
scan-build --use-analyzer=/usr/bin/$CCC_CC -o ./scan-reports-config cmake .. -DBRLCAD_EXTRADOCS=OFF -DBRLCAD_ENABLE_QT=ON -DBRLCAD_LTO_MODE=OFF -DBRLCAD_EXT_DIR="$bext_dir" -DCMAKE_C_COMPILER=ccc-analyzer -DCMAKE_CXX_COMPILER=c++-analyzer
|
||||
# Configure using the correct compiler and values.
|
||||
scan-build --use-analyzer="${CLANG_ANALYZER_BIN}" -o ./scan-reports-config cmake .. \
|
||||
-DBRLCAD_EXTRADOCS=OFF \
|
||||
-DBRLCAD_ENABLE_QT=ON \
|
||||
-DBRLCAD_LTO_MODE=OFF \
|
||||
-DBRLCAD_EXT_DIR="$bext_dir" \
|
||||
-DCMAKE_C_COMPILER=ccc-analyzer \
|
||||
-DCMAKE_CXX_COMPILER=c++-analyzer
|
||||
|
||||
# clear out any old reports
|
||||
rm -rfv ./scan-reports-*
|
||||
|
||||
@@ -45,14 +45,6 @@ cmakefiles(
|
||||
asciiquack/vendor/cxxopts.hpp
|
||||
db2adoc/db2adoc.py
|
||||
db2adoc/db2adoc.xsl
|
||||
brep_corpus_audit.py
|
||||
step_corpus_capture.py
|
||||
step_corpus_run.py
|
||||
step_corpus_summary.py
|
||||
step_corpus_verify.py
|
||||
step_external_corpus_expectations.json
|
||||
step_local_external_corpus_expectations.json
|
||||
step_dependency_subset.py
|
||||
inactvhdrs/CMakeLists.txt
|
||||
inactvhdrs/inactvhdrs.cpp
|
||||
)
|
||||
|
||||
@@ -1,619 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Audit BRep wireframe and fast shaded realizations in a directory of .g files.
|
||||
|
||||
The companion ``brep-audit`` executable does one BRep per process. This
|
||||
driver inventories each database, runs those isolated checks, checkpoints
|
||||
every result to JSON Lines, and writes JSON and Markdown summaries. Geometry
|
||||
findings do not make the driver fail: they are the intended work list.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from collections import Counter
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
FORMAT = "brlcad-brep-corpus-audit-v1"
|
||||
HELPER_FORMAT = "brlcad-brep-realization-audit-v1"
|
||||
|
||||
|
||||
def file_identity(path):
|
||||
try:
|
||||
info = path.stat()
|
||||
return {"size": info.st_size, "mtime_ns": info.st_mtime_ns}
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
|
||||
def text_tail(value, limit=8000):
|
||||
return (value or "")[-limit:]
|
||||
|
||||
|
||||
def last_phase(stderr):
|
||||
phases = re.findall(r"^brep-audit: phase=([a-z]+)$", stderr or "", re.MULTILINE)
|
||||
return phases[-1] if phases else None
|
||||
|
||||
|
||||
def process_rss_bytes(pid):
|
||||
"""Read current Linux RSS, returning zero when it is unavailable."""
|
||||
try:
|
||||
with open("/proc/{}/status".format(pid), "r", encoding="ascii") as stream:
|
||||
for line in stream:
|
||||
if line.startswith("VmRSS:"):
|
||||
return int(line.split()[1]) * 1024
|
||||
except (OSError, ValueError, IndexError):
|
||||
pass
|
||||
return 0
|
||||
|
||||
|
||||
def kill_process(process):
|
||||
try:
|
||||
if os.name == "posix":
|
||||
os.killpg(process.pid, signal.SIGKILL)
|
||||
else:
|
||||
process.kill()
|
||||
except (OSError, ProcessLookupError):
|
||||
pass
|
||||
|
||||
|
||||
def file_tail(stream, limit):
|
||||
stream.flush()
|
||||
size = os.fstat(stream.fileno()).st_size
|
||||
stream.seek(max(0, size - limit))
|
||||
return stream.read().decode("utf-8", errors="replace"), size > limit
|
||||
|
||||
|
||||
def run_process(command, timeout, max_rss_mib=0):
|
||||
"""Run and monitor one helper, retaining enough detail for diagnostics."""
|
||||
started = time.monotonic()
|
||||
with tempfile.TemporaryFile() as stdout_file, \
|
||||
tempfile.TemporaryFile() as stderr_file:
|
||||
try:
|
||||
process = subprocess.Popen(
|
||||
command,
|
||||
stdout=stdout_file,
|
||||
stderr=stderr_file,
|
||||
start_new_session=(os.name == "posix"),
|
||||
)
|
||||
except OSError as error:
|
||||
return {
|
||||
"returncode": 127,
|
||||
"stdout": "",
|
||||
"stdout_truncated": False,
|
||||
"stderr": str(error),
|
||||
"elapsed_seconds": time.monotonic() - started,
|
||||
"observed_peak_rss_bytes": 0,
|
||||
"termination_reason": "launch_error",
|
||||
}
|
||||
|
||||
peak_rss = 0
|
||||
termination_reason = None
|
||||
while True:
|
||||
elapsed = time.monotonic() - started
|
||||
remaining = timeout - elapsed
|
||||
if remaining <= 0:
|
||||
termination_reason = "timeout"
|
||||
kill_process(process)
|
||||
process.wait()
|
||||
break
|
||||
try:
|
||||
process.wait(timeout=min(0.1, remaining))
|
||||
peak_rss = max(peak_rss, process_rss_bytes(process.pid))
|
||||
break
|
||||
except subprocess.TimeoutExpired:
|
||||
rss = process_rss_bytes(process.pid)
|
||||
peak_rss = max(peak_rss, rss)
|
||||
if max_rss_mib and rss > max_rss_mib * 1024 * 1024:
|
||||
termination_reason = "rss_limit"
|
||||
kill_process(process)
|
||||
process.wait()
|
||||
break
|
||||
|
||||
stdout, stdout_truncated = file_tail(
|
||||
stdout_file, 16 * 1024 * 1024
|
||||
)
|
||||
stderr, _ = file_tail(stderr_file, 64 * 1024)
|
||||
|
||||
return {
|
||||
"returncode": process.returncode,
|
||||
"stdout": stdout,
|
||||
"stdout_truncated": stdout_truncated,
|
||||
"stderr": stderr,
|
||||
"elapsed_seconds": time.monotonic() - started,
|
||||
"observed_peak_rss_bytes": peak_rss,
|
||||
"termination_reason": termination_reason,
|
||||
}
|
||||
|
||||
|
||||
def synthetic_record(database, object_name, status, issue, run, signature):
|
||||
return {
|
||||
"format": HELPER_FORMAT,
|
||||
"scope": "brep" if object_name else "database",
|
||||
"database": str(database),
|
||||
"object": object_name,
|
||||
"status": status,
|
||||
"reference": None,
|
||||
"wireframe": None,
|
||||
"shaded": None,
|
||||
"issues": [issue],
|
||||
"audit_signature": signature,
|
||||
"database_identity": file_identity(database),
|
||||
"process": {
|
||||
"return_code": run["returncode"],
|
||||
"elapsed_seconds": run["elapsed_seconds"],
|
||||
"observed_peak_rss_bytes": run["observed_peak_rss_bytes"],
|
||||
"termination_reason": run["termination_reason"],
|
||||
"last_phase": last_phase(run["stderr"]),
|
||||
"stdout_tail": text_tail(run["stdout"]),
|
||||
"stderr_tail": text_tail(run["stderr"]),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def parse_helper_json(output):
|
||||
for line in reversed(output.splitlines()):
|
||||
if not line.lstrip().startswith("{"):
|
||||
continue
|
||||
try:
|
||||
value = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if isinstance(value, dict):
|
||||
return value
|
||||
return None
|
||||
|
||||
|
||||
def audit_one(helper, database, object_name, args, signature):
|
||||
command = [
|
||||
str(helper),
|
||||
"--ratio-min", str(args.ratio_min),
|
||||
"--ratio-max", str(args.ratio_max),
|
||||
"--tess-abs", str(args.tess_abs),
|
||||
"--tess-rel", str(args.tess_rel),
|
||||
"--tess-norm", str(args.tess_norm),
|
||||
]
|
||||
if args.address_space_limit_mib:
|
||||
command.extend([
|
||||
"--memory-limit-mib", str(args.address_space_limit_mib)
|
||||
])
|
||||
command.extend([str(database), object_name])
|
||||
run = run_process(command, args.timeout, args.max_rss_mib)
|
||||
|
||||
if run["termination_reason"] == "timeout":
|
||||
phase = last_phase(run["stderr"])
|
||||
return synthetic_record(
|
||||
database, object_name, "timeout",
|
||||
"{}_timeout".format(phase) if phase else "process_timeout",
|
||||
run, signature
|
||||
)
|
||||
if run["termination_reason"] == "rss_limit":
|
||||
phase = last_phase(run["stderr"])
|
||||
return synthetic_record(
|
||||
database, object_name, "memory_limit",
|
||||
"{}_rss_limit_exceeded".format(phase)
|
||||
if phase else "process_rss_limit_exceeded",
|
||||
run, signature
|
||||
)
|
||||
if run["termination_reason"] == "launch_error":
|
||||
return synthetic_record(
|
||||
database, object_name, "error", "helper_launch_failed", run, signature
|
||||
)
|
||||
|
||||
record = parse_helper_json(run["stdout"])
|
||||
if record is None:
|
||||
issue = (
|
||||
"process_signal_{}".format(-run["returncode"])
|
||||
if run["returncode"] is not None and run["returncode"] < 0
|
||||
else "helper_output_not_json"
|
||||
)
|
||||
return synthetic_record(
|
||||
database, object_name, "error", issue, run, signature
|
||||
)
|
||||
if record.get("format") != HELPER_FORMAT:
|
||||
return synthetic_record(
|
||||
database, object_name, "error", "unexpected_helper_format",
|
||||
run, signature
|
||||
)
|
||||
|
||||
record["scope"] = "brep"
|
||||
record["audit_signature"] = signature
|
||||
record["database_identity"] = file_identity(database)
|
||||
record["process"] = {
|
||||
"return_code": run["returncode"],
|
||||
"elapsed_seconds": run["elapsed_seconds"],
|
||||
"observed_peak_rss_bytes": run["observed_peak_rss_bytes"],
|
||||
"termination_reason": None,
|
||||
"last_phase": last_phase(run["stderr"]),
|
||||
"stdout_tail": "",
|
||||
"stderr_tail": text_tail(run["stderr"]),
|
||||
}
|
||||
expected_return = 0 if record.get("status") == "ok" else 1
|
||||
if run["returncode"] != expected_return:
|
||||
record["status"] = "error"
|
||||
record.setdefault("issues", []).append("helper_exit_status_mismatch")
|
||||
return record
|
||||
|
||||
|
||||
def list_breps(helper, database, timeout, signature):
|
||||
run = run_process([str(helper), "--list", str(database)], timeout)
|
||||
if run["termination_reason"] == "timeout":
|
||||
return [], synthetic_record(
|
||||
database, "", "timeout", "brep_inventory_timeout", run, signature
|
||||
)
|
||||
if run["returncode"] != 0:
|
||||
return [], synthetic_record(
|
||||
database, "", "error", "brep_inventory_failed", run, signature
|
||||
)
|
||||
if run["stdout_truncated"]:
|
||||
return [], synthetic_record(
|
||||
database, "", "error", "brep_inventory_output_too_large",
|
||||
run, signature
|
||||
)
|
||||
names = sorted(set(line.strip() for line in run["stdout"].splitlines()
|
||||
if line.strip()))
|
||||
return names, None
|
||||
|
||||
|
||||
def git_state(start):
|
||||
try:
|
||||
revision = subprocess.check_output(
|
||||
["git", "-C", str(start), "rev-parse", "HEAD"],
|
||||
stderr=subprocess.DEVNULL, text=True,
|
||||
).strip()
|
||||
dirty = bool(subprocess.check_output(
|
||||
["git", "-C", str(start), "status", "--porcelain"],
|
||||
stderr=subprocess.DEVNULL, text=True,
|
||||
).strip())
|
||||
return revision, dirty
|
||||
except (OSError, subprocess.CalledProcessError):
|
||||
return "unknown", None
|
||||
|
||||
|
||||
def read_checkpoint(path, signature):
|
||||
records = {}
|
||||
if not path.is_file():
|
||||
return records
|
||||
with path.open("r", encoding="utf-8") as stream:
|
||||
for number, line in enumerate(stream, 1):
|
||||
try:
|
||||
record = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
print("ignoring malformed checkpoint line {} in {}".format(
|
||||
number, path), file=sys.stderr)
|
||||
continue
|
||||
if record.get("audit_signature") != signature:
|
||||
continue
|
||||
records[(record.get("database", ""), record.get("object", ""))] = record
|
||||
return records
|
||||
|
||||
|
||||
def all_issues(record):
|
||||
issues = list(record.get("issues") or [])
|
||||
for mode in ("wireframe", "shaded"):
|
||||
result = record.get(mode)
|
||||
if result:
|
||||
issues.extend(result.get("issues") or [])
|
||||
return list(dict.fromkeys(str(issue) for issue in issues))
|
||||
|
||||
|
||||
def format_vector(values):
|
||||
if not values:
|
||||
return "-"
|
||||
return " × ".join("{:.5g}".format(value) for value in values)
|
||||
|
||||
|
||||
def format_geometry(result, primitive):
|
||||
if not result:
|
||||
return "-"
|
||||
ratios = result.get("dimension_ratios")
|
||||
ratio_text = ""
|
||||
if ratios:
|
||||
ratio_text = "; ratios " + "/".join(
|
||||
"-" if value is None else "{:.3g}".format(value)
|
||||
for value in ratios
|
||||
)
|
||||
return "{} points, {} {}{}".format(
|
||||
result.get("vertices", 0), result.get("primitives", 0),
|
||||
primitive, ratio_text,
|
||||
)
|
||||
|
||||
|
||||
def markdown_escape(value):
|
||||
return str(value).replace("|", "\\|").replace("\n", "<br>")
|
||||
|
||||
|
||||
def worklist_markdown(summary, input_directory):
|
||||
totals = summary["totals"]
|
||||
lines = [
|
||||
"# BRep realization audit",
|
||||
"",
|
||||
"Revision: `{}`{}".format(
|
||||
summary["revision"], " (dirty)" if summary.get("dirty") else ""
|
||||
),
|
||||
"",
|
||||
(
|
||||
"{} databases, {} BReps: {} okay, {} geometry findings, "
|
||||
"{} timeouts, {} memory-limit stops, and {} process errors."
|
||||
).format(
|
||||
totals["databases"], totals["breps"], totals["ok"], totals["fail"],
|
||||
totals["timeout"], totals["memory_limit"], totals["error"],
|
||||
),
|
||||
"",
|
||||
(
|
||||
"Accepted generated/reference dimension ratios: {}–{}. "
|
||||
"Shaded tessellation tolerance: abs {}, rel {}, norm {}."
|
||||
).format(
|
||||
summary["options"]["ratio_min"], summary["options"]["ratio_max"],
|
||||
summary["options"]["tess_abs"], summary["options"]["tess_rel"],
|
||||
summary["options"]["tess_norm"],
|
||||
),
|
||||
"",
|
||||
"## Work list",
|
||||
"",
|
||||
]
|
||||
findings = [
|
||||
record for record in summary["results"]
|
||||
if record.get("status") != "ok"
|
||||
]
|
||||
if not findings:
|
||||
lines.append("No findings.")
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
lines.extend([
|
||||
"| Database | BRep | Status | Reference dimensions | Wireframe | Shaded | Peak RSS MiB | Issues |",
|
||||
"|---|---|---|---|---|---|---:|---|",
|
||||
])
|
||||
for record in findings:
|
||||
database = Path(record.get("database", ""))
|
||||
try:
|
||||
database_name = str(database.relative_to(input_directory))
|
||||
except ValueError:
|
||||
database_name = str(database)
|
||||
wire = record.get("wireframe")
|
||||
shaded = record.get("shaded")
|
||||
peaks = [
|
||||
int(result.get("peak_rss_bytes") or 0)
|
||||
for result in (wire, shaded) if result
|
||||
]
|
||||
process = record.get("process") or {}
|
||||
peaks.append(int(process.get("observed_peak_rss_bytes") or 0))
|
||||
reference = record.get("reference") or {}
|
||||
lines.append("| {} | {} | {} | {} | {} | {} | {:.1f} | {} |".format(
|
||||
markdown_escape(database_name),
|
||||
markdown_escape(record.get("object") or "(inventory)"),
|
||||
markdown_escape(record.get("status", "error")),
|
||||
markdown_escape(format_vector(reference.get("dimensions"))),
|
||||
markdown_escape(format_geometry(wire, "segments")),
|
||||
markdown_escape(format_geometry(shaded, "triangles")),
|
||||
max(peaks, default=0) / (1024.0 * 1024.0),
|
||||
markdown_escape(", ".join(all_issues(record)) or "unspecified"),
|
||||
))
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("input_directory", type=Path)
|
||||
parser.add_argument("output_directory", type=Path)
|
||||
parser.add_argument(
|
||||
"--brep-audit", type=Path, default=Path(".build/bin/brep-audit")
|
||||
)
|
||||
parser.add_argument("-j", "--jobs", type=int, default=1,
|
||||
help="BRep processes to run concurrently (default: 1)")
|
||||
parser.add_argument("--timeout", type=float, default=300.0,
|
||||
help="seconds allowed per BRep")
|
||||
parser.add_argument("--list-timeout", type=float, default=120.0,
|
||||
help="seconds allowed to inventory one database")
|
||||
parser.add_argument("--ratio-min", type=float, default=0.5)
|
||||
parser.add_argument("--ratio-max", type=float, default=2.0)
|
||||
parser.add_argument("--tess-abs", type=float, default=0.0)
|
||||
parser.add_argument("--tess-rel", type=float, default=0.01)
|
||||
parser.add_argument("--tess-norm", type=float, default=0.0)
|
||||
parser.add_argument(
|
||||
"--max-rss-mib", type=int, default=0,
|
||||
help="kill a BRep helper above this observed RSS (zero disables)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--address-space-limit-mib", type=int, default=0,
|
||||
help="set the helper RLIMIT_AS in MiB (zero disables)"
|
||||
)
|
||||
parser.add_argument("--only-file",
|
||||
help="regular expression matched against .g filenames")
|
||||
parser.add_argument("--only-object",
|
||||
help="regular expression matched against BRep names")
|
||||
parser.add_argument("--resume", action="store_true",
|
||||
help="retain checkpointed results with identical options")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.jobs < 1:
|
||||
parser.error("--jobs must be positive")
|
||||
if args.timeout <= 0 or args.list_timeout <= 0:
|
||||
parser.error("timeouts must be positive")
|
||||
if args.ratio_min <= 0 or args.ratio_max < args.ratio_min:
|
||||
parser.error("dimension ratio limits are invalid")
|
||||
if min(args.tess_abs, args.tess_rel, args.tess_norm) < 0:
|
||||
parser.error("tessellation tolerances cannot be negative")
|
||||
if args.max_rss_mib < 0 or args.address_space_limit_mib < 0:
|
||||
parser.error("memory limits cannot be negative")
|
||||
|
||||
input_directory = args.input_directory.resolve()
|
||||
output_directory = args.output_directory.resolve()
|
||||
helper = args.brep_audit.resolve()
|
||||
if not helper.is_file():
|
||||
parser.error("brep-audit not found: {}".format(helper))
|
||||
file_matcher = re.compile(args.only_file) if args.only_file else None
|
||||
object_matcher = re.compile(args.only_object) if args.only_object else None
|
||||
databases = sorted(
|
||||
path.resolve() for path in input_directory.iterdir()
|
||||
if path.is_file() and path.suffix.lower() == ".g" and
|
||||
(not file_matcher or file_matcher.search(path.name))
|
||||
)
|
||||
if not databases:
|
||||
parser.error("no matching .g files in {}".format(input_directory))
|
||||
|
||||
options = {
|
||||
"ratio_min": args.ratio_min,
|
||||
"ratio_max": args.ratio_max,
|
||||
"tess_abs": args.tess_abs,
|
||||
"tess_rel": args.tess_rel,
|
||||
"tess_norm": args.tess_norm,
|
||||
"timeout": args.timeout,
|
||||
"list_timeout": args.list_timeout,
|
||||
"max_rss_mib": args.max_rss_mib,
|
||||
"address_space_limit_mib": args.address_space_limit_mib,
|
||||
"only_file": args.only_file,
|
||||
"only_object": args.only_object,
|
||||
"helper": str(helper),
|
||||
"helper_size": helper.stat().st_size,
|
||||
"helper_mtime_ns": helper.stat().st_mtime_ns,
|
||||
}
|
||||
signature = options
|
||||
output_directory.mkdir(parents=True, exist_ok=True)
|
||||
checkpoint_path = output_directory / "brep-audit-results.jsonl"
|
||||
records = (
|
||||
read_checkpoint(checkpoint_path, signature) if args.resume else {}
|
||||
)
|
||||
|
||||
tasks = []
|
||||
for index, database in enumerate(databases, 1):
|
||||
print("[inventory {}/{}] {}".format(
|
||||
index, len(databases), database.name), flush=True)
|
||||
names, error_record = list_breps(
|
||||
helper, database, args.list_timeout, signature
|
||||
)
|
||||
if error_record:
|
||||
for key in list(records):
|
||||
if key[0] == str(database):
|
||||
records.pop(key)
|
||||
records[(str(database), "")] = error_record
|
||||
continue
|
||||
records.pop((str(database), ""), None)
|
||||
if object_matcher:
|
||||
names = [name for name in names if object_matcher.search(name)]
|
||||
current_names = set(names)
|
||||
identity = file_identity(database)
|
||||
for key, record in list(records.items()):
|
||||
if key[0] != str(database):
|
||||
continue
|
||||
if key[1] not in current_names or \
|
||||
record.get("database_identity") != identity:
|
||||
records.pop(key)
|
||||
tasks.extend((database, name) for name in names)
|
||||
|
||||
# Rewrite a resumed checkpoint with only the selected run signature, then
|
||||
# append each new result immediately so an interrupted corpus run is usable.
|
||||
with checkpoint_path.open("w", encoding="utf-8") as checkpoint:
|
||||
for record in sorted(
|
||||
records.values(),
|
||||
key=lambda item: (item.get("database", ""), item.get("object", ""))):
|
||||
checkpoint.write(json.dumps(record, sort_keys=True) + "\n")
|
||||
checkpoint.flush()
|
||||
|
||||
pending = [
|
||||
task for task in tasks if (str(task[0]), task[1]) not in records
|
||||
]
|
||||
resumed = len(tasks) - len(pending)
|
||||
if resumed:
|
||||
print("resuming {} completed BRep records".format(resumed), flush=True)
|
||||
|
||||
completed = 0
|
||||
with checkpoint_path.open("a", encoding="utf-8") as checkpoint:
|
||||
with ThreadPoolExecutor(max_workers=args.jobs) as pool:
|
||||
futures = {
|
||||
pool.submit(
|
||||
audit_one, helper, database, object_name, args, signature
|
||||
): (database, object_name)
|
||||
for database, object_name in pending
|
||||
}
|
||||
for future in as_completed(futures):
|
||||
database, object_name = futures[future]
|
||||
try:
|
||||
record = future.result()
|
||||
except Exception as error: # Preserve the rest of a corpus run.
|
||||
run = {
|
||||
"returncode": 125,
|
||||
"stdout": "",
|
||||
"stderr": repr(error),
|
||||
"elapsed_seconds": 0.0,
|
||||
"observed_peak_rss_bytes": 0,
|
||||
"termination_reason": "driver_error",
|
||||
}
|
||||
record = synthetic_record(
|
||||
database, object_name, "error",
|
||||
"audit_driver_exception", run, signature
|
||||
)
|
||||
records[(str(database), object_name)] = record
|
||||
checkpoint.write(json.dumps(record, sort_keys=True) + "\n")
|
||||
checkpoint.flush()
|
||||
completed += 1
|
||||
print("[audit {}/{}] {}:{} {}".format(
|
||||
completed, len(pending), database.name, object_name,
|
||||
record["status"],
|
||||
), flush=True)
|
||||
|
||||
results = sorted(
|
||||
records.values(),
|
||||
key=lambda item: (item.get("database", ""), item.get("object", "")),
|
||||
)
|
||||
statuses = Counter(
|
||||
record.get("status", "error")
|
||||
for record in results if record.get("scope") == "brep"
|
||||
)
|
||||
database_errors = [
|
||||
record for record in results if record.get("scope") == "database"
|
||||
]
|
||||
revision, dirty = git_state(Path(__file__).resolve().parents[2])
|
||||
summary = {
|
||||
"format": FORMAT,
|
||||
"generated_utc": datetime.datetime.now(
|
||||
datetime.timezone.utc
|
||||
).isoformat(),
|
||||
"revision": revision,
|
||||
"dirty": dirty,
|
||||
"input_directory": str(input_directory),
|
||||
"output_directory": str(output_directory),
|
||||
"helper": str(helper),
|
||||
"options": options,
|
||||
"totals": {
|
||||
"databases": len(databases),
|
||||
"database_inventory_errors": len(database_errors),
|
||||
"breps": sum(statuses.values()),
|
||||
"ok": statuses["ok"],
|
||||
"fail": statuses["fail"],
|
||||
"timeout": statuses["timeout"],
|
||||
"memory_limit": statuses["memory_limit"],
|
||||
"error": statuses["error"] + len(database_errors),
|
||||
},
|
||||
"results": results,
|
||||
}
|
||||
summary_path = output_directory / "brep-audit-summary.json"
|
||||
worklist_path = output_directory / "brep-audit-worklist.md"
|
||||
summary_path.write_text(
|
||||
json.dumps(summary, indent=2, sort_keys=True) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
worklist_path.write_text(
|
||||
worklist_markdown(summary, input_directory), encoding="utf-8"
|
||||
)
|
||||
totals = summary["totals"]
|
||||
print(
|
||||
"audited {} BReps: {} okay; {} work items; reports in {}".format(
|
||||
totals["breps"], totals["ok"],
|
||||
totals["breps"] - totals["ok"] + len(database_errors),
|
||||
output_directory,
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,127 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Capture standard swrast overview images for converted STEP databases.
|
||||
|
||||
Each database is opened in its own gsh process so the display manager and GED
|
||||
state are isolated. The two modes are written to separate directories and
|
||||
use the database filename (without its final ``.g``) as the image name.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def image_stem(path):
|
||||
return path.name[:-2] if path.name.lower().endswith(".g") else path.stem
|
||||
|
||||
|
||||
def capture(gsh, model, output, mode, azimuth, elevation, timeout):
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
image = output / (image_stem(model) + ".png")
|
||||
try:
|
||||
tops = subprocess.run(
|
||||
[str(gsh), "--new-cmds", str(model), "tops"],
|
||||
text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
timeout=timeout,
|
||||
).stdout.splitlines()
|
||||
except subprocess.TimeoutExpired:
|
||||
return {"model": str(model), "mode": mode, "image": str(image),
|
||||
"returncode": 124, "complete": False,
|
||||
"log": "timed out while listing top-level objects"}
|
||||
tops = [line.strip().rstrip("/") for line in tops
|
||||
if line.strip().endswith("/") and line.strip().count("/") == 1]
|
||||
if not tops:
|
||||
return {"model": str(model), "mode": mode, "image": str(image),
|
||||
"returncode": 1, "complete": False,
|
||||
"log": "database has no top-level objects"}
|
||||
commands = [
|
||||
"dm attach swrast SWDM",
|
||||
"draw -m{} {}".format(mode, " ".join(tops)),
|
||||
"autoview",
|
||||
"ae {} {}".format(azimuth, elevation),
|
||||
"screengrab -D SWDM {}".format(image),
|
||||
"quit",
|
||||
]
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
[str(gsh), "--new-cmds", str(model)],
|
||||
input="\n".join(commands) + "\n",
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
timeout=timeout,
|
||||
)
|
||||
except subprocess.TimeoutExpired as error:
|
||||
return {"model": str(model), "mode": mode, "image": str(image),
|
||||
"returncode": 124, "complete": False,
|
||||
"log": str(error.stdout or "")[-4000:] + "\ntimed out during capture"}
|
||||
return {
|
||||
"model": str(model),
|
||||
"mode": mode,
|
||||
"image": str(image),
|
||||
"returncode": proc.returncode,
|
||||
"complete": proc.returncode == 0 and image.is_file() and image.stat().st_size > 0,
|
||||
"log": proc.stdout[-4000:],
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("input_directory", type=Path)
|
||||
parser.add_argument("output_directory", type=Path)
|
||||
parser.add_argument("--gsh", type=Path, default=Path(".build/bin/gsh"))
|
||||
parser.add_argument("--jobs", type=int, default=min(2, os.cpu_count() or 1))
|
||||
parser.add_argument("--timeout", type=int, default=300,
|
||||
help="maximum seconds per model and view")
|
||||
parser.add_argument("--mode", choices=("wireframe", "shaded", "both"), default="both")
|
||||
parser.add_argument("--only", help="regular expression matched against model names")
|
||||
parser.add_argument("--azimuth", type=float, default=35.0)
|
||||
parser.add_argument("--elevation", type=float, default=25.0)
|
||||
args = parser.parse_args()
|
||||
if args.jobs < 1:
|
||||
parser.error("--jobs must be positive")
|
||||
if args.timeout < 1:
|
||||
parser.error("--timeout must be positive")
|
||||
if not args.gsh.is_file():
|
||||
parser.error("gsh not found: {}".format(args.gsh))
|
||||
|
||||
matcher = re.compile(args.only) if args.only else None
|
||||
models = sorted(path for path in args.input_directory.glob("*.g")
|
||||
if not matcher or matcher.search(path.name))
|
||||
if not models:
|
||||
parser.error("no converted .g models in {}".format(args.input_directory))
|
||||
modes = [("wireframe", 0), ("shaded", 1)] if args.mode == "both" else [
|
||||
(args.mode, 0 if args.mode == "wireframe" else 1)
|
||||
]
|
||||
jobs = []
|
||||
for name, mode in modes:
|
||||
target = args.output_directory / name
|
||||
jobs.extend((model, target, mode) for model in models)
|
||||
|
||||
with ThreadPoolExecutor(max_workers=args.jobs) as pool:
|
||||
results = list(pool.map(
|
||||
lambda item: capture(args.gsh, item[0], item[1], item[2],
|
||||
args.azimuth, args.elevation, args.timeout), jobs
|
||||
))
|
||||
args.output_directory.mkdir(parents=True, exist_ok=True)
|
||||
manifest = {
|
||||
"format": "brlcad-step-corpus-capture-v1",
|
||||
"azimuth": args.azimuth,
|
||||
"elevation": args.elevation,
|
||||
"jobs": args.jobs,
|
||||
"results": results,
|
||||
}
|
||||
(args.output_directory / "capture-manifest.json").write_text(
|
||||
json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8"
|
||||
)
|
||||
failed = [item for item in results if not item["complete"]]
|
||||
print("captured {}/{} images".format(len(results) - len(failed), len(results)))
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,501 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run the schema-dispatching BRL-CAD STEP importer over an external corpus.
|
||||
|
||||
Unknown schemas are recorded in the run manifest but are not passed to the
|
||||
converter. Each supported input gets a structured report, a diagnostic log,
|
||||
and a tab-separated status record consumed by ``step_corpus_summary.py``.
|
||||
Nested input directories are scanned recursively, and existing complete
|
||||
records may be resumed.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
STEP_SUFFIXES = {".stp", ".step", ".p21"}
|
||||
INCOMPLETE_PROCESS_STATUSES = {124, 130, 137, 143}
|
||||
|
||||
|
||||
def schema_names(path):
|
||||
with path.open("rb") as stream:
|
||||
header = stream.read(2 * 1024 * 1024).decode("latin-1", errors="replace")
|
||||
match = re.search(
|
||||
r"FILE_SCHEMA\s*\(\s*\((.*?)\)\s*\)", header,
|
||||
re.IGNORECASE | re.DOTALL,
|
||||
)
|
||||
if not match:
|
||||
return []
|
||||
return [
|
||||
value.replace("''", "'").upper()
|
||||
for value in re.findall(r"'((?:''|[^'])*)'", match.group(1))
|
||||
]
|
||||
|
||||
|
||||
def schema_family(names):
|
||||
"""Mirror the public host's supported-family and interim-profile routing."""
|
||||
families = set()
|
||||
legacy_ap203e2 = {
|
||||
"CONFIGURATION_CONTROL_3D_DESIGN_MIM_LF",
|
||||
"CCD_CLA_GVP_AST",
|
||||
"CCD_CLA_GVP_AST_ASD",
|
||||
}
|
||||
interim_companions = (
|
||||
"GEOMETRIC_VALIDATION_PROPERTIES_MIM",
|
||||
"GEOMETRIC_VALIDATION_PROPERTY_REPRESENTATION_MIM",
|
||||
"SHAPE_APPEARANCE_LAYER_MIM",
|
||||
"SHAPE_APPEARANCE_LAYERS_MIM",
|
||||
)
|
||||
for name in names:
|
||||
if "AP203_CONFIGURATION_CONTROLLED" in name or any(
|
||||
name.startswith(alias) for alias in legacy_ap203e2):
|
||||
families.add("AP203e2")
|
||||
elif "AUTOMOTIVE_DESIGN" in name:
|
||||
families.add("AP214")
|
||||
elif "CONFIG_CONTROL_DESIGN" in name:
|
||||
families.add("AP203")
|
||||
elif "AP242" in name or "MANAGED_MODEL_BASED_3D_ENGINEERING" in name:
|
||||
families.add("AP242")
|
||||
|
||||
if families == {"AP203"} and any(
|
||||
name.startswith(companion)
|
||||
for name in names for companion in interim_companions):
|
||||
return "AP203e2"
|
||||
return next(iter(families)) if len(families) == 1 else "unsupported"
|
||||
|
||||
|
||||
def artifact_stem(path, input_directory):
|
||||
relative = path.relative_to(input_directory).as_posix()
|
||||
readable = re.sub(r"[^A-Za-z0-9_.-]+", "_", relative)
|
||||
# Sanitizing path separators and punctuation is not injective. Retain a
|
||||
# readable prefix while making nested and similarly named inputs safe.
|
||||
identity = hashlib.sha256(relative.encode("utf-8")).hexdigest()[:12]
|
||||
return "{}__{}_".format(readable, identity)
|
||||
|
||||
|
||||
def legacy_artifact_stem(path, input_directory):
|
||||
"""Return the pre-v2 stem so an in-progress corpus can be resumed."""
|
||||
relative = path.relative_to(input_directory).as_posix()
|
||||
return re.sub(r"[^A-Za-z0-9_.-]+", "_", relative) + "_"
|
||||
|
||||
|
||||
def completed_status(path):
|
||||
try:
|
||||
status = int(path.read_text(encoding="utf-8").split("\t", 1)[0])
|
||||
except (OSError, ValueError):
|
||||
return False
|
||||
return status not in INCOMPLETE_PROCESS_STATUSES
|
||||
|
||||
|
||||
def synthetic_report(input_path, status, message):
|
||||
return {
|
||||
"format": "brlcad-step-import-report-v1",
|
||||
"input": str(input_path),
|
||||
"output": "",
|
||||
"exit_status": status,
|
||||
"coverage": {
|
||||
"geometry_attempted": 0,
|
||||
"geometry_written": 0,
|
||||
"geometry_skipped": 0,
|
||||
},
|
||||
"validation": {
|
||||
"invalid_breps": 0,
|
||||
"output_failures": 0,
|
||||
"repairs": 0,
|
||||
"inferred_curves": 0,
|
||||
},
|
||||
"diagnostics": [{
|
||||
"severity": "error",
|
||||
"entity_id": 0,
|
||||
"entity_type": "",
|
||||
"attribute": "",
|
||||
"message": message,
|
||||
"count": 1,
|
||||
"aggregated": False,
|
||||
}],
|
||||
"timings_us": {},
|
||||
"peak_rss_bytes": 0,
|
||||
"skipped_items": [],
|
||||
"corpus_run_incomplete": True,
|
||||
}
|
||||
|
||||
|
||||
def file_sha256(path):
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def executable_identity(path):
|
||||
stat = path.stat()
|
||||
return {
|
||||
"path": str(path),
|
||||
"size": stat.st_size,
|
||||
"mtime_ns": stat.st_mtime_ns,
|
||||
"sha256": file_sha256(path),
|
||||
}
|
||||
|
||||
|
||||
def schema_plugin_paths(step_g):
|
||||
"""Locate build/install-tree schema modules used by the plugin host."""
|
||||
plugin_directory = step_g.parent.parent / "libexec" / "step"
|
||||
if not plugin_directory.is_dir():
|
||||
return []
|
||||
return sorted(
|
||||
path for path in plugin_directory.glob("libstep-schema-*")
|
||||
if path.is_file()
|
||||
)
|
||||
|
||||
|
||||
def schema_plugin_identities(step_g):
|
||||
"""Identify build/install-tree schema modules used by the plugin host."""
|
||||
return [executable_identity(path) for path in schema_plugin_paths(step_g)]
|
||||
|
||||
|
||||
def linked_library_identities(executables, runtime_root):
|
||||
"""Hash resolved runtime libraries supplied by the selected build tree.
|
||||
|
||||
Converter and plugin hashes do not detect a rebuilt geometry or STEPcode
|
||||
shared library. On platforms with ``ldd``, retain every resolved library
|
||||
below the executable's build/install root so a long run cannot be mistaken
|
||||
for a homogeneous baseline after one of those dependencies changes.
|
||||
"""
|
||||
libraries = {}
|
||||
for executable in executables:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["ldd", str(executable)], check=False, capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
except OSError:
|
||||
return []
|
||||
if result.returncode:
|
||||
continue
|
||||
for line in result.stdout.splitlines():
|
||||
match = re.search(r"=>\s+(/\S+)", line)
|
||||
if not match:
|
||||
match = re.match(r"\s*(/\S+)", line)
|
||||
if not match:
|
||||
continue
|
||||
path = Path(match.group(1)).resolve()
|
||||
try:
|
||||
path.relative_to(runtime_root)
|
||||
except ValueError:
|
||||
continue
|
||||
if path.is_file():
|
||||
libraries[str(path)] = path
|
||||
return [executable_identity(libraries[name]) for name in sorted(libraries)]
|
||||
|
||||
|
||||
def host_snapshot():
|
||||
"""Capture lightweight run context without adding a monitoring dependency."""
|
||||
snapshot = {
|
||||
"utc": datetime.datetime.now(datetime.timezone.utc).replace(
|
||||
microsecond=0
|
||||
).isoformat(),
|
||||
}
|
||||
try:
|
||||
snapshot["load_average"] = list(os.getloadavg())
|
||||
except (AttributeError, OSError):
|
||||
pass
|
||||
try:
|
||||
values = {}
|
||||
with Path("/proc/meminfo").open("r", encoding="ascii") as stream:
|
||||
for line in stream:
|
||||
key, value = line.split(":", 1)
|
||||
if key in ("MemTotal", "MemAvailable"):
|
||||
values[key] = int(value.split()[0]) * 1024
|
||||
if values:
|
||||
snapshot["memory_bytes"] = values
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
return snapshot
|
||||
|
||||
|
||||
def cpu_model():
|
||||
try:
|
||||
with Path("/proc/cpuinfo").open("r", encoding="ascii") as stream:
|
||||
for line in stream:
|
||||
if line.lower().startswith("model name"):
|
||||
return line.split(":", 1)[1].strip()
|
||||
except (OSError, IndexError):
|
||||
pass
|
||||
return platform.processor()
|
||||
|
||||
|
||||
def write_manifest(path, manifest):
|
||||
"""Atomically preserve progress so an interrupted long run is auditable."""
|
||||
temporary = path.with_suffix(path.suffix + ".tmp")
|
||||
temporary.write_text(
|
||||
json.dumps(manifest, indent=2, sort_keys=True) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
temporary.replace(path)
|
||||
|
||||
|
||||
def terminate_process(process, force=False):
|
||||
try:
|
||||
if os.name == "nt":
|
||||
process.kill() if force else process.terminate()
|
||||
else:
|
||||
os.killpg(process.pid, signal.SIGKILL if force else signal.SIGTERM)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
|
||||
|
||||
def run_converter(command, log, timeout):
|
||||
"""Run one converter and terminate its complete process group on timeout."""
|
||||
process = subprocess.Popen(
|
||||
command, stdout=log, stderr=subprocess.STDOUT,
|
||||
start_new_session=os.name != "nt",
|
||||
)
|
||||
try:
|
||||
return process.wait(timeout=timeout if timeout > 0 else None), False
|
||||
except subprocess.TimeoutExpired:
|
||||
terminate_process(process)
|
||||
try:
|
||||
process.wait(timeout=30)
|
||||
except subprocess.TimeoutExpired:
|
||||
terminate_process(process, force=True)
|
||||
process.wait()
|
||||
return 124, True
|
||||
except BaseException:
|
||||
terminate_process(process)
|
||||
try:
|
||||
process.wait(timeout=30)
|
||||
except subprocess.TimeoutExpired:
|
||||
terminate_process(process, force=True)
|
||||
process.wait()
|
||||
raise
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("input_directory", type=Path)
|
||||
parser.add_argument("output_directory", type=Path)
|
||||
parser.add_argument("--step-g", type=Path, default=Path(".build/bin/step-g"))
|
||||
parser.add_argument("-j", "--jobs", type=int, default=min(12, os.cpu_count() or 1),
|
||||
help="geometry workers inside each converter")
|
||||
parser.add_argument("--previous", type=Path,
|
||||
help="previous corpus-summary.json for trend deltas")
|
||||
parser.add_argument("--expectations", type=Path,
|
||||
help="checksum-pinned corpus expectation manifest")
|
||||
parser.add_argument("--allow-missing-expectations", action="store_true",
|
||||
help="allow expectation entries absent from a filtered run")
|
||||
parser.add_argument("--run-id", help="stable label for this run")
|
||||
parser.add_argument("--resume", action="store_true")
|
||||
parser.add_argument("--exact", action="store_true")
|
||||
parser.add_argument("--timeout", type=float, default=0.0,
|
||||
help="outer wall-clock limit per file in seconds (0 disables)")
|
||||
parser.add_argument("--stall-timeout", type=float,
|
||||
help="override step-g's no-progress timeout in seconds")
|
||||
parser.add_argument(
|
||||
"--only", help="regular expression matched against relative input paths"
|
||||
)
|
||||
parser.add_argument("--list", action="store_true",
|
||||
help="classify inputs without running converters")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.jobs < 1 or args.timeout < 0:
|
||||
parser.error("--jobs must be positive and --timeout must be nonnegative")
|
||||
if args.stall_timeout is not None and args.stall_timeout <= 0:
|
||||
parser.error("--stall-timeout must be positive")
|
||||
input_directory = args.input_directory.resolve()
|
||||
output_directory = args.output_directory.resolve()
|
||||
output_directory.mkdir(parents=True, exist_ok=True)
|
||||
matcher = re.compile(args.only) if args.only else None
|
||||
inputs = sorted(
|
||||
path for path in input_directory.rglob("*")
|
||||
if path.is_file() and path.suffix.lower() in STEP_SUFFIXES and
|
||||
(not matcher or matcher.search(path.relative_to(input_directory).as_posix()))
|
||||
)
|
||||
if not inputs:
|
||||
parser.error("no matching STEP files in {}".format(input_directory))
|
||||
|
||||
run_id = args.run_id or datetime.datetime.now(
|
||||
datetime.timezone.utc
|
||||
).strftime("%Y%m%dT%H%M%SZ")
|
||||
step_g = args.step_g.resolve()
|
||||
if not step_g.is_file():
|
||||
parser.error("step-g not found: {}".format(step_g))
|
||||
plugin_paths = schema_plugin_paths(step_g)
|
||||
runtime_root = step_g.parent.parent.resolve()
|
||||
manifest = {
|
||||
"format": "brlcad-step-corpus-run-v1",
|
||||
"run_id": run_id,
|
||||
"started_utc": datetime.datetime.now(datetime.timezone.utc).replace(
|
||||
microsecond=0
|
||||
).isoformat(),
|
||||
"host": {
|
||||
"hostname": platform.node(),
|
||||
"platform": platform.platform(),
|
||||
"machine": platform.machine(),
|
||||
"cpu_model": cpu_model(),
|
||||
"logical_cpus": os.cpu_count(),
|
||||
"initial_snapshot": host_snapshot(),
|
||||
},
|
||||
"input_directory": str(input_directory),
|
||||
"output_directory": str(output_directory),
|
||||
"options": {
|
||||
"jobs": args.jobs,
|
||||
"exact": args.exact,
|
||||
"recursive": True,
|
||||
"timeout_seconds": args.timeout,
|
||||
"stall_timeout_seconds": args.stall_timeout,
|
||||
},
|
||||
"step_g": executable_identity(step_g),
|
||||
"schema_plugins": [executable_identity(path) for path in plugin_paths],
|
||||
"linked_libraries": linked_library_identities(
|
||||
[step_g] + plugin_paths, runtime_root
|
||||
),
|
||||
"expectations": str(args.expectations.resolve()) if args.expectations else None,
|
||||
"files": [],
|
||||
}
|
||||
manifest_path = output_directory / "run-manifest.json"
|
||||
write_manifest(manifest_path, manifest)
|
||||
|
||||
for index, input_path in enumerate(inputs, 1):
|
||||
relative_input = input_path.relative_to(input_directory).as_posix()
|
||||
declared_schemas = schema_names(input_path)
|
||||
declared_schema = declared_schemas[0] if declared_schemas else ""
|
||||
family = schema_family(declared_schemas)
|
||||
record = {
|
||||
"input": str(input_path),
|
||||
"declared_schema": declared_schema,
|
||||
"declared_schemas": declared_schemas,
|
||||
"schema": family,
|
||||
}
|
||||
if args.list:
|
||||
print("{}\t{}\t{}".format(
|
||||
family, ";".join(declared_schemas), relative_input
|
||||
))
|
||||
manifest["files"].append(record)
|
||||
write_manifest(manifest_path, manifest)
|
||||
continue
|
||||
input_stat = input_path.stat()
|
||||
record["input_identity"] = {
|
||||
"size": input_stat.st_size,
|
||||
"mtime_ns": input_stat.st_mtime_ns,
|
||||
"sha256": file_sha256(input_path),
|
||||
}
|
||||
if family == "unsupported":
|
||||
record["result"] = "skipped"
|
||||
record["reason"] = "unrecognized schema"
|
||||
manifest["files"].append(record)
|
||||
write_manifest(manifest_path, manifest)
|
||||
print("[{}/{}] skip {} ({})".format(
|
||||
index, len(inputs), relative_input, family
|
||||
))
|
||||
continue
|
||||
|
||||
stem = artifact_stem(input_path, input_directory)
|
||||
report_path = output_directory / (stem + ".json")
|
||||
log_path = output_directory / (stem + ".log")
|
||||
status_path = output_directory / (stem + ".status")
|
||||
if args.resume:
|
||||
legacy_stem = legacy_artifact_stem(input_path, input_directory)
|
||||
legacy_report = output_directory / (legacy_stem + ".json")
|
||||
legacy_status = output_directory / (legacy_stem + ".status")
|
||||
if legacy_report.is_file() and completed_status(legacy_status):
|
||||
stem = legacy_stem
|
||||
report_path = legacy_report
|
||||
log_path = output_directory / (stem + ".log")
|
||||
status_path = legacy_status
|
||||
if args.resume and report_path.is_file() and completed_status(status_path):
|
||||
record["result"] = "resumed"
|
||||
record["report"] = report_path.name
|
||||
manifest["files"].append(record)
|
||||
write_manifest(manifest_path, manifest)
|
||||
print("[{}/{}] keep {}".format(index, len(inputs), relative_input))
|
||||
continue
|
||||
|
||||
command = [
|
||||
str(step_g), "-D", "-j", str(args.jobs),
|
||||
"--report", str(report_path),
|
||||
]
|
||||
if args.exact:
|
||||
command.append("--exact")
|
||||
if args.stall_timeout is not None:
|
||||
command.extend(["--stall-timeout", str(args.stall_timeout)])
|
||||
command.append(str(input_path))
|
||||
print("[{}/{}] {} {}".format(index, len(inputs), family, relative_input),
|
||||
flush=True)
|
||||
record.update({"result": "running", "host_before": host_snapshot()})
|
||||
manifest["files"].append(record)
|
||||
write_manifest(manifest_path, manifest)
|
||||
started = time.monotonic()
|
||||
with log_path.open("wb") as log:
|
||||
try:
|
||||
status, timed_out = run_converter(command, log, args.timeout)
|
||||
message = "converter exited without writing a structured report"
|
||||
if timed_out:
|
||||
message = "converter exceeded the {:.1f}-second outer timeout".format(
|
||||
args.timeout
|
||||
)
|
||||
except OSError as error:
|
||||
status = 127
|
||||
timed_out = False
|
||||
message = "could not execute converter: {}".format(error)
|
||||
elapsed = time.monotonic() - started
|
||||
if not report_path.is_file():
|
||||
report_path.write_text(
|
||||
json.dumps(synthetic_report(input_path, status, message), indent=2) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
status_path.write_text(
|
||||
"{}\t{:.3f}\t{}\t{}\n".format(status, elapsed, family, input_path),
|
||||
encoding="utf-8",
|
||||
)
|
||||
record.update({
|
||||
"result": "completed",
|
||||
"process_exit_status": status,
|
||||
"elapsed_seconds": elapsed,
|
||||
"timed_out": timed_out,
|
||||
"report": report_path.name,
|
||||
"log": log_path.name,
|
||||
"host_after": host_snapshot(),
|
||||
})
|
||||
write_manifest(manifest_path, manifest)
|
||||
|
||||
manifest["finished_utc"] = datetime.datetime.now(
|
||||
datetime.timezone.utc
|
||||
).replace(microsecond=0).isoformat()
|
||||
manifest["host"]["final_snapshot"] = host_snapshot()
|
||||
write_manifest(manifest_path, manifest)
|
||||
if not args.list:
|
||||
summary_command = [
|
||||
sys.executable,
|
||||
str(Path(__file__).with_name("step_corpus_summary.py")),
|
||||
str(output_directory),
|
||||
"--run-id", run_id,
|
||||
]
|
||||
if args.previous:
|
||||
summary_command.extend(["--previous", str(args.previous.resolve())])
|
||||
summary_status = subprocess.call(summary_command)
|
||||
if summary_status or not args.expectations:
|
||||
return summary_status
|
||||
verify_command = [
|
||||
sys.executable,
|
||||
str(Path(__file__).with_name("step_corpus_verify.py")),
|
||||
str(output_directory),
|
||||
str(args.expectations.resolve()),
|
||||
]
|
||||
if args.allow_missing_expectations:
|
||||
verify_command.append("--allow-missing")
|
||||
return subprocess.call(verify_command)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,939 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Summarize one BRL-CAD STEP converter corpus run and compare it to another.
|
||||
|
||||
The corpus runner writes one ``NAME.status`` record and one ``NAME.json``
|
||||
converter report per input. A status record is tab separated::
|
||||
|
||||
exit_status<TAB>elapsed_seconds<TAB>schema<TAB>input_path
|
||||
|
||||
This utility intentionally consumes the converters' structured reports rather
|
||||
than scraping diagnostics. It writes a durable JSON record suitable for trend
|
||||
plots and a compact Markdown companion for review.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import statistics
|
||||
import subprocess
|
||||
import sys
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
FORMAT = "brlcad-step-corpus-summary-v2"
|
||||
|
||||
|
||||
INCOMPLETE_PROCESS_STATUSES = {124, 130, 137, 143}
|
||||
|
||||
|
||||
def run_complete(item):
|
||||
"""Return whether an input reached a normal converter terminal state."""
|
||||
if "run_complete" in item:
|
||||
return bool(item["run_complete"])
|
||||
return int(item.get("process_exit_status", 0)) not in INCOMPLETE_PROCESS_STATUSES
|
||||
|
||||
|
||||
def read_json(path):
|
||||
with path.open("r", encoding="utf-8") as stream:
|
||||
return json.load(stream)
|
||||
|
||||
|
||||
def git_state(start):
|
||||
try:
|
||||
revision = subprocess.check_output(
|
||||
["git", "-C", str(start), "rev-parse", "HEAD"],
|
||||
stderr=subprocess.DEVNULL,
|
||||
text=True,
|
||||
).strip()
|
||||
dirty = bool(
|
||||
subprocess.check_output(
|
||||
["git", "-C", str(start), "status", "--porcelain"],
|
||||
stderr=subprocess.DEVNULL,
|
||||
text=True,
|
||||
).strip()
|
||||
)
|
||||
return revision, dirty
|
||||
except (OSError, subprocess.CalledProcessError):
|
||||
return "unknown", None
|
||||
|
||||
|
||||
def failure_category(reason):
|
||||
text = (reason or "").lower()
|
||||
if any(word in text for word in ("budget", "expired", "timed out", "timeout")):
|
||||
return "work_budget"
|
||||
if any(word in text for word in (
|
||||
"not solid", "non-solid", "solid validation", "validate as a solid"
|
||||
)):
|
||||
return "non_solid"
|
||||
if "structural" in text:
|
||||
return "structural"
|
||||
if "exact" in text or "conversion failed" in text:
|
||||
return "exact_conversion"
|
||||
if "opennurbs" in text:
|
||||
return "structural"
|
||||
if "no importer" in text or "unsupported" in text:
|
||||
return "unsupported"
|
||||
if "output" in text or "database" in text or "write" in text:
|
||||
return "output"
|
||||
return "other"
|
||||
|
||||
|
||||
def parse_status(path):
|
||||
fields = path.read_text(encoding="utf-8").rstrip("\n").split("\t", 3)
|
||||
if len(fields) != 4:
|
||||
raise ValueError("{} is not a four-field corpus status record".format(path))
|
||||
return {
|
||||
"process_exit_status": int(fields[0]),
|
||||
"elapsed_seconds": float(fields[1]),
|
||||
"schema": fields[2],
|
||||
"input": fields[3],
|
||||
}
|
||||
|
||||
|
||||
def occurrence_depth(occurrences):
|
||||
"""Return the maximum product-assembly depth represented by occurrences."""
|
||||
children = {}
|
||||
vertices = set()
|
||||
for occurrence in occurrences:
|
||||
parent = int(occurrence.get("parent_product_id") or 0)
|
||||
child = int(occurrence.get("child_product_id") or 0)
|
||||
if parent <= 0 or child <= 0:
|
||||
continue
|
||||
children.setdefault(parent, set()).add(child)
|
||||
vertices.update((parent, child))
|
||||
|
||||
active = set()
|
||||
memo = {}
|
||||
|
||||
def visit(vertex):
|
||||
if vertex in memo:
|
||||
return memo[vertex]
|
||||
if vertex in active:
|
||||
# A cycle is invalid, but retain a useful finite metric and let the
|
||||
# converter's structured diagnostics report the underlying issue.
|
||||
return 0
|
||||
active.add(vertex)
|
||||
depth = max((1 + visit(child)
|
||||
for child in children.get(vertex, ())), default=0)
|
||||
active.remove(vertex)
|
||||
memo[vertex] = depth
|
||||
return depth
|
||||
|
||||
return max((visit(vertex) for vertex in vertices), default=0)
|
||||
|
||||
|
||||
def report_record(status_path):
|
||||
status = parse_status(status_path)
|
||||
report_path = status_path.with_suffix(".json")
|
||||
if not report_path.is_file():
|
||||
raise ValueError("missing report paired with {}".format(status_path))
|
||||
report = read_json(report_path)
|
||||
coverage = report.get("coverage") or {}
|
||||
validation = report.get("validation") or {}
|
||||
timings = report.get("timings_us") or {}
|
||||
performance = report.get("performance") or {}
|
||||
calibration = performance.get("calibration") or {}
|
||||
stage_timings = performance.get("stages") or {}
|
||||
skipped = report.get("skipped_items") or []
|
||||
occurrences = report.get("occurrence_details") or []
|
||||
configuration_records = report.get("configuration_records") or []
|
||||
representation_coverage = report.get("representation_coverage") or []
|
||||
attempted = int(coverage.get("geometry_attempted") or 0)
|
||||
written = int(coverage.get("geometry_written") or 0)
|
||||
skipped_count = int(coverage.get("geometry_skipped") or len(skipped))
|
||||
categories = Counter(failure_category(item.get("reason")) for item in skipped)
|
||||
invalid_breps = int(validation.get("invalid_breps") or 0)
|
||||
invalid_breps_written = int(validation.get("invalid_breps_written") or 0)
|
||||
invalid_breps_rejected = int(validation.get("invalid_breps_rejected") or 0)
|
||||
output_failures = int(validation.get("output_failures") or 0)
|
||||
inferred_curves = int(validation.get("inferred_curves") or 0)
|
||||
if invalid_breps_written:
|
||||
categories["invalid_brep_preserved"] += invalid_breps_written
|
||||
if output_failures:
|
||||
categories["output"] += output_failures
|
||||
configuration_types = Counter(
|
||||
item.get("type") or "UNKNOWN" for item in configuration_records
|
||||
)
|
||||
representation_statuses = Counter(
|
||||
item.get("status") or "unknown" for item in representation_coverage
|
||||
)
|
||||
converter_status = int(
|
||||
report.get("exit_status", status["process_exit_status"])
|
||||
)
|
||||
intentionally_empty = (
|
||||
status["process_exit_status"] == 6 and converter_status == 6 and
|
||||
attempted == 0 and bool(representation_coverage) and
|
||||
all(item.get("status") == "intentionally_non_geometric"
|
||||
for item in representation_coverage)
|
||||
)
|
||||
input_path = str(report.get("input") or status["input"])
|
||||
result = {
|
||||
"name": os.path.basename(input_path),
|
||||
"input": input_path,
|
||||
"schema": status["schema"],
|
||||
"report": report_path.name,
|
||||
"process_exit_status": status["process_exit_status"],
|
||||
"converter_exit_status": converter_status,
|
||||
"run_complete": status["process_exit_status"] not in
|
||||
INCOMPLETE_PROCESS_STATUSES,
|
||||
"elapsed_seconds": status["elapsed_seconds"],
|
||||
"geometry_attempted": attempted,
|
||||
"geometry_written": written,
|
||||
"geometry_skipped": skipped_count,
|
||||
"success_fraction": (float(written) / attempted) if attempted else 0.0,
|
||||
"products": int(coverage.get("products") or 0),
|
||||
"occurrences": int(coverage.get("occurrences") or 0),
|
||||
"assembly_usage_count": len(report.get("assembly_usages") or []),
|
||||
"occurrence_detail_count": len(occurrences),
|
||||
"occurrence_max_depth": occurrence_depth(occurrences),
|
||||
"occurrence_methods": sorted({
|
||||
item.get("shape_method") or "unknown" for item in occurrences
|
||||
}),
|
||||
"styles_extracted": int(coverage.get("styles_extracted") or 0),
|
||||
"styles_applied": int(coverage.get("styles_applied") or 0),
|
||||
"layers_extracted": int(coverage.get("layers_extracted") or 0),
|
||||
"materials_extracted": int(coverage.get("materials_extracted") or 0),
|
||||
"properties_extracted": int(coverage.get("properties_extracted") or 0),
|
||||
"configuration_record_count": len(configuration_records),
|
||||
"configuration_record_types": dict(sorted(configuration_types.items())),
|
||||
"configuration_metadata_count": len(
|
||||
report.get("configuration_metadata") or {}
|
||||
),
|
||||
"product_alternative_count": len(
|
||||
report.get("product_alternatives") or []
|
||||
),
|
||||
"usage_substitute_count": len(report.get("usage_substitutes") or []),
|
||||
"entity_type_counts": dict(sorted(
|
||||
(coverage.get("entity_counts") or {}).items()
|
||||
)),
|
||||
"unsupported_entity_counts": dict(sorted(
|
||||
(coverage.get("unsupported_counts") or {}).items()
|
||||
)),
|
||||
"representation_status_counts": dict(sorted(
|
||||
representation_statuses.items()
|
||||
)),
|
||||
"clean": status["process_exit_status"] not in
|
||||
INCOMPLETE_PROCESS_STATUSES and attempted > 0 and
|
||||
written == attempted and
|
||||
converter_status == 0 and
|
||||
invalid_breps_written == 0 and output_failures == 0 and
|
||||
inferred_curves == 0,
|
||||
"intentionally_empty": intentionally_empty,
|
||||
"invalid_breps": invalid_breps,
|
||||
"invalid_breps_written": invalid_breps_written,
|
||||
"invalid_breps_rejected": invalid_breps_rejected,
|
||||
"output_failures": output_failures,
|
||||
"repairs": int(validation.get("repairs") or 0),
|
||||
"inferred_curves": inferred_curves,
|
||||
"tolerance_mm": validation.get("tolerance_mm"),
|
||||
"peak_rss_bytes": int(report.get("peak_rss_bytes") or 0),
|
||||
"load_us": int(timings.get("load") or 0),
|
||||
"index_us": int(timings.get("index") or 0),
|
||||
"inventory_us": int(timings.get("inventory") or 0),
|
||||
"convert_us": int(timings.get("convert") or 0),
|
||||
"calibration": {
|
||||
"queries": int(calibration.get("queries") or 0),
|
||||
"elapsed_us": int(calibration.get("elapsed_us") or 0),
|
||||
"scalar_queries_per_second": float(
|
||||
calibration.get("scalar_queries_per_second") or 0.0
|
||||
),
|
||||
"parallel_queries_per_second": float(
|
||||
calibration.get("parallel_queries_per_second") or 0.0
|
||||
),
|
||||
},
|
||||
"stage_timings_us": {
|
||||
name: int(values.get("total_us") or 0)
|
||||
for name, values in sorted(stage_timings.items())
|
||||
},
|
||||
"failure_categories": dict(sorted(categories.items())),
|
||||
"skipped_items": [
|
||||
{
|
||||
"entity_id": item.get("entity_id"),
|
||||
"entity_type": item.get("entity_type"),
|
||||
"reason": item.get("reason"),
|
||||
"category": failure_category(item.get("reason")),
|
||||
}
|
||||
for item in skipped
|
||||
],
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
def totals(files):
|
||||
by_schema = {}
|
||||
for schema in sorted(set(item["schema"] for item in files)):
|
||||
by_schema[schema] = totals_base(
|
||||
[item for item in files if item["schema"] == schema]
|
||||
)
|
||||
result = totals_base(files)
|
||||
result["by_schema"] = by_schema
|
||||
return result
|
||||
|
||||
|
||||
def totals_base(files):
|
||||
completed = [item for item in files if run_complete(item)]
|
||||
incomplete = [item for item in files if not run_complete(item)]
|
||||
process_statuses = Counter(
|
||||
str(item["process_exit_status"]) for item in completed
|
||||
)
|
||||
converter_statuses = Counter(
|
||||
str(item["converter_exit_status"]) for item in completed
|
||||
)
|
||||
attempted = sum(item["geometry_attempted"] for item in completed)
|
||||
written = sum(item["geometry_written"] for item in completed)
|
||||
categories = Counter()
|
||||
entity_types = Counter()
|
||||
unsupported_types = Counter()
|
||||
configuration_types = Counter()
|
||||
representation_statuses = Counter()
|
||||
occurrence_methods = set()
|
||||
for item in completed:
|
||||
categories.update(item["failure_categories"])
|
||||
entity_types.update(item.get("entity_type_counts") or {})
|
||||
unsupported_types.update(item.get("unsupported_entity_counts") or {})
|
||||
configuration_types.update(item.get("configuration_record_types") or {})
|
||||
representation_statuses.update(
|
||||
item.get("representation_status_counts") or {}
|
||||
)
|
||||
occurrence_methods.update(item.get("occurrence_methods") or [])
|
||||
return {
|
||||
"files": len(files),
|
||||
"completed_files": len(completed),
|
||||
"inconclusive_files": len(incomplete),
|
||||
"clean_files": sum(1 for item in completed if item["clean"]),
|
||||
"intentionally_empty_files": sum(
|
||||
1 for item in completed if item.get("intentionally_empty")
|
||||
),
|
||||
"process_exit_status_counts": dict(sorted(process_statuses.items())),
|
||||
"converter_exit_status_counts": dict(sorted(converter_statuses.items())),
|
||||
"geometry_attempted": attempted,
|
||||
"geometry_written": written,
|
||||
"geometry_skipped": sum(item["geometry_skipped"] for item in completed),
|
||||
"success_fraction": (float(written) / attempted) if attempted else 0.0,
|
||||
"products": sum(item.get("products", 0) for item in completed),
|
||||
"occurrences": sum(item.get("occurrences", 0) for item in completed),
|
||||
"assembly_usage_count": sum(
|
||||
item.get("assembly_usage_count", 0) for item in completed
|
||||
),
|
||||
"occurrence_detail_count": sum(
|
||||
item.get("occurrence_detail_count", 0) for item in completed
|
||||
),
|
||||
"occurrence_max_depth": max(
|
||||
(item.get("occurrence_max_depth", 0) for item in completed),
|
||||
default=0,
|
||||
),
|
||||
"occurrence_methods": sorted(occurrence_methods),
|
||||
"styles_extracted": sum(
|
||||
item.get("styles_extracted", 0) for item in completed
|
||||
),
|
||||
"styles_applied": sum(
|
||||
item.get("styles_applied", 0) for item in completed
|
||||
),
|
||||
"layers_extracted": sum(
|
||||
item.get("layers_extracted", 0) for item in completed
|
||||
),
|
||||
"materials_extracted": sum(
|
||||
item.get("materials_extracted", 0) for item in completed
|
||||
),
|
||||
"properties_extracted": sum(
|
||||
item.get("properties_extracted", 0) for item in completed
|
||||
),
|
||||
"configuration_record_count": sum(
|
||||
item.get("configuration_record_count", 0) for item in completed
|
||||
),
|
||||
"configuration_metadata_count": sum(
|
||||
item.get("configuration_metadata_count", 0) for item in completed
|
||||
),
|
||||
"product_alternative_count": sum(
|
||||
item.get("product_alternative_count", 0) for item in completed
|
||||
),
|
||||
"usage_substitute_count": sum(
|
||||
item.get("usage_substitute_count", 0) for item in completed
|
||||
),
|
||||
"invalid_breps": sum(
|
||||
item.get("invalid_breps", 0) for item in completed
|
||||
),
|
||||
"invalid_breps_written": sum(
|
||||
item.get("invalid_breps_written", 0) for item in completed
|
||||
),
|
||||
"invalid_breps_rejected": sum(
|
||||
item.get("invalid_breps_rejected", 0) for item in completed
|
||||
),
|
||||
"output_failures": sum(
|
||||
item.get("output_failures", 0) for item in completed
|
||||
),
|
||||
"repairs": sum(item.get("repairs", 0) for item in completed),
|
||||
"inferred_curves": sum(
|
||||
item.get("inferred_curves", 0) for item in completed
|
||||
),
|
||||
"elapsed_seconds": sum(item["elapsed_seconds"] for item in completed),
|
||||
"load_seconds": sum(item.get("load_us", 0) for item in completed) / 1.0e6,
|
||||
"index_seconds": sum(item.get("index_us", 0) for item in completed) / 1.0e6,
|
||||
"inventory_seconds": sum(
|
||||
item.get("inventory_us", 0) for item in completed
|
||||
) / 1.0e6,
|
||||
"convert_seconds": sum(
|
||||
item.get("convert_us", 0) for item in completed
|
||||
) / 1.0e6,
|
||||
"inconclusive_elapsed_seconds": sum(
|
||||
item["elapsed_seconds"] for item in incomplete
|
||||
),
|
||||
"observed_inconclusive_geometry_attempted": sum(
|
||||
item["geometry_attempted"] for item in incomplete
|
||||
),
|
||||
"observed_inconclusive_geometry_written": sum(
|
||||
item["geometry_written"] for item in incomplete
|
||||
),
|
||||
"observed_inconclusive_geometry_skipped": sum(
|
||||
item["geometry_skipped"] for item in incomplete
|
||||
),
|
||||
"peak_file_rss_bytes": max(
|
||||
(item["peak_rss_bytes"] for item in files), default=0
|
||||
),
|
||||
"entity_type_counts": dict(sorted(entity_types.items())),
|
||||
"unsupported_entity_counts": dict(sorted(unsupported_types.items())),
|
||||
"configuration_record_types": dict(sorted(configuration_types.items())),
|
||||
"representation_status_counts": dict(
|
||||
sorted(representation_statuses.items())
|
||||
),
|
||||
"failure_categories": dict(sorted(categories.items())),
|
||||
}
|
||||
|
||||
|
||||
def positive_ratio(current, previous):
|
||||
return (float(current) / float(previous)) if previous > 0 else None
|
||||
|
||||
|
||||
def median_ratio(current_files, previous_files, field):
|
||||
ratios = [
|
||||
positive_ratio(current.get(field, 0), previous.get(field, 0))
|
||||
for current, previous in zip(current_files, previous_files)
|
||||
]
|
||||
ratios = [value for value in ratios if value is not None and value > 0]
|
||||
return statistics.median(ratios) if ratios else None
|
||||
|
||||
|
||||
def geometric_mean_ratio(current_files, previous_files, field):
|
||||
ratios = [
|
||||
positive_ratio(current.get(field, 0), previous.get(field, 0))
|
||||
for current, previous in zip(current_files, previous_files)
|
||||
]
|
||||
ratios = [value for value in ratios if value is not None and value > 0]
|
||||
return math.exp(sum(math.log(value) for value in ratios) / len(ratios)) \
|
||||
if ratios else None
|
||||
|
||||
|
||||
def performance_comparison(current_files, previous_files):
|
||||
current = totals_base(current_files)
|
||||
previous = totals_base(previous_files)
|
||||
fields = {
|
||||
"elapsed": "elapsed_seconds",
|
||||
"load": "load_seconds",
|
||||
"convert": "convert_seconds",
|
||||
"peak_file_rss": "peak_file_rss_bytes",
|
||||
}
|
||||
aggregate_ratios = {
|
||||
label: positive_ratio(current[field], previous[field])
|
||||
for label, field in fields.items()
|
||||
}
|
||||
per_file_fields = {
|
||||
"elapsed": "elapsed_seconds",
|
||||
"load": "load_us",
|
||||
"convert": "convert_us",
|
||||
"peak_rss": "peak_rss_bytes",
|
||||
}
|
||||
return {
|
||||
"comparable_files": len(current_files),
|
||||
"current": {field: current[field] for field in fields.values()},
|
||||
"previous": {field: previous[field] for field in fields.values()},
|
||||
"aggregate_ratios": aggregate_ratios,
|
||||
"median_file_ratios": {
|
||||
label: median_ratio(current_files, previous_files, field)
|
||||
for label, field in per_file_fields.items()
|
||||
},
|
||||
"geometric_mean_file_ratios": {
|
||||
label: geometric_mean_ratio(current_files, previous_files, field)
|
||||
for label, field in per_file_fields.items()
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def converter_options(configuration):
|
||||
options = (configuration or {}).get("options") or {}
|
||||
return {
|
||||
"jobs": options.get("jobs"),
|
||||
"exact": bool(options.get("exact", False)),
|
||||
"stall_timeout_seconds": options.get("stall_timeout_seconds"),
|
||||
}
|
||||
|
||||
|
||||
def converter_identity(configuration):
|
||||
configuration = configuration or {}
|
||||
executable = configuration.get("step_g") or {}
|
||||
plugins = configuration.get("schema_plugins") or []
|
||||
libraries = configuration.get("linked_libraries") or []
|
||||
return {
|
||||
"host": executable.get("sha256"),
|
||||
"schema_plugins": [
|
||||
(os.path.basename(item.get("path") or ""), item.get("sha256"))
|
||||
for item in plugins
|
||||
],
|
||||
"linked_libraries": [
|
||||
(os.path.basename(item.get("path") or ""), item.get("sha256"))
|
||||
for item in libraries
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def host_identity(configuration):
|
||||
host = (configuration or {}).get("host") or {}
|
||||
return {
|
||||
key: host.get(key)
|
||||
for key in ("hostname", "platform", "machine", "cpu_model",
|
||||
"logical_cpus")
|
||||
}
|
||||
|
||||
|
||||
def host_observations(manifest):
|
||||
snapshots = []
|
||||
host = manifest.get("host") or {}
|
||||
for name in ("initial_snapshot", "final_snapshot"):
|
||||
if isinstance(host.get(name), dict):
|
||||
snapshots.append(host[name])
|
||||
for item in manifest.get("files", []):
|
||||
for name in ("host_before", "host_after"):
|
||||
if isinstance(item.get(name), dict):
|
||||
snapshots.append(item[name])
|
||||
|
||||
loads = [item.get("load_average") for item in snapshots]
|
||||
loads = [item for item in loads if isinstance(item, list) and len(item) == 3]
|
||||
available = [
|
||||
(item.get("memory_bytes") or {}).get("MemAvailable")
|
||||
for item in snapshots
|
||||
]
|
||||
available = [int(value) for value in available if value is not None]
|
||||
result = {"snapshot_count": len(snapshots)}
|
||||
if loads:
|
||||
labels = ("one_minute", "five_minutes", "fifteen_minutes")
|
||||
result["load_average"] = {
|
||||
label: {
|
||||
"minimum": min(float(item[index]) for item in loads),
|
||||
"mean": statistics.mean(float(item[index]) for item in loads),
|
||||
"maximum": max(float(item[index]) for item in loads),
|
||||
}
|
||||
for index, label in enumerate(labels)
|
||||
}
|
||||
if available:
|
||||
result["available_memory_bytes"] = {
|
||||
"minimum": min(available),
|
||||
"maximum": max(available),
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
def compare(current_files, previous, current_configuration=None,
|
||||
previous_configuration=None):
|
||||
if not previous:
|
||||
return None
|
||||
old_files = {item["input"]: item for item in previous.get("files", [])}
|
||||
changes = []
|
||||
comparable_current = []
|
||||
comparable_previous = []
|
||||
for current in current_files:
|
||||
old = old_files.get(current["input"])
|
||||
if not run_complete(current):
|
||||
changes.append({
|
||||
"input": current["input"],
|
||||
"change": "inconclusive",
|
||||
"process_exit_status": current["process_exit_status"],
|
||||
})
|
||||
continue
|
||||
if not old:
|
||||
changes.append({"input": current["input"], "change": "new"})
|
||||
continue
|
||||
if not run_complete(old):
|
||||
changes.append({"input": current["input"],
|
||||
"change": "previous_inconclusive"})
|
||||
continue
|
||||
comparable_current.append(current)
|
||||
comparable_previous.append(old)
|
||||
written_delta = current["geometry_written"] - old.get("geometry_written", 0)
|
||||
skipped_delta = current["geometry_skipped"] - old.get("geometry_skipped", 0)
|
||||
inferred_delta = current.get("inferred_curves", 0) - \
|
||||
old.get("inferred_curves", 0)
|
||||
old_skips = {
|
||||
(item.get("entity_id"), item.get("entity_type"), item.get("reason"))
|
||||
for item in old.get("skipped_items", [])
|
||||
}
|
||||
new_skips = {
|
||||
(item.get("entity_id"), item.get("entity_type"), item.get("reason"))
|
||||
for item in current.get("skipped_items", [])
|
||||
}
|
||||
if (written_delta or skipped_delta or inferred_delta or
|
||||
bool(old.get("clean")) != current["clean"] or
|
||||
old_skips != new_skips):
|
||||
changes.append({
|
||||
"input": current["input"],
|
||||
"written_delta": written_delta,
|
||||
"skipped_delta": skipped_delta,
|
||||
"inferred_curves_delta": inferred_delta,
|
||||
"became_clean": current["clean"] and not old.get("clean"),
|
||||
"regressed_from_clean": bool(old.get("clean")) and not current["clean"],
|
||||
"fixed_skips": [list(item) for item in sorted(old_skips - new_skips)],
|
||||
"new_skips": [list(item) for item in sorted(new_skips - old_skips)],
|
||||
})
|
||||
previous_totals = totals(comparable_previous)
|
||||
current_totals = totals(comparable_current)
|
||||
input_hash_pairs = [
|
||||
(current.get("input_sha256"), old.get("input_sha256"))
|
||||
for current, old in zip(comparable_current, comparable_previous)
|
||||
]
|
||||
input_hashes_verified = bool(input_hash_pairs) and all(
|
||||
current and old and current == old
|
||||
for current, old in input_hash_pairs
|
||||
)
|
||||
options_match = bool(
|
||||
current_configuration and previous_configuration
|
||||
) and converter_options(current_configuration) == converter_options(
|
||||
previous_configuration
|
||||
)
|
||||
hosts_match = bool(
|
||||
current_configuration and previous_configuration
|
||||
) and host_identity(current_configuration) == host_identity(
|
||||
previous_configuration
|
||||
)
|
||||
current_observations = (
|
||||
current_configuration or {}
|
||||
).get("host_observations") or {}
|
||||
previous_observations = (
|
||||
previous_configuration or {}
|
||||
).get("host_observations") or {}
|
||||
loads_recorded = bool(
|
||||
current_observations.get("load_average") and
|
||||
previous_observations.get("load_average")
|
||||
)
|
||||
return {
|
||||
"previous_run_id": previous.get("run_id"),
|
||||
"geometry_written_delta": current_totals["geometry_written"] -
|
||||
int(previous_totals.get("geometry_written") or 0),
|
||||
"geometry_skipped_delta": current_totals["geometry_skipped"] -
|
||||
int(previous_totals.get("geometry_skipped") or 0),
|
||||
"clean_files_delta": current_totals["clean_files"] -
|
||||
int(previous_totals.get("clean_files") or 0),
|
||||
"regressed_clean_files": [
|
||||
item["input"] for item in changes if item.get("regressed_from_clean")
|
||||
],
|
||||
"newly_clean_files": [
|
||||
item["input"] for item in changes if item.get("became_clean")
|
||||
],
|
||||
"inconclusive_files": [
|
||||
item["input"] for item in changes
|
||||
if item.get("change") == "inconclusive"
|
||||
],
|
||||
"configuration": {
|
||||
"current_options": (current_configuration or {}).get("options"),
|
||||
"previous_options": (previous_configuration or {}).get("options"),
|
||||
"converter_options_match": options_match,
|
||||
"host_identity_match": hosts_match,
|
||||
"input_hashes_verified": input_hashes_verified,
|
||||
"load_observations_recorded": loads_recorded,
|
||||
"timing_context_comparable": options_match and hosts_match and
|
||||
input_hashes_verified and
|
||||
loads_recorded,
|
||||
"executable_match": bool(
|
||||
(current_configuration or {}).get("step_g") and
|
||||
(previous_configuration or {}).get("step_g")
|
||||
) and converter_identity(current_configuration) ==
|
||||
converter_identity(previous_configuration),
|
||||
"current_outer_timeout_seconds": (
|
||||
(current_configuration or {}).get("options") or {}
|
||||
).get("timeout_seconds"),
|
||||
"previous_outer_timeout_seconds": (
|
||||
(previous_configuration or {}).get("options") or {}
|
||||
).get("timeout_seconds"),
|
||||
"current_host_identity": host_identity(current_configuration),
|
||||
"previous_host_identity": host_identity(previous_configuration),
|
||||
"current_host_observations": current_observations,
|
||||
"previous_host_observations": previous_observations,
|
||||
},
|
||||
"performance": performance_comparison(
|
||||
comparable_current, comparable_previous
|
||||
),
|
||||
"changes": changes,
|
||||
}
|
||||
|
||||
|
||||
def run_configuration(run_directory):
|
||||
path = Path(run_directory) / "run-manifest.json"
|
||||
if not path.is_file():
|
||||
return None
|
||||
manifest = read_json(path)
|
||||
return {
|
||||
"manifest": str(path.resolve()),
|
||||
"options": manifest.get("options"),
|
||||
"step_g": manifest.get("step_g"),
|
||||
"schema_plugins": manifest.get("schema_plugins"),
|
||||
"linked_libraries": manifest.get("linked_libraries"),
|
||||
"host": manifest.get("host"),
|
||||
"host_observations": host_observations(manifest),
|
||||
"started_utc": manifest.get("started_utc"),
|
||||
"finished_utc": manifest.get("finished_utc"),
|
||||
}
|
||||
|
||||
|
||||
def attach_input_identities(files, run_directory):
|
||||
path = Path(run_directory) / "run-manifest.json"
|
||||
if not path.is_file():
|
||||
return
|
||||
manifest = read_json(path)
|
||||
identities = {
|
||||
record.get("input"): record.get("input_identity") or {}
|
||||
for record in manifest.get("files") or []
|
||||
}
|
||||
for item in files:
|
||||
identity = identities.get(item["input"]) or {}
|
||||
if identity.get("sha256"):
|
||||
item["input_sha256"] = identity["sha256"]
|
||||
item["input_size"] = identity.get("size")
|
||||
|
||||
|
||||
def markdown(summary):
|
||||
total = summary["totals"]
|
||||
def count_list(values):
|
||||
return ", ".join(
|
||||
"{} {}".format(count, name)
|
||||
for name, count in sorted(
|
||||
values.items(), key=lambda item: (-item[1], item[0])
|
||||
)
|
||||
)
|
||||
|
||||
lines = [
|
||||
"# STEP corpus run {}".format(summary["run_id"]),
|
||||
"",
|
||||
"Revision: `{}`{}".format(
|
||||
summary["revision"], " (dirty)" if summary.get("dirty") else ""
|
||||
),
|
||||
"",
|
||||
"| Schema | Files clean | Intentional empty | Geometry written | Success |",
|
||||
"|---|---:|---:|---:|---:|",
|
||||
]
|
||||
for schema, values in sorted(total["by_schema"].items()):
|
||||
lines.append("| {} | {}/{} | {} | {}/{} | {:.2%} |".format(
|
||||
schema, values["clean_files"], values["completed_files"],
|
||||
values["intentionally_empty_files"],
|
||||
values["geometry_written"], values["geometry_attempted"],
|
||||
values["success_fraction"],
|
||||
))
|
||||
lines.append("| Combined | {}/{} | {} | {}/{} | {:.2%} |".format(
|
||||
total["clean_files"], total["completed_files"],
|
||||
total["intentionally_empty_files"], total["geometry_written"],
|
||||
total["geometry_attempted"], total["success_fraction"],
|
||||
))
|
||||
lines.extend([
|
||||
"",
|
||||
"Assembly coverage: {} product(s), {} occurrence(s), maximum depth "
|
||||
"{}, using {}.".format(
|
||||
total["products"], total["occurrences"],
|
||||
total["occurrence_max_depth"],
|
||||
", ".join(total["occurrence_methods"]) or "no occurrence method",
|
||||
),
|
||||
"Presentation and metadata: {} style(s) extracted, {} applied, {} "
|
||||
"layer(s), {} material(s), {} property value(s), and {} retained "
|
||||
"configuration record(s).".format(
|
||||
total["styles_extracted"], total["styles_applied"],
|
||||
total["layers_extracted"], total["materials_extracted"],
|
||||
total["properties_extracted"],
|
||||
total["configuration_record_count"],
|
||||
),
|
||||
"Validation: {} invalid B-Rep(s), {} preserved and {} rejected; {} "
|
||||
"bounded repair(s) and {} tagged curve inference(s) applied.".format(
|
||||
total["invalid_breps"], total["invalid_breps_written"],
|
||||
total["invalid_breps_rejected"],
|
||||
total["repairs"],
|
||||
total["inferred_curves"],
|
||||
),
|
||||
])
|
||||
if total["failure_categories"]:
|
||||
lines.append("Geometry omission categories: {}.".format(
|
||||
count_list(total["failure_categories"])
|
||||
))
|
||||
lines.append("Converter terminal statuses: {}.".format(
|
||||
count_list(total["converter_exit_status_counts"])
|
||||
))
|
||||
if total["unsupported_entity_counts"]:
|
||||
lines.append("Unsupported STEP entity inventory: {}.".format(
|
||||
count_list(total["unsupported_entity_counts"])
|
||||
))
|
||||
observations = ((summary.get("run_configuration") or {}).get(
|
||||
"host_observations") or {})
|
||||
one_minute = (observations.get("load_average") or {}).get("one_minute")
|
||||
if one_minute:
|
||||
lines.extend([
|
||||
"",
|
||||
"Host one-minute load over {} boundary samples: {:.2f} mean "
|
||||
"({:.2f}-{:.2f}).".format(
|
||||
observations["snapshot_count"], one_minute["mean"],
|
||||
one_minute["minimum"], one_minute["maximum"],
|
||||
),
|
||||
])
|
||||
if total["inconclusive_files"]:
|
||||
lines.append("")
|
||||
lines.append(
|
||||
"Inconclusive (timed out or interrupted): {} file(s); partial "
|
||||
"geometry is excluded from totals and deltas.".format(
|
||||
total["inconclusive_files"]
|
||||
)
|
||||
)
|
||||
lines.extend([
|
||||
"", "## Per file", "",
|
||||
"| Schema | Input | Status | Written | Skipped | Inferred | Reported load s | "
|
||||
"Reported convert s | Wall s | Peak RSS MiB |",
|
||||
"|---|---|---|---:|---:|---:|---:|---:|---:|---:|",
|
||||
])
|
||||
for item in summary["files"]:
|
||||
lines.append("| {} | {} | {} | {}/{} | {} | {} | {:.1f} | {:.1f} | {:.1f} | {:.1f} |".format(
|
||||
item["schema"], item["name"].replace("|", "\\|"),
|
||||
"clean" if item["clean"] else
|
||||
("intentional empty" if item.get("intentionally_empty") else
|
||||
("partial" if run_complete(item) else "inconclusive")),
|
||||
item["geometry_written"], item["geometry_attempted"],
|
||||
item["geometry_skipped"], item.get("inferred_curves", 0),
|
||||
item["load_us"] / 1.0e6,
|
||||
item["convert_us"] / 1.0e6, item["elapsed_seconds"],
|
||||
item["peak_rss_bytes"] / (1024.0 * 1024.0),
|
||||
))
|
||||
delta = summary.get("delta")
|
||||
if delta:
|
||||
lines.extend([
|
||||
"", "## Delta", "",
|
||||
"Written geometry: {:+d}; skipped geometry: {:+d}; clean files: {:+d}.".format(
|
||||
delta["geometry_written_delta"], delta["geometry_skipped_delta"],
|
||||
delta["clean_files_delta"],
|
||||
),
|
||||
])
|
||||
if delta["regressed_clean_files"]:
|
||||
lines.append("Regressed clean files: {}.".format(
|
||||
", ".join(delta["regressed_clean_files"])
|
||||
))
|
||||
if delta["inconclusive_files"]:
|
||||
lines.append("Not compared because the run was incomplete: {}.".format(
|
||||
", ".join(delta["inconclusive_files"])
|
||||
))
|
||||
performance = delta.get("performance") or {}
|
||||
ratios = performance.get("aggregate_ratios") or {}
|
||||
if performance.get("comparable_files"):
|
||||
lines.extend([
|
||||
"",
|
||||
"Performance over {} comparable files (current/previous): "
|
||||
"wall {:.3f}x and peak-file RSS {:.3f}x. Reported phase "
|
||||
"ratios are load {:.3f}x and conversion {:.3f}x; phase "
|
||||
"boundaries may differ across binaries.".format(
|
||||
performance["comparable_files"],
|
||||
ratios.get("elapsed") or 0.0,
|
||||
ratios.get("peak_file_rss") or 0.0,
|
||||
ratios.get("load") or 0.0,
|
||||
ratios.get("convert") or 0.0,
|
||||
),
|
||||
])
|
||||
configuration = delta.get("configuration") or {}
|
||||
if not configuration.get("converter_options_match"):
|
||||
lines.append(
|
||||
"Run options differ or could not be verified; performance "
|
||||
"ratios are informational only."
|
||||
)
|
||||
if not configuration.get("host_identity_match"):
|
||||
lines.append(
|
||||
"Host identity differs or could not be verified; timing "
|
||||
"ratios are not controlled benchmarks."
|
||||
)
|
||||
if not configuration.get("input_hashes_verified"):
|
||||
lines.append(
|
||||
"Input hashes differ or were not recorded in both runs; "
|
||||
"timing ratios are informational only."
|
||||
)
|
||||
if not configuration.get("load_observations_recorded"):
|
||||
lines.append(
|
||||
"Both runs did not record host-load observations; "
|
||||
"timing ratios are not controlled benchmarks."
|
||||
)
|
||||
else:
|
||||
current_load = configuration["current_host_observations"][
|
||||
"load_average"
|
||||
]["one_minute"]["mean"]
|
||||
previous_load = configuration["previous_host_observations"][
|
||||
"load_average"
|
||||
]["one_minute"]["mean"]
|
||||
lines.append(
|
||||
"Recorded one-minute load mean was {:.2f} current versus "
|
||||
"{:.2f} previous.".format(current_load, previous_load)
|
||||
)
|
||||
if configuration.get("timing_context_comparable"):
|
||||
lines.append(
|
||||
"Host, input hashes, converter options, and host-load "
|
||||
"telemetry are comparable; unrecorded background activity "
|
||||
"may still affect timings."
|
||||
)
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("run_directory", type=Path)
|
||||
parser.add_argument("--previous", type=Path,
|
||||
help="previous corpus-summary.json for delta reporting")
|
||||
parser.add_argument("--output", type=Path,
|
||||
help="output JSON (default: RUN_DIRECTORY/corpus-summary.json)")
|
||||
parser.add_argument("--run-id", help="stable run label; defaults to directory name")
|
||||
parser.add_argument("--no-markdown", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
run_directory = args.run_directory.resolve()
|
||||
status_paths = sorted(run_directory.glob("*.status"))
|
||||
if not status_paths:
|
||||
parser.error("{} contains no *.status corpus records".format(run_directory))
|
||||
try:
|
||||
files = [report_record(path) for path in status_paths]
|
||||
except (OSError, ValueError, json.JSONDecodeError) as error:
|
||||
print("step_corpus_summary.py: {}".format(error), file=sys.stderr)
|
||||
return 2
|
||||
attach_input_identities(files, run_directory)
|
||||
|
||||
revision, dirty = git_state(Path(__file__).resolve().parents[2])
|
||||
previous = read_json(args.previous) if args.previous else None
|
||||
current_configuration = run_configuration(run_directory)
|
||||
previous_configuration = run_configuration(
|
||||
previous.get("run_directory")
|
||||
) if previous and previous.get("run_directory") else None
|
||||
generated = datetime.datetime.now(datetime.timezone.utc).replace(
|
||||
microsecond=0
|
||||
).isoformat()
|
||||
summary = {
|
||||
"format": FORMAT,
|
||||
"run_id": args.run_id or run_directory.name,
|
||||
"generated_utc": generated,
|
||||
"run_directory": str(run_directory),
|
||||
"revision": revision,
|
||||
"dirty": dirty,
|
||||
"run_configuration": current_configuration,
|
||||
"totals": totals(files),
|
||||
"files": files,
|
||||
}
|
||||
summary["delta"] = compare(
|
||||
files, previous, current_configuration, previous_configuration
|
||||
)
|
||||
|
||||
output = args.output or (run_directory / "corpus-summary.json")
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
with output.open("w", encoding="utf-8") as stream:
|
||||
json.dump(summary, stream, indent=2, sort_keys=True)
|
||||
stream.write("\n")
|
||||
if not args.no_markdown:
|
||||
output.with_suffix(".md").write_text(markdown(summary), encoding="utf-8")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,288 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verify a recorded STEP corpus run against checksum-pinned expectations.
|
||||
|
||||
External STEP samples are often valuable but not suitable for copying into
|
||||
BRL-CAD. This tool keeps their evidence reproducible without copying the
|
||||
files: an expectation manifest records the source, accepted local filenames,
|
||||
SHA-256 digest, and report assertions. The verifier consumes the structured
|
||||
reports and run manifest produced by ``step_corpus_run.py``.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
FORMAT = "brlcad-step-corpus-verification-v1"
|
||||
|
||||
|
||||
def read_json(path):
|
||||
with path.open("r", encoding="utf-8") as stream:
|
||||
return json.load(stream)
|
||||
|
||||
|
||||
def file_sha256(path):
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def dotted_value(document, path):
|
||||
value = document
|
||||
for component in path.split("."):
|
||||
if not isinstance(value, dict) or component not in value:
|
||||
raise KeyError(path)
|
||||
value = value[component]
|
||||
return value
|
||||
|
||||
|
||||
def occurrence_depth(occurrences):
|
||||
children = {}
|
||||
vertices = set()
|
||||
for occurrence in occurrences:
|
||||
parent = int(occurrence.get("parent_product_id") or 0)
|
||||
child = int(occurrence.get("child_product_id") or 0)
|
||||
if parent <= 0 or child <= 0:
|
||||
continue
|
||||
children.setdefault(parent, set()).add(child)
|
||||
vertices.update((parent, child))
|
||||
|
||||
active = set()
|
||||
memo = {}
|
||||
|
||||
def visit(vertex):
|
||||
if vertex in memo:
|
||||
return memo[vertex]
|
||||
if vertex in active:
|
||||
raise ValueError("occurrence product graph contains a cycle")
|
||||
active.add(vertex)
|
||||
depth = max((1 + visit(child) for child in children.get(vertex, ())),
|
||||
default=0)
|
||||
active.remove(vertex)
|
||||
memo[vertex] = depth
|
||||
return depth
|
||||
|
||||
return max((visit(vertex) for vertex in vertices), default=0)
|
||||
|
||||
|
||||
def derived_values(report):
|
||||
occurrences = report.get("occurrence_details") or []
|
||||
metadata = report.get("product_metadata") or []
|
||||
materials = [
|
||||
material
|
||||
for product in metadata
|
||||
for material in product.get("materials") or []
|
||||
]
|
||||
material_properties = [
|
||||
prop
|
||||
for material in materials
|
||||
for prop in material.get("properties") or []
|
||||
]
|
||||
material_numeric_values = sorted({
|
||||
value
|
||||
for prop in material_properties
|
||||
for value in prop.get("values") or []
|
||||
if isinstance(value, (int, float))
|
||||
})
|
||||
return {
|
||||
"assembly_usage_count": len(report.get("assembly_usages") or []),
|
||||
"occurrence_detail_count": len(occurrences),
|
||||
"occurrence_max_depth": occurrence_depth(occurrences),
|
||||
"occurrence_methods": sorted({
|
||||
item.get("shape_method") or "" for item in occurrences
|
||||
}),
|
||||
"skipped_entity_ids": sorted({
|
||||
int(item.get("entity_id") or 0)
|
||||
for item in report.get("skipped_items") or []
|
||||
if int(item.get("entity_id") or 0) > 0
|
||||
}),
|
||||
"product_metadata_count": len(metadata),
|
||||
"material_count": len(materials),
|
||||
"material_identifiers": sorted({
|
||||
material.get("identifier") or "" for material in materials
|
||||
}),
|
||||
"material_names": sorted({
|
||||
material.get("name") or "" for material in materials
|
||||
}),
|
||||
"material_property_count": len(material_properties),
|
||||
"material_property_names": sorted({
|
||||
prop.get("name") or "" for prop in material_properties
|
||||
}),
|
||||
"material_property_units": sorted({
|
||||
prop.get("units") or "" for prop in material_properties
|
||||
if prop.get("units")
|
||||
}),
|
||||
"material_property_numeric_values": material_numeric_values,
|
||||
"validation_property_count": sum(
|
||||
len(product.get("validation_properties") or [])
|
||||
for product in metadata
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def compare_expected(report, expected):
|
||||
combined = dict(report)
|
||||
combined["derived"] = derived_values(report)
|
||||
checks = []
|
||||
errors = []
|
||||
for path, wanted in sorted((expected.get("equals") or {}).items()):
|
||||
try:
|
||||
actual = dotted_value(combined, path)
|
||||
except KeyError:
|
||||
errors.append("{} is missing".format(path))
|
||||
continue
|
||||
checks.append(path)
|
||||
if actual != wanted:
|
||||
errors.append("{} is {!r}, expected {!r}".format(
|
||||
path, actual, wanted
|
||||
))
|
||||
for path, wanted in sorted((expected.get("minimum") or {}).items()):
|
||||
try:
|
||||
actual = dotted_value(combined, path)
|
||||
except KeyError:
|
||||
errors.append("{} is missing".format(path))
|
||||
continue
|
||||
checks.append(path)
|
||||
if not isinstance(actual, (int, float)) or actual < wanted:
|
||||
errors.append("{} is {!r}, expected at least {!r}".format(
|
||||
path, actual, wanted
|
||||
))
|
||||
return checks, errors, combined["derived"]
|
||||
|
||||
|
||||
def markdown(result):
|
||||
lines = [
|
||||
"# STEP external corpus verification",
|
||||
"",
|
||||
"Expectation manifest: `{}`".format(result["expectations"]),
|
||||
"",
|
||||
"| Input | Source/producer | Result | Checks |",
|
||||
"|---|---|---:|---:|",
|
||||
]
|
||||
for item in result["files"]:
|
||||
source = item.get("producer") or item.get("source_page") or ""
|
||||
lines.append("| {} | {} | {} | {} |".format(
|
||||
item["name"].replace("|", "\\|"),
|
||||
source.replace("|", "\\|"),
|
||||
item["result"], len(item.get("checks") or []),
|
||||
))
|
||||
for error in item.get("errors") or []:
|
||||
lines.append(" - **{}:** {}".format(item["name"], error))
|
||||
lines.extend([
|
||||
"",
|
||||
"Verified {}/{} present files; {} manifest entries were missing.".format(
|
||||
result["passed_files"], result["present_files"],
|
||||
result["missing_files"],
|
||||
),
|
||||
])
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("run_directory", type=Path)
|
||||
parser.add_argument("expectations", type=Path)
|
||||
parser.add_argument("--allow-missing", action="store_true",
|
||||
help="do not fail for expectation entries absent from the run")
|
||||
parser.add_argument("--output", type=Path,
|
||||
help="output JSON (default: RUN/corpus-verification.json)")
|
||||
args = parser.parse_args()
|
||||
|
||||
run_directory = args.run_directory.resolve()
|
||||
manifest_path = run_directory / "run-manifest.json"
|
||||
if not manifest_path.is_file():
|
||||
parser.error("{} does not contain run-manifest.json".format(run_directory))
|
||||
expectations_path = args.expectations.resolve()
|
||||
run = read_json(manifest_path)
|
||||
expectations = read_json(expectations_path)
|
||||
if expectations.get("format") != "brlcad-step-corpus-expectations-v1":
|
||||
parser.error("unsupported expectation manifest format")
|
||||
|
||||
run_files = {}
|
||||
for record in run.get("files") or []:
|
||||
input_name = Path(record.get("input") or "").name
|
||||
if input_name:
|
||||
run_files[input_name] = record
|
||||
|
||||
results = []
|
||||
passed = 0
|
||||
present = 0
|
||||
missing = 0
|
||||
for expectation in expectations.get("files") or []:
|
||||
names = [expectation["name"]] + list(expectation.get("aliases") or [])
|
||||
record = next((run_files[name] for name in names if name in run_files), None)
|
||||
item = {
|
||||
"name": expectation["name"],
|
||||
"source_page": expectation.get("source_page"),
|
||||
"producer": expectation.get("producer"),
|
||||
"checks": [],
|
||||
"errors": [],
|
||||
}
|
||||
if record is None:
|
||||
missing += 1
|
||||
item["result"] = "missing"
|
||||
if not args.allow_missing:
|
||||
item["errors"].append("input was not present in the corpus run")
|
||||
results.append(item)
|
||||
continue
|
||||
|
||||
present += 1
|
||||
input_path = Path(record["input"])
|
||||
actual_hash = file_sha256(input_path) if input_path.is_file() else None
|
||||
item["input"] = str(input_path)
|
||||
item["sha256"] = actual_hash
|
||||
item["checks"].append("sha256")
|
||||
recorded_hash = (record.get("input_identity") or {}).get("sha256")
|
||||
if recorded_hash and recorded_hash != actual_hash:
|
||||
item["errors"].append(
|
||||
"input changed after the run manifest recorded its SHA-256"
|
||||
)
|
||||
if actual_hash != expectation.get("sha256"):
|
||||
item["errors"].append("SHA-256 is {}, expected {}".format(
|
||||
actual_hash, expectation.get("sha256")
|
||||
))
|
||||
|
||||
report_name = record.get("report")
|
||||
report_path = run_directory / report_name if report_name else None
|
||||
if not report_path or not report_path.is_file():
|
||||
item["errors"].append("structured converter report is missing")
|
||||
else:
|
||||
report = read_json(report_path)
|
||||
checks, errors, derived = compare_expected(
|
||||
report, expectation.get("expected") or {}
|
||||
)
|
||||
item["checks"].extend(checks)
|
||||
item["errors"].extend(errors)
|
||||
item["derived"] = derived
|
||||
item["report"] = str(report_path)
|
||||
|
||||
item["result"] = "pass" if not item["errors"] else "fail"
|
||||
if item["result"] == "pass":
|
||||
passed += 1
|
||||
results.append(item)
|
||||
|
||||
failed = sum(1 for item in results if item.get("errors"))
|
||||
result = {
|
||||
"format": FORMAT,
|
||||
"expectations": str(expectations_path),
|
||||
"run_directory": str(run_directory),
|
||||
"passed_files": passed,
|
||||
"present_files": present,
|
||||
"missing_files": missing,
|
||||
"failed_files": failed,
|
||||
"files": results,
|
||||
}
|
||||
output = args.output or (run_directory / "corpus-verification.json")
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
output.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n",
|
||||
encoding="utf-8")
|
||||
output.with_suffix(".md").write_text(markdown(result), encoding="utf-8")
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,449 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Extract a small, forward-complete STEP geometry subset.
|
||||
|
||||
This helper is intended for local diagnosis and for identifying the minimal
|
||||
geometry that a project-owned synthetic regression test must reproduce. It
|
||||
copies the dependency closure of one or more STEP face entities and wraps them
|
||||
in an OPEN_SHELL product, retaining the source entity numbers so converter
|
||||
diagnostics remain directly comparable to the original file. A diagnostic
|
||||
--closed-shell mode instead uses CLOSED_SHELL/MANIFOLD_SOLID_BREP so reductions
|
||||
can exercise topology rules which only apply to asserted solids. A face subset
|
||||
is not necessarily a valid closed shell, so that mode is for failure reduction,
|
||||
not evidence that the extracted result is valid geometry. The output keeps the
|
||||
input model's provenance and is not automatically suitable for committing or
|
||||
redistribution.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ADVANCED_FACE = re.compile(
|
||||
r"^[ \t]*#[0-9]+[ \t]*=[ \t]*ADVANCED_FACE[ \t]*\(",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
FILE_SCHEMA = re.compile(
|
||||
r"FILE_SCHEMA\s*\(\s*\(\s*'([^']+)'", re.IGNORECASE | re.DOTALL
|
||||
)
|
||||
ENTITY_TYPE = re.compile(
|
||||
r"^[ \t]*#[0-9]+[ \t]*=[ \t]*([A-Z0-9_]+)[ \t]*\(",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def instance_end(text, start):
|
||||
"""Return the first record-ending semicolon outside strings/comments."""
|
||||
in_string = False
|
||||
in_comment = False
|
||||
offset = start
|
||||
while offset < len(text):
|
||||
current = text[offset]
|
||||
following = text[offset + 1] if offset + 1 < len(text) else ""
|
||||
if in_comment:
|
||||
if current == "*" and following == "/":
|
||||
in_comment = False
|
||||
offset += 2
|
||||
continue
|
||||
elif in_string:
|
||||
if current == "'":
|
||||
if following == "'":
|
||||
offset += 2
|
||||
continue
|
||||
in_string = False
|
||||
elif current == "/" and following == "*":
|
||||
in_comment = True
|
||||
offset += 2
|
||||
continue
|
||||
elif current == "'":
|
||||
in_string = True
|
||||
elif current == ";":
|
||||
return offset + 1
|
||||
offset += 1
|
||||
raise ValueError("unterminated STEP instance starting at byte {}".format(start))
|
||||
|
||||
|
||||
def instances(text):
|
||||
"""Return Part 21 instances without recognizing text in strings/comments."""
|
||||
result = {}
|
||||
offset = 0
|
||||
line_start = True
|
||||
in_string = False
|
||||
in_comment = False
|
||||
while offset < len(text):
|
||||
current = text[offset]
|
||||
following = text[offset + 1] if offset + 1 < len(text) else ""
|
||||
if in_comment:
|
||||
if current == "*" and following == "/":
|
||||
in_comment = False
|
||||
offset += 2
|
||||
continue
|
||||
elif in_string:
|
||||
if current == "'":
|
||||
if following == "'":
|
||||
offset += 2
|
||||
continue
|
||||
in_string = False
|
||||
elif current == "/" and following == "*":
|
||||
in_comment = True
|
||||
offset += 2
|
||||
continue
|
||||
elif current == "'":
|
||||
in_string = True
|
||||
elif current == "\n" or current == "\r":
|
||||
line_start = True
|
||||
elif line_start and current in " \t":
|
||||
pass
|
||||
elif line_start and current == "#":
|
||||
digit_end = offset + 1
|
||||
while digit_end < len(text) and text[digit_end].isdigit():
|
||||
digit_end += 1
|
||||
separator = digit_end
|
||||
while separator < len(text) and text[separator] in " \t":
|
||||
separator += 1
|
||||
if digit_end > offset + 1 and separator < len(text) and \
|
||||
text[separator] == "=":
|
||||
entity_id = int(text[offset + 1:digit_end])
|
||||
end = instance_end(text, offset)
|
||||
if entity_id in result:
|
||||
raise ValueError(
|
||||
"duplicate STEP entity #{}".format(entity_id)
|
||||
)
|
||||
result[entity_id] = text[offset:end].strip()
|
||||
offset = end
|
||||
line_start = False
|
||||
continue
|
||||
line_start = False
|
||||
else:
|
||||
line_start = False
|
||||
offset += 1
|
||||
return result
|
||||
|
||||
|
||||
def references(record):
|
||||
"""Yield entity references outside Part 21 strings and comments."""
|
||||
offset = 0
|
||||
in_string = False
|
||||
in_comment = False
|
||||
while offset < len(record):
|
||||
current = record[offset]
|
||||
following = record[offset + 1] if offset + 1 < len(record) else ""
|
||||
if in_comment:
|
||||
if current == "*" and following == "/":
|
||||
in_comment = False
|
||||
offset += 2
|
||||
continue
|
||||
elif in_string:
|
||||
if current == "'":
|
||||
if following == "'":
|
||||
offset += 2
|
||||
continue
|
||||
in_string = False
|
||||
elif current == "/" and following == "*":
|
||||
in_comment = True
|
||||
offset += 2
|
||||
continue
|
||||
elif current == "'":
|
||||
in_string = True
|
||||
elif current == "#":
|
||||
digit_end = offset + 1
|
||||
while digit_end < len(record) and record[digit_end].isdigit():
|
||||
digit_end += 1
|
||||
if digit_end > offset + 1:
|
||||
yield int(record[offset + 1:digit_end])
|
||||
offset = digit_end
|
||||
continue
|
||||
offset += 1
|
||||
|
||||
|
||||
def dependency_closure(records, roots):
|
||||
pending = list(roots)
|
||||
selected = set()
|
||||
while pending:
|
||||
entity_id = pending.pop()
|
||||
if entity_id in selected:
|
||||
continue
|
||||
record = records.get(entity_id)
|
||||
if record is None:
|
||||
raise ValueError("STEP entity #{} is not present".format(entity_id))
|
||||
selected.add(entity_id)
|
||||
for referenced_id in references(record):
|
||||
if referenced_id != entity_id and referenced_id not in selected:
|
||||
pending.append(referenced_id)
|
||||
return selected
|
||||
|
||||
|
||||
def record_type(record):
|
||||
match = ENTITY_TYPE.match(record)
|
||||
return match.group(1).upper() if match else ""
|
||||
|
||||
|
||||
def adjacent_faces(records, faces):
|
||||
"""Return ADVANCED_FACE entities sharing an EDGE_CURVE with faces."""
|
||||
reverse = {}
|
||||
for entity_id, record in records.items():
|
||||
for referenced_id in references(record):
|
||||
reverse.setdefault(referenced_id, set()).add(entity_id)
|
||||
|
||||
edge_curves = {
|
||||
entity_id
|
||||
for entity_id in dependency_closure(records, faces)
|
||||
if record_type(records[entity_id]) == "EDGE_CURVE"
|
||||
}
|
||||
oriented_edges = {
|
||||
entity_id
|
||||
for edge_id in edge_curves
|
||||
for entity_id in reverse.get(edge_id, ())
|
||||
if record_type(records[entity_id]) == "ORIENTED_EDGE"
|
||||
}
|
||||
edge_loops = {
|
||||
entity_id
|
||||
for oriented_id in oriented_edges
|
||||
for entity_id in reverse.get(oriented_id, ())
|
||||
if record_type(records[entity_id]) == "EDGE_LOOP"
|
||||
}
|
||||
face_bounds = {
|
||||
entity_id
|
||||
for loop_id in edge_loops
|
||||
for entity_id in reverse.get(loop_id, ())
|
||||
if record_type(records[entity_id]) in {
|
||||
"FACE_BOUND", "FACE_OUTER_BOUND"
|
||||
}
|
||||
}
|
||||
return {
|
||||
entity_id
|
||||
for bound_id in face_bounds
|
||||
for entity_id in reverse.get(bound_id, ())
|
||||
if record_type(records[entity_id]) == "ADVANCED_FACE"
|
||||
}
|
||||
|
||||
|
||||
def quoted(value):
|
||||
return value.replace("'", "''")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("input", type=Path)
|
||||
parser.add_argument("output", type=Path)
|
||||
parser.add_argument(
|
||||
"--face",
|
||||
type=int,
|
||||
action="append",
|
||||
default=[],
|
||||
help="ADVANCED_FACE entity ID to retain (repeatable)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--face-range",
|
||||
action="append",
|
||||
default=[],
|
||||
metavar="FIRST:LAST",
|
||||
help="inclusive contiguous ADVANCED_FACE ID range to retain (repeatable)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--face-neighbor-depth",
|
||||
type=int,
|
||||
default=0,
|
||||
metavar="N",
|
||||
help=(
|
||||
"also retain ADVANCED_FACE neighbors reachable through N layers "
|
||||
"of shared EDGE_CURVEs"
|
||||
),
|
||||
)
|
||||
parser.add_argument("--name", default="reduced_step_regression")
|
||||
parser.add_argument(
|
||||
"--context",
|
||||
type=int,
|
||||
help=(
|
||||
"source GEOMETRIC_REPRESENTATION_CONTEXT entity ID to preserve; "
|
||||
"recommended when source angle or length units are not SI defaults"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--uncertainty",
|
||||
type=float,
|
||||
default=1.0e-6,
|
||||
help="fixture uncertainty in millimetres (default: 1e-6)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--closed-shell",
|
||||
action="store_true",
|
||||
help=(
|
||||
"wrap the face subset as CLOSED_SHELL/MANIFOLD_SOLID_BREP "
|
||||
"instead of OPEN_SHELL/SHELL_BASED_SURFACE_MODEL"
|
||||
),
|
||||
)
|
||||
args = parser.parse_args()
|
||||
faces = list(args.face)
|
||||
for face_range in args.face_range:
|
||||
fields = face_range.split(":", 1)
|
||||
if len(fields) != 2:
|
||||
parser.error("--face-range must be FIRST:LAST")
|
||||
try:
|
||||
first, last = (int(field) for field in fields)
|
||||
except ValueError:
|
||||
parser.error("--face-range must contain integer entity IDs")
|
||||
if first > last:
|
||||
parser.error("--face-range FIRST must not exceed LAST")
|
||||
faces.extend(range(first, last + 1))
|
||||
faces = sorted(set(faces))
|
||||
if not faces:
|
||||
parser.error("at least one --face or --face-range is required")
|
||||
if args.face_neighbor_depth < 0:
|
||||
parser.error("--face-neighbor-depth must be nonnegative")
|
||||
|
||||
source = args.input.read_text(encoding="latin-1")
|
||||
records = instances(source)
|
||||
for entity_id in faces:
|
||||
record = records.get(entity_id)
|
||||
if record is None:
|
||||
parser.error("STEP entity #{} is not present".format(entity_id))
|
||||
if not ADVANCED_FACE.match(record):
|
||||
parser.error(
|
||||
"STEP entity #{} is not an ADVANCED_FACE".format(entity_id)
|
||||
)
|
||||
expanded_faces = set(faces)
|
||||
frontier = set(faces)
|
||||
for _ in range(args.face_neighbor_depth):
|
||||
neighbors = adjacent_faces(records, frontier) - expanded_faces
|
||||
if not neighbors:
|
||||
break
|
||||
expanded_faces.update(neighbors)
|
||||
frontier = neighbors
|
||||
faces = sorted(expanded_faces)
|
||||
closure_roots = list(faces)
|
||||
if args.context is not None:
|
||||
closure_roots.append(args.context)
|
||||
selected = dependency_closure(records, closure_roots)
|
||||
schema_match = FILE_SCHEMA.search(source)
|
||||
schema = (
|
||||
schema_match.group(1)
|
||||
if schema_match
|
||||
else "AUTOMOTIVE_DESIGN { 1 0 10303 214 3 1 1 1 }"
|
||||
)
|
||||
|
||||
next_id = max(records) + 1
|
||||
wrapper_ids = list(range(next_id, next_id + 18))
|
||||
(
|
||||
shell_id,
|
||||
model_id,
|
||||
length_id,
|
||||
angle_id,
|
||||
solid_angle_id,
|
||||
uncertainty_id,
|
||||
context_id,
|
||||
application_id,
|
||||
protocol_id,
|
||||
product_context_id,
|
||||
definition_context_id,
|
||||
product_id,
|
||||
formation_id,
|
||||
definition_id,
|
||||
definition_shape_id,
|
||||
representation_id,
|
||||
shape_definition_id,
|
||||
_unused_id,
|
||||
) = wrapper_ids
|
||||
|
||||
safe_name = quoted(args.name)
|
||||
face_refs = ",".join("#{}".format(entity_id) for entity_id in faces)
|
||||
if args.closed_shell:
|
||||
wrapper = [
|
||||
"#{}=CLOSED_SHELL('{} shell',({}));".format(
|
||||
shell_id, safe_name, face_refs
|
||||
),
|
||||
"#{}=MANIFOLD_SOLID_BREP('{} model',#{});".format(
|
||||
model_id, safe_name, shell_id
|
||||
),
|
||||
]
|
||||
else:
|
||||
wrapper = [
|
||||
"#{}=OPEN_SHELL('{} shell',({}));".format(
|
||||
shell_id, safe_name, face_refs
|
||||
),
|
||||
"#{}=SHELL_BASED_SURFACE_MODEL('{} model',(#{}));".format(
|
||||
model_id, safe_name, shell_id
|
||||
),
|
||||
]
|
||||
if args.context is None:
|
||||
wrapper.extend(
|
||||
[
|
||||
"#{}=(LENGTH_UNIT() NAMED_UNIT(*) "
|
||||
"SI_UNIT(.MILLI.,.METRE.));".format(length_id),
|
||||
"#{}=(NAMED_UNIT(*) PLANE_ANGLE_UNIT() "
|
||||
"SI_UNIT($,.RADIAN.));".format(angle_id),
|
||||
"#{}=(NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) "
|
||||
"SOLID_ANGLE_UNIT());".format(solid_angle_id),
|
||||
"#{}=UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE({:.17g}),"
|
||||
"#{},'distance_accuracy_value','maximum gap value');".format(
|
||||
uncertainty_id, args.uncertainty, length_id
|
||||
),
|
||||
"#{}=(GEOMETRIC_REPRESENTATION_CONTEXT(3) "
|
||||
"GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#{})) "
|
||||
"GLOBAL_UNIT_ASSIGNED_CONTEXT((#{},#{},#{})) "
|
||||
"REPRESENTATION_CONTEXT('fixture','3D'));".format(
|
||||
context_id,
|
||||
uncertainty_id,
|
||||
length_id,
|
||||
angle_id,
|
||||
solid_angle_id,
|
||||
),
|
||||
]
|
||||
)
|
||||
representation_context_id = (
|
||||
args.context if args.context is not None else context_id
|
||||
)
|
||||
wrapper.extend(
|
||||
[
|
||||
"#{}=APPLICATION_CONTEXT('automotive design');".format(application_id),
|
||||
"#{}=APPLICATION_PROTOCOL_DEFINITION("
|
||||
"'fixture','automotive_design',2010,#{});".format(
|
||||
protocol_id, application_id
|
||||
),
|
||||
"#{}=PRODUCT_CONTEXT('',#{},'mechanical');".format(
|
||||
product_context_id, application_id
|
||||
),
|
||||
"#{}=PRODUCT_DEFINITION_CONTEXT('part definition',#{},'design');".format(
|
||||
definition_context_id, application_id
|
||||
),
|
||||
"#{}=PRODUCT('{}','{}','reduced corpus regression fixture',(#{}));".format(
|
||||
product_id, safe_name, safe_name, product_context_id
|
||||
),
|
||||
"#{}=PRODUCT_DEFINITION_FORMATION_WITH_SPECIFIED_SOURCE("
|
||||
"'1','',#{},.MADE.);".format(formation_id, product_id),
|
||||
"#{}=PRODUCT_DEFINITION('design','',#{},#{});".format(
|
||||
definition_id, formation_id, definition_context_id
|
||||
),
|
||||
"#{}=PRODUCT_DEFINITION_SHAPE('','',#{});".format(
|
||||
definition_shape_id, definition_id
|
||||
),
|
||||
"#{}={}('',(#{}),#{});".format(
|
||||
representation_id,
|
||||
"ADVANCED_BREP_SHAPE_REPRESENTATION" if args.closed_shell else
|
||||
"MANIFOLD_SURFACE_SHAPE_REPRESENTATION",
|
||||
model_id,
|
||||
representation_context_id,
|
||||
),
|
||||
"#{}=SHAPE_DEFINITION_REPRESENTATION(#{},#{});".format(
|
||||
shape_definition_id, definition_shape_id, representation_id
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
lines = [
|
||||
"ISO-10303-21;",
|
||||
"HEADER;",
|
||||
"FILE_DESCRIPTION(('BRL-CAD reduced corpus regression fixture'),'2;1');",
|
||||
"FILE_NAME('{}','',('BRL-CAD'),('BRL-CAD'),'','','');".format(safe_name),
|
||||
"FILE_SCHEMA(('{}'));".format(quoted(schema)),
|
||||
"ENDSEC;",
|
||||
"DATA;",
|
||||
]
|
||||
lines.extend(records[entity_id] for entity_id in sorted(selected))
|
||||
lines.extend(wrapper)
|
||||
lines.extend(("ENDSEC;", "END-ISO-10303-21;", ""))
|
||||
args.output.write_text("\n".join(lines), encoding="latin-1")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,178 +0,0 @@
|
||||
{
|
||||
"format": "brlcad-step-corpus-expectations-v1",
|
||||
"description": "Checksum-pinned external samples used for independent producer qualification without copying their exchange files into BRL-CAD.",
|
||||
"redistribution": "The linked STEP Tools pages state All Rights Reserved, while the STEPcode collective work is distributed under its 3-clause BSD COPYING terms. Obtain each sample from its stated source; this manifest does not independently grant redistribution rights.",
|
||||
"files": [
|
||||
{
|
||||
"name": "cubsomcy.stp",
|
||||
"aliases": ["ap203_cubsomcy.stp"],
|
||||
"sha256": "eb8fb40f56523fb154b435066f7b2719a88e15e30d0b8d8e6d01dd99339ddcf1",
|
||||
"source_page": "https://www.steptools.com/docs/stpfiles/ap203/index.html",
|
||||
"producer": "CATIA / Alcatel Space Industries",
|
||||
"expected": {
|
||||
"equals": {
|
||||
"exit_status": 0,
|
||||
"coverage.products": 1,
|
||||
"coverage.occurrences": 0,
|
||||
"coverage.geometry_attempted": 2,
|
||||
"coverage.geometry_written": 2,
|
||||
"coverage.geometry_skipped": 0,
|
||||
"coverage.outcome": "complete",
|
||||
"coverage.entity_counts.ADVANCED_BREP_SHAPE_REPRESENTATION": 1,
|
||||
"coverage.entity_counts.FACETED_BREP_SHAPE_REPRESENTATION": 1,
|
||||
"derived.occurrence_max_depth": 0,
|
||||
"derived.skipped_entity_ids": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "moon_buggy_asm.stp",
|
||||
"aliases": ["ap203_moon_buggy_asm.stp"],
|
||||
"sha256": "2afaaf50cd7ec15e36367be0a1e41730e7af5440ed974b62a0a21cf6346289dd",
|
||||
"source_page": "https://www.steptools.com/docs/stpfiles/ap203/index.html",
|
||||
"producer": "STEPNet / EDS Unigraphics 10.5",
|
||||
"expected": {
|
||||
"equals": {
|
||||
"exit_status": 1,
|
||||
"coverage.products": 20,
|
||||
"coverage.occurrences": 23,
|
||||
"coverage.geometry_attempted": 12,
|
||||
"coverage.geometry_written": 9,
|
||||
"coverage.geometry_skipped": 3,
|
||||
"coverage.outcome": "partial",
|
||||
"coverage.entity_counts.MAPPED_ITEM": 23,
|
||||
"derived.assembly_usage_count": 23,
|
||||
"derived.occurrence_detail_count": 23,
|
||||
"derived.occurrence_max_depth": 4,
|
||||
"derived.occurrence_methods": ["mapped"],
|
||||
"derived.skipped_entity_ids": [32060, 32220, 32820]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "123Block_Color.stp",
|
||||
"aliases": ["ap203e2_123Block_Color.stp"],
|
||||
"sha256": "4bbdd752fd3c4e7ebfee633da210b9e3df62c583a604e874ca5e9c36545d2be4",
|
||||
"source_page": "https://www.steptools.com/docs/stpfiles/ap203e2/index.html",
|
||||
"producer": "CATIA / Boeing",
|
||||
"expected": {
|
||||
"equals": {
|
||||
"exit_status": 0,
|
||||
"coverage.products": 1,
|
||||
"coverage.geometry_attempted": 1,
|
||||
"coverage.geometry_written": 1,
|
||||
"coverage.geometry_skipped": 0,
|
||||
"coverage.outcome": "complete",
|
||||
"coverage.styles_extracted": 1,
|
||||
"coverage.styles_applied": 1,
|
||||
"derived.skipped_entity_ids": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "boxy_with_linearsize.stp",
|
||||
"aliases": ["ap203e2_boxy_with_linearsize.stp"],
|
||||
"sha256": "e120b982c5bb5c8b62c99bb03d5ae46c16d9200fcb53be6521c201be98ead6d6",
|
||||
"source_page": "https://www.steptools.com/docs/stpfiles/ap214/index.html",
|
||||
"producer": "STEP-NC Machine / STEP Tools, Inc.",
|
||||
"expected": {
|
||||
"equals": {
|
||||
"exit_status": 3,
|
||||
"coverage.products": 1,
|
||||
"coverage.geometry_attempted": 1,
|
||||
"coverage.geometry_written": 0,
|
||||
"coverage.geometry_skipped": 1,
|
||||
"coverage.outcome": "failed",
|
||||
"coverage.entity_counts.DATUM": 3,
|
||||
"coverage.entity_counts.PLUS_MINUS_TOLERANCE": 1,
|
||||
"derived.skipped_entity_ids": [26]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "as1-ac-214.stp",
|
||||
"aliases": ["ap214_as1-ac-214.stp"],
|
||||
"sha256": "1bb1a0e55dc4a0169e329529eb520596690f8745077c1bdd2f9fa41fa5a2aa95",
|
||||
"source_page": "https://www.steptools.com/docs/stpfiles/ap214/index.html",
|
||||
"producer": "AutoCAD / STEPnet",
|
||||
"expected": {
|
||||
"equals": {
|
||||
"exit_status": 0,
|
||||
"coverage.products": 9,
|
||||
"coverage.occurrences": 13,
|
||||
"coverage.geometry_attempted": 5,
|
||||
"coverage.geometry_written": 5,
|
||||
"coverage.geometry_skipped": 0,
|
||||
"coverage.outcome": "complete",
|
||||
"coverage.styles_extracted": 5,
|
||||
"coverage.styles_applied": 5,
|
||||
"coverage.layers_extracted": 1,
|
||||
"derived.assembly_usage_count": 13,
|
||||
"derived.occurrence_detail_count": 13,
|
||||
"derived.occurrence_max_depth": 3,
|
||||
"derived.occurrence_methods": ["referenced"],
|
||||
"derived.skipped_entity_ids": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "as1-ug-214.stp",
|
||||
"aliases": ["ap214_as1-ug-214.stp"],
|
||||
"sha256": "53c5069f5d6b59435eb32ac743cc82e53bd3607954c94785d989d579d0e29426",
|
||||
"source_page": "https://www.steptools.com/docs/stpfiles/ap214/index.html",
|
||||
"producer": "Unigraphics / STEPnet",
|
||||
"expected": {
|
||||
"equals": {
|
||||
"exit_status": 0,
|
||||
"coverage.products": 9,
|
||||
"coverage.occurrences": 13,
|
||||
"coverage.geometry_attempted": 5,
|
||||
"coverage.geometry_written": 5,
|
||||
"coverage.geometry_skipped": 0,
|
||||
"coverage.outcome": "complete",
|
||||
"coverage.styles_extracted": 5,
|
||||
"coverage.styles_applied": 5,
|
||||
"coverage.layers_extracted": 3,
|
||||
"coverage.properties_extracted": 42,
|
||||
"derived.assembly_usage_count": 13,
|
||||
"derived.occurrence_detail_count": 13,
|
||||
"derived.occurrence_max_depth": 3,
|
||||
"derived.occurrence_methods": ["referenced"],
|
||||
"derived.validation_property_count": 42,
|
||||
"derived.skipped_entity_ids": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "dm1-id-214.stp",
|
||||
"sha256": "25e727cdc4d738064da7907d604bd4ceeb4e941c6fdc201f3df04c0b94671661",
|
||||
"source_page": "STEPcode data/ap214e3/dm1-id-214.stp (3-clause BSD collective work)",
|
||||
"producer": "I-DEAS Master Series 9 / SDRC",
|
||||
"expected": {
|
||||
"equals": {
|
||||
"exit_status": 0,
|
||||
"coverage.products": 7,
|
||||
"coverage.occurrences": 7,
|
||||
"coverage.geometry_attempted": 3,
|
||||
"coverage.geometry_written": 3,
|
||||
"coverage.geometry_skipped": 0,
|
||||
"coverage.outcome": "complete",
|
||||
"coverage.styles_extracted": 3,
|
||||
"coverage.styles_applied": 3,
|
||||
"coverage.materials_extracted": 3,
|
||||
"coverage.properties_extracted": 22,
|
||||
"coverage.entity_counts.MAKE_FROM_USAGE_OPTION": 3,
|
||||
"derived.assembly_usage_count": 7,
|
||||
"derived.occurrence_detail_count": 7,
|
||||
"derived.occurrence_max_depth": 1,
|
||||
"derived.occurrence_methods": ["referenced"],
|
||||
"derived.material_count": 3,
|
||||
"derived.material_identifiers": ["AMS 4928", "AMS 5613", "AMS 5662"],
|
||||
"derived.material_names": ["Greek Ascoloy", "Inconel 718", "Titanium 6-4"],
|
||||
"derived.validation_property_count": 19,
|
||||
"derived.skipped_entity_ids": []
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
{
|
||||
"format": "brlcad-step-corpus-expectations-v1",
|
||||
"description": "Checksum-pinned independent-producer samples from locally retained, non-redistributed corpora whose original download pages were not recorded.",
|
||||
"redistribution": "These records provide reproducible local qualification evidence only. They do not grant permission to copy the exchange files into BRL-CAD or redistribute them.",
|
||||
"files": [
|
||||
{
|
||||
"name": "Leopard2-A6.step",
|
||||
"sha256": "ef8f21f6454f7d952b619ed1e41dab824f31f393e81a3e138392c53605a0ab94",
|
||||
"source_page": "Local non-redistributed GrabCAD corpus; original download page not recorded",
|
||||
"producer": "Autodesk Inventor 2018 / ST-Developer v17",
|
||||
"expected": {
|
||||
"equals": {
|
||||
"exit_status": 1,
|
||||
"coverage.products": 14,
|
||||
"coverage.occurrences": 338,
|
||||
"coverage.geometry_attempted": 31,
|
||||
"coverage.geometry_written": 31,
|
||||
"coverage.geometry_skipped": 0,
|
||||
"coverage.outcome": "partial",
|
||||
"coverage.styles_extracted": 2425,
|
||||
"coverage.styles_applied": 31,
|
||||
"coverage.materials_extracted": 13,
|
||||
"coverage.properties_extracted": 26,
|
||||
"coverage.properties_invalid": 13,
|
||||
"validation.invalid_breps": 1,
|
||||
"validation.invalid_breps_written": 1,
|
||||
"validation.invalid_breps_rejected": 0,
|
||||
"derived.assembly_usage_count": 338,
|
||||
"derived.occurrence_detail_count": 338,
|
||||
"derived.occurrence_max_depth": 1,
|
||||
"derived.occurrence_methods": ["referenced"],
|
||||
"derived.product_metadata_count": 13,
|
||||
"derived.material_count": 13,
|
||||
"derived.material_identifiers": [""],
|
||||
"derived.material_names": ["Generic", "SAE 1020"],
|
||||
"derived.material_property_count": 26,
|
||||
"derived.material_property_names": ["Generic", "SAE 1020", "density measure"],
|
||||
"derived.material_property_units": ["gram^1*centimetre^3"],
|
||||
"derived.material_property_numeric_values": [1, 7.84],
|
||||
"derived.validation_property_count": 0,
|
||||
"derived.skipped_entity_ids": []
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -117,6 +117,20 @@ if(TARGET libged)
|
||||
|
||||
endif(TARGET libged)
|
||||
|
||||
if(TARGET librt AND FUZZER_SUPPORTED)
|
||||
set(RT_EDIT_FUZZ_DIR "${CMAKE_SOURCE_DIR}/src/librt/tests/edit")
|
||||
brlcad_addexec(
|
||||
fuzz_rt_edit_test
|
||||
"${RT_EDIT_FUZZ_DIR}/fuzz.cpp;${RT_EDIT_FUZZ_DIR}/ars.cpp;${RT_EDIT_FUZZ_DIR}/bot.cpp"
|
||||
"libbu;librt;libnmg;libwdb;libbrep"
|
||||
TEST
|
||||
)
|
||||
set_target_properties(fuzz_rt_edit_test PROPERTIES UNITY_BUILD OFF)
|
||||
target_compile_options(fuzz_rt_edit_test PRIVATE ${FUZZER_EXE_COMPILE_OPTIONS})
|
||||
target_link_options(fuzz_rt_edit_test PRIVATE ${FUZZER_LINK_OPTIONS})
|
||||
BRLCAD_ADD_TEST(NAME fuzz_rt_edit_smoke COMMAND fuzz_rt_edit_test -runs=1 -max_total_time=5)
|
||||
endif()
|
||||
|
||||
cmakefiles(
|
||||
CMakeLists.txt
|
||||
fuzz_ged.cpp
|
||||
|
||||
@@ -68,6 +68,13 @@
|
||||
* ("/tc6_reg.c/sub6.s#sub"), so the lookup missed and no variant was
|
||||
* applied, leaving the phantom face. After the fix the plan is built
|
||||
* from region roots so keys match and the phantom face is eliminated.
|
||||
*
|
||||
* TC7 - NMG Boolean isolation and output modes: verifies that an interrupted
|
||||
* worker cannot commit output, then exercises BoT, NMG, region, and
|
||||
* in-place output paths.
|
||||
*
|
||||
* TC8 - implicit solid roots survive the working-database close and remain
|
||||
* valid members of the imported output hierarchy.
|
||||
*/
|
||||
|
||||
#include "common.h"
|
||||
@@ -78,7 +85,9 @@
|
||||
#include <string>
|
||||
|
||||
#include "bu/app.h"
|
||||
#include "bu/env.h"
|
||||
#include "bu/file.h"
|
||||
#include "bu/log.h"
|
||||
#include "bu/malloc.h"
|
||||
#include "bu/str.h"
|
||||
#include "bu/vls.h"
|
||||
@@ -142,6 +151,36 @@ write_comb2(struct rt_wdb *wdbp,
|
||||
0, 0, 0, 0, 0, 0, 0);
|
||||
}
|
||||
|
||||
static int
|
||||
write_comb1(struct rt_wdb *wdbp, const char *comb_name, const char *member, int is_region)
|
||||
{
|
||||
struct bu_list head;
|
||||
BU_LIST_INIT(&head);
|
||||
mk_addmember(member, &head, NULL, WMOP_UNION);
|
||||
return mk_comb(wdbp, comb_name, &head, is_region, NULL, NULL, NULL,
|
||||
0, 0, 0, 0, 0, 0, 0);
|
||||
}
|
||||
|
||||
static int
|
||||
run_facetize_command(const char *gfile, int argc, const char **argv, int verbose)
|
||||
{
|
||||
struct ged *gedp = ged_open("db", gfile, 1);
|
||||
if (!gedp) {
|
||||
bu_log("[regress_facetize] ged_open(%s) failed\n", gfile);
|
||||
return BRLCAD_ERROR;
|
||||
}
|
||||
|
||||
int ret = ged_exec(gedp, argc, argv);
|
||||
if (verbose || ret != BRLCAD_OK) {
|
||||
const char *log = bu_vls_cstr(gedp->ged_result_str);
|
||||
if (log && log[0])
|
||||
bu_log("[facetize] %s\n", log);
|
||||
}
|
||||
|
||||
ged_close(gedp);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open @a gfile with ged_open and run "facetize @a input @a output".
|
||||
* Returns BRLCAD_OK on success, BRLCAD_ERROR otherwise.
|
||||
@@ -149,23 +188,8 @@ write_comb2(struct rt_wdb *wdbp,
|
||||
static int
|
||||
run_facetize(const char *gfile, const char *input, const char *output, int verbose)
|
||||
{
|
||||
struct ged *gedp = ged_open("db", gfile, 1);
|
||||
if (!gedp) {
|
||||
bu_log("[regress_facetize] ged_open(%s) failed\n", gfile);
|
||||
return BRLCAD_ERROR;
|
||||
}
|
||||
|
||||
const char *av[4] = {"facetize", input, output, NULL};
|
||||
int ret = ged_exec(gedp, 3, av);
|
||||
|
||||
if (verbose || ret != BRLCAD_OK) {
|
||||
const char *log = bu_vls_cstr(gedp->ged_result_str);
|
||||
if (log && log[0])
|
||||
bu_log("[facetize] %s\n", log);
|
||||
}
|
||||
|
||||
ged_close(gedp);
|
||||
return ret;
|
||||
return run_facetize_command(gfile, 3, av, verbose);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -212,6 +236,46 @@ bu_log("[regress_facetize] check_bot: '%s' type %d is not a BoT\n",
|
||||
return ok;
|
||||
}
|
||||
|
||||
static int
|
||||
check_object_type(const char *gfile, const char *object_name, int expected_type)
|
||||
{
|
||||
struct db_i *dbip = db_open(gfile, DB_OPEN_READONLY);
|
||||
if (!dbip || db_dirbuild(dbip) < 0) {
|
||||
if (dbip)
|
||||
db_close(dbip);
|
||||
return BRLCAD_ERROR;
|
||||
}
|
||||
|
||||
struct directory *dp = db_lookup(dbip, object_name, LOOKUP_QUIET);
|
||||
if (!dp) {
|
||||
bu_log("[regress_facetize] '%s' does not exist\n", object_name);
|
||||
db_close(dbip);
|
||||
return BRLCAD_ERROR;
|
||||
}
|
||||
|
||||
struct rt_db_internal intern;
|
||||
RT_DB_INTERNAL_INIT(&intern);
|
||||
if (rt_db_get_internal(&intern, dp, dbip, NULL) < 0) {
|
||||
db_close(dbip);
|
||||
return BRLCAD_ERROR;
|
||||
}
|
||||
int ret = (intern.idb_minor_type == expected_type) ? BRLCAD_OK : BRLCAD_ERROR;
|
||||
if (ret != BRLCAD_OK)
|
||||
bu_log("[regress_facetize] '%s' type %d, expected %d\n",
|
||||
object_name, intern.idb_minor_type, expected_type);
|
||||
rt_db_free_internal(&intern);
|
||||
db_close(dbip);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int
|
||||
count_log_hook(void *data, void *UNUSED(message))
|
||||
{
|
||||
int *count = (int *)data;
|
||||
(*count)++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run rt_crofton_shoot on @a obj_name in @a gfile.
|
||||
* Stores SA (mm2) and VOL (mm3) in *sa / *vol.
|
||||
@@ -1120,6 +1184,183 @@ tc6_havoc_wind9_pattern(const char *tmpdir, int verbose)
|
||||
return BRLCAD_OK;
|
||||
}
|
||||
|
||||
static int
|
||||
tc7_nmg_modes(const char *tmpdir, int verbose)
|
||||
{
|
||||
bu_log("[regress_facetize] TC7: NMG Boolean and output modes...\n");
|
||||
|
||||
struct bu_vls gpath = BU_VLS_INIT_ZERO;
|
||||
bu_vls_printf(&gpath, "%s/tc7_nmg.g", tmpdir);
|
||||
const char *gfile = bu_vls_cstr(&gpath);
|
||||
if (bu_file_exists(gfile, NULL))
|
||||
bu_file_delete(gfile);
|
||||
|
||||
struct db_i *dbip = db_create(gfile, 5);
|
||||
if (!dbip) {
|
||||
bu_vls_free(&gpath);
|
||||
return BRLCAD_ERROR;
|
||||
}
|
||||
struct rt_wdb *wdbp = wdb_dbopen(dbip, RT_WDB_TYPE_DB_DEFAULT);
|
||||
point_t box_pts[8] = {
|
||||
{0.0, 0.0, 0.0}, {10.0, 0.0, 0.0},
|
||||
{10.0, 10.0, 0.0}, {0.0, 10.0, 0.0},
|
||||
{0.0, 0.0, 10.0}, {10.0, 0.0, 10.0},
|
||||
{10.0, 10.0, 10.0}, {0.0, 10.0, 10.0}
|
||||
};
|
||||
if (write_arb8(wdbp, "box.s", box_pts) < 0 ||
|
||||
write_comb1(wdbp, "box.r", "box.s", 1) < 0 ||
|
||||
write_comb1(wdbp, "box.c", "box.s", 0) < 0) {
|
||||
wdb_close(wdbp);
|
||||
bu_vls_free(&gpath);
|
||||
return BRLCAD_ERROR;
|
||||
}
|
||||
wdb_close(wdbp);
|
||||
|
||||
const char *bot_av[] = {"facetize", "--nmg-booleval", "box.r", "box.bot", NULL};
|
||||
const char *fault_av[] = {"facetize", "--nmg-booleval", "box.r", "fault.bot", NULL};
|
||||
const char *nmg_av[] = {"facetize", "-n", "box.r", "box.nmg", NULL};
|
||||
const char *regions_av[] = {"facetize", "-r", "--nmg-booleval", "box.r", "box.regions", NULL};
|
||||
const char *fallback_av[] = {"facetize", "-r", "--nmg-booleval", "box.c", "box-fallback.bot", NULL};
|
||||
const char *object_in_place_av[] = {"facetize", "--in-place", "--nmg-booleval", "box.c", NULL};
|
||||
const char *in_place_av[] = {"facetize", "-r", "--in-place", "--nmg-booleval", "box.r", NULL};
|
||||
|
||||
int ret = BRLCAD_OK;
|
||||
auto check_tc7 = [&ret](bool passed, const char *description) {
|
||||
if (passed)
|
||||
return;
|
||||
bu_log("[regress_facetize] TC7: FAIL - %s\n", description);
|
||||
ret = BRLCAD_ERROR;
|
||||
};
|
||||
|
||||
struct bu_vls fault_path = BU_VLS_INIT_ZERO;
|
||||
bu_vls_printf(&fault_path, "%s/tc7_nmg_worker.fault", tmpdir);
|
||||
(void)bu_file_delete(bu_vls_cstr(&fault_path));
|
||||
(void)bu_setenv("LIBGED_FACETIZE_TEST_FAULT",
|
||||
"nmg-before-ready:fault.bot", 1);
|
||||
(void)bu_setenv("LIBGED_FACETIZE_TEST_FAULT_FILE",
|
||||
bu_vls_cstr(&fault_path), 1);
|
||||
int fault_ret = run_facetize_command(gfile, 4, fault_av, verbose);
|
||||
(void)bu_setenv("LIBGED_FACETIZE_TEST_FAULT", "", 1);
|
||||
(void)bu_setenv("LIBGED_FACETIZE_TEST_FAULT_FILE", "", 1);
|
||||
check_tc7(fault_ret == BRLCAD_ERROR,
|
||||
"an interrupted NMG Boolean worker did not report failure");
|
||||
check_tc7(bu_file_exists(bu_vls_cstr(&fault_path), NULL),
|
||||
"NMG Boolean worker fault did not trigger");
|
||||
check_tc7(check_object_type(gfile, "fault.bot", ID_BOT) != BRLCAD_OK,
|
||||
"an interrupted NMG Boolean worker committed an output");
|
||||
(void)bu_file_delete(bu_vls_cstr(&fault_path));
|
||||
bu_vls_free(&fault_path);
|
||||
|
||||
int hook_calls = 0;
|
||||
bu_log_add_hook(count_log_hook, &hook_calls);
|
||||
int bot_ret = run_facetize_command(gfile, 4, bot_av, verbose);
|
||||
int hook_calls_before_probe = hook_calls;
|
||||
bu_log("facetize NMG hook restoration probe\n");
|
||||
bu_log_delete_hook(count_log_hook, &hook_calls);
|
||||
|
||||
check_tc7(bot_ret == BRLCAD_OK, "NMG Boolean BoT command failed");
|
||||
check_tc7(hook_calls == hook_calls_before_probe + 1,
|
||||
"temporary log hook was not restored");
|
||||
check_tc7(check_bot_exists(gfile, "box.bot") == BRLCAD_OK,
|
||||
"NMG Boolean BoT output is missing");
|
||||
|
||||
int nmg_ret = run_facetize_command(gfile, 4, nmg_av, verbose);
|
||||
check_tc7(nmg_ret == BRLCAD_OK, "NMG output command failed");
|
||||
check_tc7(check_object_type(gfile, "box.nmg", ID_NMG) == BRLCAD_OK,
|
||||
"NMG output has the wrong type");
|
||||
|
||||
int regions_ret = run_facetize_command(gfile, 5, regions_av, verbose);
|
||||
check_tc7(regions_ret == BRLCAD_OK, "NMG Boolean region command failed");
|
||||
check_tc7(check_object_type(gfile, "box.regions", ID_COMBINATION) == BRLCAD_OK,
|
||||
"NMG Boolean region output is missing");
|
||||
|
||||
int fallback_ret = run_facetize_command(gfile, 5, fallback_av, verbose);
|
||||
check_tc7(fallback_ret == BRLCAD_OK,
|
||||
"region mode object fallback command failed");
|
||||
check_tc7(check_bot_exists(gfile, "box-fallback.bot") == BRLCAD_OK,
|
||||
"region mode object fallback output is missing");
|
||||
|
||||
int object_in_place_ret = run_facetize_command(gfile, 4,
|
||||
object_in_place_av, verbose);
|
||||
check_tc7(object_in_place_ret == BRLCAD_OK,
|
||||
"in-place NMG Boolean object command failed");
|
||||
check_tc7(check_object_type(gfile, "box.c", ID_BOT) == BRLCAD_OK,
|
||||
"in-place NMG Boolean object has the wrong type");
|
||||
|
||||
int in_place_ret = run_facetize_command(gfile, 5, in_place_av, verbose);
|
||||
check_tc7(in_place_ret == BRLCAD_OK,
|
||||
"in-place NMG Boolean region command failed");
|
||||
check_tc7(check_object_type(gfile, "box.r", ID_COMBINATION) == BRLCAD_OK,
|
||||
"in-place region root has the wrong type");
|
||||
check_tc7(check_bot_exists(gfile, "box.r.bot") == BRLCAD_OK,
|
||||
"in-place region BoT output is missing");
|
||||
|
||||
bu_log("[regress_facetize] TC7: %s\n", ret == BRLCAD_OK ? "PASS" : "FAIL");
|
||||
bu_vls_free(&gpath);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* TC8: implicit root replacement persists across database reopen */
|
||||
/* ------------------------------------------------------------------ */
|
||||
static int
|
||||
tc8_implicit_root_persistence(const char *tmpdir, int verbose)
|
||||
{
|
||||
bu_log("[regress_facetize] TC8: implicit root persistence...\n");
|
||||
|
||||
struct bu_vls gpath = BU_VLS_INIT_ZERO;
|
||||
bu_vls_printf(&gpath, "%s/tc8_implicit_root.g", tmpdir);
|
||||
const char *gfile = bu_vls_cstr(&gpath);
|
||||
if (bu_file_exists(gfile, NULL))
|
||||
bu_file_delete(gfile);
|
||||
|
||||
struct db_i *dbip = db_create(gfile, 5);
|
||||
if (!dbip) {
|
||||
bu_vls_free(&gpath);
|
||||
return BRLCAD_ERROR;
|
||||
}
|
||||
struct rt_wdb *wdbp = wdb_dbopen(dbip, RT_WDB_TYPE_DB_DEFAULT);
|
||||
point_t region_points[8] = {
|
||||
{0.0, 0.0, 0.0}, {10.0, 0.0, 0.0},
|
||||
{10.0, 10.0, 0.0}, {0.0, 10.0, 0.0},
|
||||
{0.0, 0.0, 10.0}, {10.0, 0.0, 10.0},
|
||||
{10.0, 10.0, 10.0}, {0.0, 10.0, 10.0}
|
||||
};
|
||||
point_t loose_points[8] = {
|
||||
{20.0, 0.0, 0.0}, {30.0, 0.0, 0.0},
|
||||
{30.0, 10.0, 0.0}, {20.0, 10.0, 0.0},
|
||||
{20.0, 0.0, 10.0}, {30.0, 0.0, 10.0},
|
||||
{30.0, 10.0, 10.0}, {20.0, 10.0, 10.0}
|
||||
};
|
||||
if (!wdbp || write_arb8(wdbp, "region.s", region_points) < 0 ||
|
||||
write_arb8(wdbp, "loose.s", loose_points) < 0 ||
|
||||
write_comb1(wdbp, "box.r", "region.s", 1) < 0 ||
|
||||
write_comb2(wdbp, "assembly.c", "box.r", 'u',
|
||||
"loose.s", 'u') < 0) {
|
||||
if (wdbp)
|
||||
wdb_close(wdbp);
|
||||
bu_vls_free(&gpath);
|
||||
return BRLCAD_ERROR;
|
||||
}
|
||||
wdb_close(wdbp);
|
||||
|
||||
const char *facetize_args[] = {
|
||||
"facetize", "-r", "assembly.c", "assembly.regions", NULL
|
||||
};
|
||||
int ret = run_facetize_command(gfile, 4, facetize_args, verbose);
|
||||
if (ret == BRLCAD_OK &&
|
||||
(check_object_type(gfile, "assembly.regions", ID_COMBINATION) != BRLCAD_OK ||
|
||||
check_object_type(gfile, "facetize_assembly.c", ID_COMBINATION) != BRLCAD_OK ||
|
||||
check_bot_exists(gfile, "box.r.bot") != BRLCAD_OK ||
|
||||
check_bot_exists(gfile, "facetize_loose.s") != BRLCAD_OK))
|
||||
ret = BRLCAD_ERROR;
|
||||
|
||||
bu_log("[regress_facetize] TC8: %s\n",
|
||||
ret == BRLCAD_OK ? "PASS" : "FAIL");
|
||||
bu_vls_free(&gpath);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* main */
|
||||
/* ------------------------------------------------------------------ */
|
||||
@@ -1151,6 +1392,8 @@ return 1;
|
||||
if (tc4_havoc_reuse(tmpdir, verbose) != BRLCAD_OK) ret = 1;
|
||||
if (tc5_near_coplanar_subTOL(tmpdir, verbose) != BRLCAD_OK) ret = 1;
|
||||
if (tc6_havoc_wind9_pattern(tmpdir, verbose) != BRLCAD_OK) ret = 1;
|
||||
if (tc7_nmg_modes(tmpdir, verbose) != BRLCAD_OK) ret = 1;
|
||||
if (tc8_implicit_root_persistence(tmpdir, verbose) != BRLCAD_OK) ret = 1;
|
||||
|
||||
if (ret == 0)
|
||||
bu_log("[regress_facetize] All tests PASSED\n");
|
||||
|
||||
@@ -23,6 +23,7 @@ set(
|
||||
gqa.mass_mix.am.out
|
||||
gqa.mass_mix.aw.out
|
||||
gqa.mass_mix.u0.out
|
||||
gqa.missing_density.out
|
||||
gqa.mged
|
||||
gqa.overlaps.plot3
|
||||
gqa.sphere_adaptive.out
|
||||
|
||||
@@ -67,6 +67,11 @@ run_capture ( ) {
|
||||
return $ret
|
||||
}
|
||||
|
||||
expect_failure ( ) {
|
||||
"$@"
|
||||
test $? -ne 0
|
||||
}
|
||||
|
||||
extract_last_number ( ) {
|
||||
label="$1"
|
||||
file="$2"
|
||||
@@ -101,6 +106,19 @@ assert_close ( ) {
|
||||
fi
|
||||
}
|
||||
|
||||
assert_contains ( ) {
|
||||
expected="$1"
|
||||
file="$2"
|
||||
desc="$3"
|
||||
|
||||
if grep -F -q "$expected" "$file" ; then
|
||||
log "PASS: $desc"
|
||||
else
|
||||
log "FAIL: $desc"
|
||||
STATUS="`expr $STATUS + 1`"
|
||||
fi
|
||||
}
|
||||
|
||||
MGED="`ensearch mged`"
|
||||
if test ! -f "$MGED" ; then
|
||||
log "Unable to find mged, aborting"
|
||||
@@ -177,6 +195,10 @@ adjust mass_air.r GIFTmater 2
|
||||
g mass_air_only.g mass_air.r
|
||||
g mass_air_mix.g mass_solid.r mass_air.r
|
||||
|
||||
in missing_density.s rpp 2 3 0 1 0 1
|
||||
r missing_density.r u missing_density.s
|
||||
adjust missing_density.r GIFTmater 123
|
||||
|
||||
in sphere.s sph 0 0 0 1
|
||||
r sphere.r u sphere.s
|
||||
adjust sphere.r GIFTmater 5
|
||||
@@ -243,6 +265,7 @@ run_capture gqa.mass_mix.aw.out $GQABIN -u m,m^3,kg -g 250mm-50mm -Aw gqa.g mass
|
||||
run_capture gqa.mass_mix.am.out $GQABIN -u m,m^3,kg -g 250mm-50mm -Am gqa.g mass_air_mix.g
|
||||
run_capture gqa.mass_mix.u0.out $GQABIN -u m,m^3,kg -g 250mm-50mm -U 0 -Aw gqa.g mass_air_mix.g
|
||||
run_capture gqa.sphere_adaptive.out $GQABIN -u m,m^3,kg -q -Av -g 0.05m gqa.g sphere.r
|
||||
run_capture gqa.missing_density.out expect_failure $GQABIN -u m,m^3,kg -g 250mm-50mm -f density_table.txt -Aw gqa.g missing_density.r
|
||||
|
||||
MASS_AIR_AW="`extract_last_number 'Average total weight:' gqa.mass_air.aw.out`"
|
||||
MASS_AIR_AM="`extract_last_number 'Average total weight:' gqa.mass_air.am.out`"
|
||||
@@ -257,6 +280,7 @@ assert_close 1001 "$MASS_MIX_AW" 0.001 "mixed solid+air modeled weight with -Aw"
|
||||
assert_close "$MASS_MIX_AW" "$MASS_MIX_AM" 0.001 "-Aw and -Am agree on mixed modeled air"
|
||||
assert_close 1000 "$MASS_MIX_U0" 0.001 "-U 0 excludes modeled air from weight"
|
||||
assert_close 4.18879 "$SPHERE_VOLUME" 0.01 "adaptive sphere volume refines beyond symmetric first pass"
|
||||
assert_contains "Density 123 on region /missing_density.r is not defined in the density table." gqa.missing_density.out "undefined density produces an error"
|
||||
|
||||
|
||||
if [ $STATUS = 0 ] ; then
|
||||
|
||||
@@ -68,6 +68,7 @@ set(
|
||||
sca_view.mged
|
||||
search_exec.mged
|
||||
sed.mged
|
||||
sed_reset.mged
|
||||
set_perspective.mged
|
||||
setview.mged
|
||||
size.mged
|
||||
@@ -100,6 +101,7 @@ if(TARGET mged)
|
||||
continue()
|
||||
endif(TNAME STREQUAL "")
|
||||
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/${testfile}")
|
||||
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/${testfile}" TEST_CONTENTS)
|
||||
if(TNAME STREQUAL "search_exec")
|
||||
string(CONFIGURE "${TEST_CONTENTS}" TEST_CONTENTS @ONLY)
|
||||
@@ -126,6 +128,12 @@ if(TARGET mged)
|
||||
elseif(TNAME STREQUAL "arb")
|
||||
set(TEST_EXPECT "'arb' testing completed")
|
||||
set(TEST_SOURCE "${CMAKE_CURRENT_BINARY_DIR}/regress_${testfile}")
|
||||
elseif(TNAME MATCHES "^(extrude|facedef|mirface|permute)$")
|
||||
set(TEST_EXPECT "'${TNAME}' testing completed")
|
||||
set(TEST_SOURCE "${CMAKE_CURRENT_BINARY_DIR}/regress_${testfile}")
|
||||
elseif(TNAME STREQUAL "edgedir")
|
||||
set(TEST_EXPECT "'edarb edgedir' testing completed")
|
||||
set(TEST_SOURCE "${CMAKE_CURRENT_BINARY_DIR}/regress_${testfile}")
|
||||
elseif(TNAME STREQUAL "killtree")
|
||||
set(TEST_EXPECT "KILL Solid: killtree_dup.s")
|
||||
set(TEST_REJECT "db_lookup(killtree_dup.s) failed")
|
||||
@@ -138,6 +146,10 @@ if(TARGET mged)
|
||||
elseif(TNAME STREQUAL "prim_edit")
|
||||
set(TEST_EXPECT "Primitive parameter editing testing completed")
|
||||
set(TEST_REJECT "FAIL:")
|
||||
elseif(TNAME STREQUAL "sed_reset")
|
||||
set(TEST_EXPECT "PASS: 'sed_reset' testing completed")
|
||||
set(TEST_REJECT "FAIL:")
|
||||
set(TEST_SOURCE "${CMAKE_CURRENT_BINARY_DIR}/regress_${testfile}")
|
||||
elseif(TNAME STREQUAL "overlap_plotting")
|
||||
set(TEST_DB "${CMAKE_SOURCE_DIR}/src/libged/tests/ged_tests.g")
|
||||
set(TEST_EXPECT "PASS overlap plotting")
|
||||
@@ -187,6 +199,7 @@ distclean(${mged_outfiles})
|
||||
|
||||
cmakefiles(
|
||||
CMakeLists.txt
|
||||
csg_edit.mged
|
||||
mged.tcl
|
||||
regression_resources.tcl
|
||||
search_exec_procs.tcl
|
||||
|
||||
@@ -119,4 +119,35 @@ if {![catch {get $invalid_inner}]} {
|
||||
}
|
||||
|
||||
|
||||
# ARBN uses one independent thickness per plane. Exercise unequal offsets so
|
||||
# the test also verifies the plane ordering and inward normal convention.
|
||||
set arbn_outer inside_arbn_outer.s
|
||||
set arbn_inner inside_arbn_inner.s
|
||||
set arbn_thicknesses {1 2 3 4 1.5 0.5}
|
||||
in $arbn_outer arbn 6 \
|
||||
1 0 0 10 -1 0 0 0 \
|
||||
0 1 0 10 0 -1 0 0 \
|
||||
0 0 1 10 0 0 -1 0
|
||||
if {[catch {inside $arbn_outer $arbn_inner {*}$arbn_thicknesses} result]} {
|
||||
error "inside failed for ARBN: $result"
|
||||
}
|
||||
set arbn_definition [get $arbn_inner]
|
||||
set expected_offsets {9 -2 7 -4 8.5 -0.5}
|
||||
for {set plane 0} {$plane < 6} {incr plane} {
|
||||
set equation [lindex $arbn_definition [expr {4 + 2 * $plane}]]
|
||||
set actual_offset [lindex $equation 3]
|
||||
if {abs($actual_offset - [lindex $expected_offsets $plane]) > $plane_tolerance} {
|
||||
error "ARBN inside plane $plane has offset $actual_offset: $arbn_definition"
|
||||
}
|
||||
}
|
||||
facetize -n inside_arbn_inner_nmg.s $arbn_inner
|
||||
|
||||
set invalid_arbn_inner inside_arbn_invalid.s
|
||||
if {![catch {inside $arbn_outer $invalid_arbn_inner {*}[lrepeat 6 6]}]} {
|
||||
error "ARBN inside accepted thicknesses that collapse the solid"
|
||||
}
|
||||
if {![catch {get $invalid_arbn_inner}]} {
|
||||
error "ARBN inside wrote an invalid collapsed solid"
|
||||
}
|
||||
|
||||
puts "*** 'arb' testing completed ***"
|
||||
|
||||
@@ -962,8 +962,140 @@ foreach test [list \
|
||||
csg_matrix_mouse_check {*}$test
|
||||
}
|
||||
|
||||
puts "--- Repeated graphical object transforms ---"
|
||||
proc csg_matrix_repeated_mouse_check {} {
|
||||
global orig_gui transform csg_mouse_dm_pixels csg_mouse_view_size
|
||||
global csg_drag_scale
|
||||
set prim csg_matrix_repeat.s
|
||||
set inner csg_matrix_repeat_inner.c
|
||||
set outer csg_matrix_repeat_outer.c
|
||||
set saved_orig_gui $orig_gui
|
||||
set expected [csg_edit_identity_matrix]
|
||||
set failed [catch {
|
||||
in $prim sph 0 0 0 10
|
||||
comb $inner u $prim
|
||||
comb $outer u $inner
|
||||
set orig_gui 0
|
||||
for {set step 0} {$step < 16} {incr step} {
|
||||
e $outer
|
||||
center 0 0 0
|
||||
size $csg_mouse_view_size
|
||||
setview 0 0 0
|
||||
dm size $csg_mouse_dm_pixels $csg_mouse_dm_pixels
|
||||
_mged_press oill
|
||||
_mged_ill -e -i 1 /$outer/$inner/$prim
|
||||
_mged_matpick 2
|
||||
if {[status state] ne "OBJ EDIT" || $transform ne "e"} {
|
||||
error "step $step did not enter object editing"
|
||||
}
|
||||
set view_before [csg_edit_view_state]
|
||||
if {$step % 2 == 0} {
|
||||
press Rotate
|
||||
dm con r z 100 100
|
||||
set operation [csg_matrix_axis_rotation z 10]
|
||||
set motion {dm motion 140 100}
|
||||
} else {
|
||||
press Scale
|
||||
dm am s 100 100
|
||||
set operation [csg_matrix_scale all $csg_drag_scale]
|
||||
set motion {dm motion 164 100}
|
||||
}
|
||||
set motion_code [catch {uplevel #0 $motion} motion_result]
|
||||
if {$motion_code != 0 && $motion_code != 2} {
|
||||
error "step $step mouse motion failed: $motion_result"
|
||||
}
|
||||
dm idle
|
||||
press accept
|
||||
set expected [csg_matrix_multiply $operation $expected]
|
||||
csg_edit_comb_leaf_matrix_at $inner $prim $expected
|
||||
csg_edit_view_state_at $view_before
|
||||
d $outer
|
||||
}
|
||||
} failure]
|
||||
set orig_gui $saved_orig_gui
|
||||
catch {press reject}
|
||||
catch {Z}
|
||||
foreach object [list $outer $inner $prim] {
|
||||
catch {kill $object}
|
||||
}
|
||||
if {$failed} {
|
||||
puts " FAIL: repeated object mouse rotations/scales ($failure)"
|
||||
} else {
|
||||
puts " PASS: repeated object mouse rotations/scales"
|
||||
}
|
||||
}
|
||||
csg_matrix_repeated_mouse_check
|
||||
|
||||
puts "--- All ARB point, edge, and face states ---"
|
||||
|
||||
proc csg_edit_sed_point {attribute} {
|
||||
set serialized [get_sed]
|
||||
set index [lsearch -exact $serialized $attribute]
|
||||
if {$index < 0 || $index + 1 >= [llength $serialized]} {
|
||||
error "get_sed has no $attribute: $serialized"
|
||||
}
|
||||
return [lindex $serialized [expr {$index + 1}]]
|
||||
}
|
||||
|
||||
proc csg_arb_repeated_mouse_check {} {
|
||||
global orig_gui csg_bv_coord_half_range csg_mouse_dm_pixels
|
||||
global csg_mouse_view_size
|
||||
set source csg_lifecycle_arb5.s
|
||||
set prim csg_arb_repeat.s
|
||||
set saved_orig_gui $orig_gui
|
||||
set expected {}
|
||||
set failed [catch {
|
||||
cp $source $prim
|
||||
e $prim
|
||||
center 0 0 0
|
||||
size $csg_mouse_view_size
|
||||
setview 0 0 0
|
||||
dm size $csg_mouse_dm_pixels $csg_mouse_dm_pixels
|
||||
set orig_gui 0
|
||||
sed $prim
|
||||
press {Move Edges}
|
||||
press {Move Point 5}
|
||||
set original_z [lindex [db get $source V5] 2]
|
||||
for {set step 0} {$step < 64} {incr step} {
|
||||
if {$step > 0 && $step % 8 == 0} {
|
||||
press sedit
|
||||
press {Move Edges}
|
||||
press {Move Point 5}
|
||||
}
|
||||
set mouse_x [expr {150 + 2 * ($step % 5)}]
|
||||
set mouse_y [expr {150 + 2 * (($step * 3) % 5)}]
|
||||
M 1 $mouse_x $mouse_y
|
||||
set expected [list \
|
||||
[expr {$mouse_x / $csg_bv_coord_half_range * $csg_mouse_view_size / 2.0}] \
|
||||
[expr {$mouse_y / $csg_bv_coord_half_range * $csg_mouse_view_size / 2.0}] \
|
||||
$original_z]
|
||||
set actual [csg_edit_sed_point V5]
|
||||
if {[csg_edit_point_distance $actual $expected] > 0.000001} {
|
||||
error "step $step expected V5 $expected, got $actual"
|
||||
}
|
||||
}
|
||||
press accept
|
||||
d $prim
|
||||
if {[csg_edit_point_distance [db get $prim V5] $expected] > 0.000001} {
|
||||
error "accepted V5 differs from the final mouse target"
|
||||
}
|
||||
foreach vertex {V1 V2 V3 V4} {
|
||||
csg_edit_point_matches_source $source $vertex $prim
|
||||
}
|
||||
arb_standard_storage_check arb5 $prim
|
||||
} failure]
|
||||
set orig_gui $saved_orig_gui
|
||||
catch {press reject}
|
||||
catch {d $prim}
|
||||
catch {kill $prim}
|
||||
if {$failed} {
|
||||
puts " FAIL: repeated ARB5 mouse movement ($failure)"
|
||||
} else {
|
||||
puts " PASS: repeated ARB5 mouse movement"
|
||||
}
|
||||
}
|
||||
csg_arb_repeated_mouse_check
|
||||
|
||||
# Every ARB menu entry is checked independently. The variants do not share
|
||||
# menu indices, so sampling one edge or face would miss precisely the class of
|
||||
# integration failure this test is intended to catch. Move each selection a
|
||||
@@ -1322,6 +1454,280 @@ csg_edit_sequence_check csg_lifecycle_dsp.s "Toggle Smooth" {
|
||||
{press {Toggle Smooth}}
|
||||
} [list csg_edit_scalar_at sm 1]
|
||||
|
||||
puts "--- Specialized mouse editing and menu inventory ---"
|
||||
proc csg_edit_prepare_mouse_view {} {
|
||||
global csg_dispatch_view_size
|
||||
center 0 0 0
|
||||
size $csg_dispatch_view_size
|
||||
setview 0 0 0
|
||||
}
|
||||
array set csg_mouse_covered {}
|
||||
proc csg_mouse_case {source menu_items label commands validator} {
|
||||
global csg_mouse_covered
|
||||
csg_edit_sequence_check $source $label $commands $validator
|
||||
set type [lindex [db get $source] 0]
|
||||
if {![info exists csg_mouse_covered($type)]} {
|
||||
set csg_mouse_covered($type) {}
|
||||
}
|
||||
foreach item $menu_items {
|
||||
if {[lsearch -exact $csg_mouse_covered($type) $item] < 0} {
|
||||
lappend csg_mouse_covered($type) $item
|
||||
}
|
||||
}
|
||||
}
|
||||
proc csg_edit_menu_inventory_check {source ignored} {
|
||||
global csg_mouse_covered
|
||||
if {[catch {
|
||||
Z
|
||||
e $source
|
||||
sed $source
|
||||
set actual [mmenu_get 0]
|
||||
set type [lindex [db get $source] 0]
|
||||
set covered {}
|
||||
if {[info exists csg_mouse_covered($type)]} {
|
||||
set covered $csg_mouse_covered($type)
|
||||
}
|
||||
foreach item $actual {
|
||||
set tested [expr {[lsearch -exact $covered $item] >= 0}]
|
||||
set exempt [expr {[lsearch -exact $ignored $item] >= 0}]
|
||||
if {$tested == $exempt} {
|
||||
error "menu entry '$item' needs exactly one mouse-test or exclusion"
|
||||
}
|
||||
}
|
||||
foreach item [concat $covered $ignored] {
|
||||
if {[lsearch -exact $actual $item] < 0} {
|
||||
error "recorded menu entry '$item' is absent from $actual"
|
||||
}
|
||||
}
|
||||
press reject
|
||||
d $source
|
||||
} failure]} {
|
||||
puts " FAIL: menu inventory $source ($failure)"
|
||||
catch {press reject}
|
||||
catch {Z}
|
||||
} else {
|
||||
puts " PASS: menu inventory $source"
|
||||
}
|
||||
}
|
||||
|
||||
csg_edit_prepare_mouse_view
|
||||
set csg_special_mouse_target [list \
|
||||
[expr {512.0 / $csg_bv_coord_half_range * $csg_dispatch_view_size / 2.0}] \
|
||||
[expr {512.0 / $csg_bv_coord_half_range * $csg_dispatch_view_size / 2.0}] 0]
|
||||
set csg_special_mouse_scale [expr {1.0 + 2047.0 / ($csg_bv_coord_half_range * 4.0)}]
|
||||
set csg_special_mouse_shrink [expr {
|
||||
1.0 / (1.0 + 1024.0 / ($csg_bv_coord_half_range * 4.0))}]
|
||||
csg_mouse_case csg_lifecycle_pipe.s {{Move Point}} "mouse select and move point" {
|
||||
{csg_edit_prepare_mouse_view}
|
||||
{press {Select Point}}
|
||||
{p 0 0 0}
|
||||
{press {Move Point}}
|
||||
{M 1 512 512}
|
||||
} [list csg_edit_point_at V0 $csg_special_mouse_target]
|
||||
in csg_mouse_pipe_topology.s pipe 4 \
|
||||
0 0 0 1 2 3 \
|
||||
0 0 30 1 2 3 \
|
||||
30 40 80 1 2 3 \
|
||||
80 80 100 1 2 3
|
||||
csg_mouse_case csg_mouse_pipe_topology.s {{Select Point} {Move Point}} "mouse select point" {
|
||||
{csg_edit_prepare_mouse_view}
|
||||
{press {Select Point}}
|
||||
{M 1 0 0}
|
||||
{press {Move Point}}
|
||||
{M 1 512 512}
|
||||
} [list csg_edit_point_at V0 $csg_special_mouse_target]
|
||||
csg_mouse_case csg_mouse_pipe_topology.s {{Append Point}} "mouse append point" {
|
||||
{csg_edit_prepare_mouse_view}
|
||||
{press {Append Point}}
|
||||
{M 1 512 512}
|
||||
} [list csg_edit_point_at V4 {5 5 0}]
|
||||
csg_mouse_case csg_mouse_pipe_topology.s {{Prepend Point}} "mouse prepend point" {
|
||||
{csg_edit_prepare_mouse_view}
|
||||
{press {Prepend Point}}
|
||||
{M 1 512 512}
|
||||
} [list csg_edit_point_at V0 {5 5 0}]
|
||||
csg_mouse_case csg_mouse_pipe_topology.s {{Split Segment}} "mouse split segment" {
|
||||
{csg_edit_prepare_mouse_view}
|
||||
{press {Select Point}}
|
||||
{p 0 0 0}
|
||||
{press {Split Segment}}
|
||||
{M 1 512 512}
|
||||
} [list csg_edit_point_at V1 {5 5 0}]
|
||||
foreach {item attribute initial} {
|
||||
{Set Point OD} O0 5
|
||||
{Set Point ID} I0 3
|
||||
{Set Point Bend} R0 6
|
||||
} {
|
||||
csg_mouse_case csg_lifecycle_pipe.s [list $item] "mouse $item" [list \
|
||||
{csg_edit_prepare_mouse_view} \
|
||||
{press {Select Point}} {p 0 0 0} [list press $item] \
|
||||
{M 1 0 -1024}] \
|
||||
[list csg_edit_scalar_at $attribute [expr {$initial * $csg_special_mouse_shrink}]]
|
||||
}
|
||||
foreach {item attributes initial} {
|
||||
{Set Pipe OD} {O0 O1 O2 O3} {5 5 6 6}
|
||||
{Set Pipe ID} {I0 I1 I2 I3} {3 3 2 0}
|
||||
{Set Pipe Bend} {R0 R1 R2 R3} {6 7 10 8}
|
||||
} {
|
||||
set expected {}
|
||||
foreach value $initial {
|
||||
lappend expected [expr {$value * $csg_special_mouse_shrink}]
|
||||
}
|
||||
csg_mouse_case csg_lifecycle_pipe.s [list $item] "mouse $item" [list \
|
||||
{csg_edit_prepare_mouse_view} \
|
||||
[list press $item] {M 1 0 -1024}] \
|
||||
[list csg_edit_scalar_values_at $attributes $expected]
|
||||
}
|
||||
proc csg_edit_bot_vertices_moved {source moved delta prim} {
|
||||
set vertices [db get $source V]
|
||||
for {set index 0} {$index < [llength $vertices]} {incr index} {
|
||||
set expected [lindex $vertices $index]
|
||||
if {[lsearch -exact $moved $index] >= 0} {
|
||||
set translated {}
|
||||
foreach value $expected shift $delta {
|
||||
lappend translated [expr {$value + $shift}]
|
||||
}
|
||||
set expected $translated
|
||||
}
|
||||
csg_edit_list_value_at V $index $expected $prim
|
||||
}
|
||||
}
|
||||
csg_mouse_case csg_lifecycle_bot.s {{Move Vertex}} "mouse move vertex" {
|
||||
{csg_edit_prepare_mouse_view}
|
||||
{press {Pick Vertex}}
|
||||
{p 0}
|
||||
{press {Move Vertex}}
|
||||
{M 1 512 512}
|
||||
} [list csg_edit_bot_vertices_moved csg_lifecycle_bot.s {0} \
|
||||
$csg_special_mouse_target]
|
||||
csg_mouse_case csg_lifecycle_bot.s {{Pick Vertex} {Move Vertex}} "mouse pick and move vertex" {
|
||||
{csg_edit_prepare_mouse_view}
|
||||
{press {Pick Vertex}}
|
||||
{M 1 0 0}
|
||||
{press {Move Vertex}}
|
||||
{M 1 512 512}
|
||||
} [list csg_edit_bot_vertices_moved csg_lifecycle_bot.s {0} \
|
||||
$csg_special_mouse_target]
|
||||
csg_mouse_case csg_lifecycle_bot.s {{Move Edge}} "mouse move edge" {
|
||||
{csg_edit_prepare_mouse_view}
|
||||
{press {Pick Edge}}
|
||||
{p 0 1}
|
||||
{press {Move Edge}}
|
||||
{M 1 1024 512}
|
||||
} [list csg_edit_bot_vertices_moved csg_lifecycle_bot.s {0 1} {10 5 0}]
|
||||
csg_mouse_case csg_lifecycle_bot.s {{Pick Edge} {Move Edge}} "mouse pick and move edge" {
|
||||
{csg_edit_prepare_mouse_view}
|
||||
{press {Pick Edge}}
|
||||
{M 1 512 512}
|
||||
{press {Move Edge}}
|
||||
{M 1 1024 512}
|
||||
} [list csg_edit_bot_vertices_moved csg_lifecycle_bot.s {0 1} {10 5 0}]
|
||||
csg_mouse_case csg_lifecycle_bot.s {{Move Triangle}} "mouse move triangle" {
|
||||
{csg_edit_prepare_mouse_view}
|
||||
{press {Pick Triangle}}
|
||||
{p 0 1 2}
|
||||
{press {Move Triangle}}
|
||||
{M 1 512 512}
|
||||
} [list csg_edit_bot_vertices_moved csg_lifecycle_bot.s {0 1 2} \
|
||||
$csg_special_mouse_target]
|
||||
csg_mouse_case csg_lifecycle_ebm.s {{Extrude Depth}} "mouse extrusion depth" {
|
||||
{csg_edit_prepare_mouse_view}
|
||||
{press {Extrude Depth}}
|
||||
{M 1 0 2047}
|
||||
} [list csg_edit_scalar_at H [expr {4.0 * $csg_special_mouse_scale}]]
|
||||
csg_mouse_case csg_lifecycle_vol.s {{Threshold (low)}} "mouse low threshold" {
|
||||
{csg_edit_prepare_mouse_view}
|
||||
{press {Threshold (low)}}
|
||||
{p 10}
|
||||
{M 1 0 2047}
|
||||
} [list csg_edit_scalar_at lo [expr {int(10.0 * $csg_special_mouse_scale)}]]
|
||||
csg_mouse_case csg_lifecycle_vol.s {{Voxel Size (X Y Z)}} "mouse voxel size" {
|
||||
{csg_edit_prepare_mouse_view}
|
||||
{press {Voxel Size (X Y Z)}}
|
||||
{M 1 0 2047}
|
||||
} [list csg_edit_point_at size [list \
|
||||
$csg_special_mouse_scale $csg_special_mouse_scale $csg_special_mouse_scale]]
|
||||
csg_mouse_case csg_lifecycle_vol.s {{Threshold (hi)}} "mouse high threshold" {
|
||||
{csg_edit_prepare_mouse_view}
|
||||
{press {Threshold (hi)}}
|
||||
{M 1 0 -1024}
|
||||
} [list csg_edit_scalar_at hi [expr {int(255.0 * $csg_special_mouse_shrink)}]]
|
||||
csg_mouse_case csg_lifecycle_dsp.s {{Set X}} "mouse X scale" {
|
||||
{csg_edit_prepare_mouse_view}
|
||||
{press {Set X}}
|
||||
{M 1 0 2047}
|
||||
} [list csg_edit_matrix_component_at stom 0 $csg_special_mouse_scale]
|
||||
foreach {item index} {{Set Y} 5 {Set ALT} 10} {
|
||||
csg_mouse_case csg_lifecycle_dsp.s [list $item] "mouse $item" [list \
|
||||
{csg_edit_prepare_mouse_view} \
|
||||
[list press $item] {M 1 0 2047}] \
|
||||
[list csg_edit_matrix_component_at stom $index $csg_special_mouse_scale]
|
||||
}
|
||||
foreach type {arbn brep} {
|
||||
set source csg_lifecycle_${type}.s
|
||||
csg_edit_sequence_check $source "generic mouse translation" {
|
||||
{csg_edit_prepare_mouse_view}
|
||||
{keypoint 0 0 0}
|
||||
{press sxy}
|
||||
{M 1 512 512}
|
||||
} [list csg_edit_bbox_translated $source $csg_special_mouse_target]
|
||||
}
|
||||
|
||||
# Every specialized menu entry must be accounted for. Exclusions are menu
|
||||
# headings, non-positional actions/settings, or the BOT triangle picker (tested
|
||||
# under Xmin because it needs Tk). A new entry without a mouse case or
|
||||
# explicit exclusion fails here.
|
||||
foreach {source ignored} {
|
||||
csg_lifecycle_pipe.s {
|
||||
{PIPE MENU} {Next Point} {Previous Point} {Delete Point}
|
||||
}
|
||||
csg_lifecycle_bot.s {
|
||||
{BOT MENU} {Pick Triangle} {Move Vertex List}
|
||||
{Split Edge} {Split Face} {Fuse Vertices} {Fuse Faces}
|
||||
{Delete Triangle} {Select Mode} {Select Orientation}
|
||||
{Set flags} {Set Face Thickness} {Set Face Mode}
|
||||
}
|
||||
csg_lifecycle_dsp.s {
|
||||
{DSP MENU} Name {Toggle Smooth} {Set Data Source}
|
||||
}
|
||||
csg_lifecycle_ebm.s {
|
||||
{EBM MENU} {File Name} {File Size (W N)}
|
||||
}
|
||||
csg_lifecycle_vol.s {
|
||||
{VOL MENU} {File Name} {File Size (X Y Z)}
|
||||
}
|
||||
csg_lifecycle_arbn.s {}
|
||||
csg_lifecycle_brep.s {}
|
||||
} {
|
||||
csg_edit_menu_inventory_check $source $ignored
|
||||
}
|
||||
|
||||
# A view-space request must remain view-space even when the editor's current
|
||||
# coordinate system is object-relative. This is the fast path underlying the
|
||||
# corresponding real mouse drag in the Xmin GUI test.
|
||||
set csg_saved_coords [rset var coords]
|
||||
set csg_saved_transform [rset var transform]
|
||||
if {[catch {
|
||||
Z
|
||||
e csg_lifecycle_bot.s
|
||||
sed csg_lifecycle_bot.s
|
||||
center 0 0 0
|
||||
rset var coords o
|
||||
rset var transform v
|
||||
knob -i -v aX 1
|
||||
if {[csg_edit_point_distance [center] {-1 0 0}] > 0.000001} {
|
||||
error "explicit view knob left center at [center]"
|
||||
}
|
||||
} csg_view_override_error]} {
|
||||
puts " FAIL: view-space translation with object coordinates ($csg_view_override_error)"
|
||||
} else {
|
||||
puts " PASS: view-space translation with object coordinates"
|
||||
}
|
||||
catch {press reject}
|
||||
catch {Z}
|
||||
rset var coords $csg_saved_coords
|
||||
rset var transform $csg_saved_transform
|
||||
|
||||
file delete -force $csg_data_file
|
||||
file delete -force $csg_dsp_file
|
||||
|
||||
|
||||
@@ -43,4 +43,37 @@ foreach {arb_type face expected_type} $arb_extrude_cases {
|
||||
arb_standard_storage_check $expected_type $prim
|
||||
}
|
||||
|
||||
# A one-inch extrusion must extend a face by 25.4 base millimeters through
|
||||
# both the direct libged command and MGED's interactive solid editor.
|
||||
set inch_in_mm 25.4
|
||||
set tolerance 0.001
|
||||
foreach mode {direct interactive} {
|
||||
set suffix "_inch_$mode"
|
||||
in_arb8 extrude $suffix
|
||||
set prim "extrude_arb8${suffix}.s"
|
||||
set base_x [lindex [db get $prim V1] 0]
|
||||
|
||||
units in
|
||||
if {$mode eq "direct"} {
|
||||
if {[catch {edarb extrude $prim 1234 1} result]} {
|
||||
error "inch edarb extrusion failed: $result"
|
||||
}
|
||||
} else {
|
||||
Z
|
||||
e $prim
|
||||
sed $prim
|
||||
if {[catch {extrude 1234 1} result]} {
|
||||
error "inch MGED extrusion failed: $result"
|
||||
}
|
||||
accept
|
||||
Z
|
||||
}
|
||||
units mm
|
||||
|
||||
set new_x [lindex [db get $prim V5] 0]
|
||||
if {abs($new_x - $base_x - $inch_in_mm) > $tolerance} {
|
||||
error "inch $mode extrusion moved V5 by [expr {$new_x - $base_x}] mm"
|
||||
}
|
||||
}
|
||||
|
||||
puts "*** 'extrude' testing completed ***"
|
||||
|
||||
@@ -31,6 +31,7 @@ in tor_pe.s tor 0 0 0 0 0 1 20 2
|
||||
prim_edit_check tor_pe.s "Set Radius 1" 30 r_a 30 -1
|
||||
prim_edit_check tor_pe.s "Set Radius 2" 5 r_h 5 -1
|
||||
prim_edit_mouse_check tor_pe.s "Set Radius 1" r_a 30 -1
|
||||
prim_edit_mouse_check tor_pe.s "Set Radius 2" r_h 5 -1
|
||||
kill tor_pe.s
|
||||
|
||||
puts "--- ELL (ellipsoid: A, B, C) ---"
|
||||
@@ -39,6 +40,8 @@ prim_edit_check ell_pe.s "Set A" 8 A 8 0
|
||||
prim_edit_check ell_pe.s "Set B" 6 B 6 1
|
||||
prim_edit_check ell_pe.s "Set C" 4 C 4 2
|
||||
prim_edit_mouse_check ell_pe.s "Set A" A 8 0
|
||||
prim_edit_mouse_check ell_pe.s "Set B" B 6 1
|
||||
prim_edit_mouse_check ell_pe.s "Set C" C 4 2
|
||||
kill ell_pe.s
|
||||
|
||||
puts "--- RPC (right parabolic cylinder: H, r) ---"
|
||||
@@ -46,6 +49,7 @@ in rpc_pe.s rpc 0 0 0 0 0 10 0 5 0 3
|
||||
prim_edit_check rpc_pe.s "Set H" 20 H 20 2
|
||||
prim_edit_check rpc_pe.s "Set r" 6 r 6 -1
|
||||
prim_edit_mouse_check rpc_pe.s "Set H" H 20 2
|
||||
prim_edit_mouse_check rpc_pe.s "Set r" r 6 -1
|
||||
kill rpc_pe.s
|
||||
|
||||
puts "--- RHC (right hyperbolic cylinder: H, r, c) ---"
|
||||
@@ -54,6 +58,8 @@ prim_edit_check rhc_pe.s "Set H" 20 H 20 2
|
||||
prim_edit_check rhc_pe.s "Set r" 6 r 6 -1
|
||||
prim_edit_check rhc_pe.s "Set c" 3 c 3 -1
|
||||
prim_edit_mouse_check rhc_pe.s "Set r" r 6 -1
|
||||
prim_edit_mouse_check rhc_pe.s "Set H" H 20 2
|
||||
prim_edit_mouse_check rhc_pe.s "Set c" c 3 -1
|
||||
kill rhc_pe.s
|
||||
|
||||
puts "--- EPA (elliptical paraboloid: H, r_1, r_2) ---"
|
||||
@@ -62,6 +68,8 @@ prim_edit_check epa_pe.s "Set H" 20 H 20 2
|
||||
prim_edit_check epa_pe.s "Set A" 8 r_1 8 -1
|
||||
prim_edit_check epa_pe.s "Set B" 4 r_2 4 -1
|
||||
prim_edit_mouse_check epa_pe.s "Set H" H 20 2
|
||||
prim_edit_mouse_check epa_pe.s "Set A" r_1 8 -1
|
||||
prim_edit_mouse_check epa_pe.s "Set B" r_2 4 -1
|
||||
kill epa_pe.s
|
||||
|
||||
puts "--- EHY (elliptical hyperboloid: H, r_1, c) ---"
|
||||
@@ -70,6 +78,8 @@ prim_edit_check ehy_pe.s "Set H" 20 H 20 2
|
||||
prim_edit_check ehy_pe.s "Set A" 8 r_1 8 -1
|
||||
prim_edit_check ehy_pe.s "Set c" 3 c 3 -1
|
||||
prim_edit_mouse_check ehy_pe.s "Set A" r_1 8 -1
|
||||
prim_edit_mouse_check ehy_pe.s "Set H" H 20 2
|
||||
prim_edit_mouse_check ehy_pe.s "Set c" c 3 -1
|
||||
kill ehy_pe.s
|
||||
|
||||
puts "--- PART (particle: H, r_v, r_h) ---"
|
||||
@@ -78,6 +88,8 @@ prim_edit_check part_pe.s "Set H" 20 H 20 2
|
||||
prim_edit_check part_pe.s "Set v" 4 r_v 4 -1
|
||||
prim_edit_check part_pe.s "Set h" 3 r_h 3 -1
|
||||
prim_edit_mouse_check part_pe.s "Set v" r_v 4 -1
|
||||
prim_edit_mouse_check part_pe.s "Set H" H 20 2
|
||||
prim_edit_mouse_check part_pe.s "Set h" r_h 3 -1
|
||||
kill part_pe.s
|
||||
|
||||
puts "--- TGC (truncated general cone: H, A, B, C, D) ---"
|
||||
@@ -91,6 +103,10 @@ prim_edit_check tgc_pe.s "Set B" 3 B 3 1
|
||||
prim_edit_check tgc_pe.s "Set C" 3 C 3 0
|
||||
prim_edit_check tgc_pe.s "Set D" 4 D 4 1
|
||||
prim_edit_mouse_check tgc_pe.s "Set H" H 8 2
|
||||
prim_edit_mouse_check tgc_pe.s "Set A" A 4 0
|
||||
prim_edit_mouse_check tgc_pe.s "Set B" B 3 1
|
||||
prim_edit_mouse_check tgc_pe.s "Set C" C 3 0
|
||||
prim_edit_mouse_check tgc_pe.s "Set D" D 4 1
|
||||
kill tgc_pe.s
|
||||
|
||||
puts "--- ETO (elliptical torus: r, C, D) ---"
|
||||
@@ -100,6 +116,7 @@ prim_edit_check eto_pe.s "Set r" 8 r 8 -1
|
||||
prim_edit_check eto_pe.s "Set D" 1.5 r_d 1.5 -1
|
||||
prim_edit_mouse_check eto_pe.s "Set r" r 8 -1
|
||||
prim_edit_mouse_check eto_pe.s "Set C" C 5 0
|
||||
prim_edit_mouse_check eto_pe.s "Set D" r_d 1.5 -1
|
||||
kill eto_pe.s
|
||||
|
||||
puts "--- HYP (one-sheet hyperboloid: H, A, B, c) ---"
|
||||
@@ -111,6 +128,9 @@ prim_edit_check hyp_pe.s "Set A" 2 A 10 1
|
||||
prim_edit_check hyp_pe.s "Set B" 2 b 4 -1
|
||||
prim_edit_check hyp_pe.s "Set c" 0.5 bnr 0.25 -1
|
||||
prim_edit_mouse_check hyp_pe.s "Set H" H 20 2
|
||||
prim_edit_mouse_check hyp_pe.s "Set A" A 10 1
|
||||
prim_edit_mouse_check hyp_pe.s "Set B" b 4 -1
|
||||
prim_edit_mouse_check hyp_pe.s "Set c" bnr 0.25 -1
|
||||
kill hyp_pe.s
|
||||
|
||||
puts "--- SUPERELL (superellipsoid: A, B, C) ---"
|
||||
@@ -119,6 +139,8 @@ prim_edit_check superell_pe.s "Set A" 5 A 5 0
|
||||
prim_edit_check superell_pe.s "Set B" 4 B 4 1
|
||||
prim_edit_check superell_pe.s "Set C" 3 C 3 2
|
||||
prim_edit_mouse_check superell_pe.s "Set A" A 5 0
|
||||
prim_edit_mouse_check superell_pe.s "Set B" B 4 1
|
||||
prim_edit_mouse_check superell_pe.s "Set C" C 3 2
|
||||
kill superell_pe.s
|
||||
|
||||
puts "--- CLINE (cylinder line: H, R, plate thickness) ---"
|
||||
@@ -129,6 +151,7 @@ prim_edit_check cline_pe.s "Set R" 4 R 4 -1
|
||||
prim_edit_check cline_pe.s "Set plate thickness" 0.4 T 0.4 -1
|
||||
prim_edit_mouse_check cline_pe.s "Set H" H 8 2
|
||||
prim_edit_mouse_check cline_pe.s "Set R" R 4 -1
|
||||
prim_edit_mouse_check cline_pe.s "Set plate thickness" T 0.4 -1
|
||||
kill cline_pe.s
|
||||
|
||||
puts "--- NMG (non-manifold geometry: Pick Edge, Move Edge, Next EU) ---"
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
puts "*** Testing 'sed_reset' command ***"
|
||||
|
||||
proc sed_reset_check {primitive edit_commands} {
|
||||
e $primitive
|
||||
sed $primitive
|
||||
if {[status state] ne "SOL EDIT"} {
|
||||
error "$primitive entered [status state], not SOL EDIT"
|
||||
}
|
||||
|
||||
set original [get_sed]
|
||||
foreach command $edit_commands {
|
||||
uplevel #0 $command
|
||||
}
|
||||
if {[get_sed] eq $original} {
|
||||
error "$primitive edit did not change the working geometry"
|
||||
}
|
||||
|
||||
sed_reset
|
||||
if {[status state] ne "SOL EDIT"} {
|
||||
error "$primitive reset entered [status state], not SOL EDIT"
|
||||
}
|
||||
if {[get_sed] ne $original} {
|
||||
error "$primitive reset did not restore the original geometry"
|
||||
}
|
||||
|
||||
press reject
|
||||
if {[status state] ne "VIEWING"} {
|
||||
error "$primitive reject entered [status state], not VIEWING"
|
||||
}
|
||||
d $primitive
|
||||
}
|
||||
|
||||
if {[catch {
|
||||
in sed_reset_arb8.s arb8 \
|
||||
0 0 0 10 0 0 10 10 0 0 10 0 \
|
||||
0 0 10 10 0 10 10 10 10 0 10 10
|
||||
sed_reset_check sed_reset_arb8.s {
|
||||
{press {Move Faces}}
|
||||
{press {Move Face 1234}}
|
||||
{p 0 0 -1}
|
||||
}
|
||||
} failure]} {
|
||||
puts "FAIL: ARB8 face move reset ($failure)"
|
||||
catch {press reject}
|
||||
catch {Z}
|
||||
} else {
|
||||
puts "PASS: ARB8 face move reset"
|
||||
}
|
||||
|
||||
if {[catch {
|
||||
make sed_reset_cline.s cline
|
||||
sed_reset_check sed_reset_cline.s {
|
||||
{press {Move End H}}
|
||||
{p 3 4 12}
|
||||
}
|
||||
} failure]} {
|
||||
puts "FAIL: CLINE edit reset ($failure)"
|
||||
catch {press reject}
|
||||
catch {Z}
|
||||
} else {
|
||||
puts "PASS: CLINE edit reset"
|
||||
}
|
||||
|
||||
puts "PASS: 'sed_reset' testing completed"
|
||||
@@ -93,6 +93,7 @@ if(BRLCAD_X11_PROVIDER_RESOLVED STREQUAL "XMIN" AND
|
||||
"PLOT3_ASC_BIN=$<TARGET_FILE:plot3-asc>"
|
||||
)
|
||||
mged_add_xmin_gui_fixture(help "mged;dm-tkswrast;m35.g")
|
||||
mged_add_xmin_gui_fixture(edit-interaction "mged;dm-tkswrast;m35.g")
|
||||
mged_add_xmin_gui_fixture(bot "mged;dm-tkswrast;m35.g")
|
||||
mged_add_xmin_gui_fixture(editors "mged;dm-tkswrast;m35.g")
|
||||
mged_add_xmin_gui_fixture(query-pattern "mged;dm-tkswrast;m35.g")
|
||||
@@ -143,6 +144,18 @@ if(BRLCAD_X11_PROVIDER_RESOLVED STREQUAL "XMIN" AND
|
||||
)
|
||||
brlcad_regression_test(regress-archer-embedded-xmin-gui
|
||||
"archer;bwish" STAND_ALONE TEST_DEFINED)
|
||||
|
||||
brlcad_add_gui_test(
|
||||
NAME regress-archer-geometry-xmin-gui
|
||||
TIMEOUT 120
|
||||
ENVIRONMENT ${_gui_smoke_environment}
|
||||
"GUI_TEST_LIBRARY=${_gui_test_library}"
|
||||
COMMAND "${SH_EXEC}" "${_gui_tcl_runner}" archer-geometry
|
||||
$<TARGET_FILE:bwish>
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/archer_geometry_xmin_gui.tcl"
|
||||
)
|
||||
brlcad_regression_test(regress-archer-geometry-xmin-gui
|
||||
"archer;bwish" STAND_ALONE TEST_DEFINED)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
@@ -200,6 +213,7 @@ cmakefiles(
|
||||
archer_action.sh
|
||||
archer_smoke.sh
|
||||
archer_embedded_xmin_gui.tcl
|
||||
archer_geometry_xmin_gui.tcl
|
||||
archer_menu_manifest.txt
|
||||
archer_xmin_gui.tcl
|
||||
qged_action.sh
|
||||
@@ -212,18 +226,19 @@ cmakefiles(
|
||||
mged/mged_xmin_gui.sh
|
||||
mged/xmin_gui_bot.tcl
|
||||
mged/xmin_gui_controls.tcl
|
||||
mged/xmin_gui_edit.tcl
|
||||
mged/xmin_gui_edit_interaction.tcl
|
||||
mged/xmin_gui_editors.tcl
|
||||
mged/xmin_gui_help.tcl
|
||||
mged/xmin_gui_lod.tcl
|
||||
mged/xmin_gui_overlaps.tcl
|
||||
mged/xmin_gui_pipe_oracle.tcl
|
||||
mged/xmin_gui_query_pattern.tcl
|
||||
mged/xmin_gui_render_outputs.tcl
|
||||
mged/xmin_gui_search_exec.tcl
|
||||
mged/xmin_gui_shotvis.tcl
|
||||
mged/xmin_gui_test.tcl
|
||||
mged/xmin_gui_workflows.tcl
|
||||
mged/xmin_gui_edit.tcl
|
||||
mged/xmin_gui_pipe_oracle.tcl
|
||||
)
|
||||
|
||||
# Local Variables:
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
# A R C H E R _ G E O M E T R Y _ X M I N _ G U I . T C L
|
||||
# BRL-CAD
|
||||
#
|
||||
# Copyright (c) 2026 United States Government as represented by
|
||||
# the U.S. Army Research Laboratory.
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public License
|
||||
# version 2.1 as published by the Free Software Foundation.
|
||||
#
|
||||
# This library is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this file; see the file named COPYING for more
|
||||
# information.
|
||||
#
|
||||
###
|
||||
if {![info exists ::env(GUI_TEST_LIBRARY)]} {
|
||||
puts stderr "GUI_TEST_LIBRARY is required"
|
||||
exit 2
|
||||
}
|
||||
source $::env(GUI_TEST_LIBRARY)
|
||||
set ::env(ARCHER_PREFS_FILE) [file join $::env(GUI_TEST_DIR) .archerrc]
|
||||
|
||||
proc archer_geometry_preferences {{geometry ""}} {
|
||||
set channel [open $::env(ARCHER_PREFS_FILE) w]
|
||||
if {$geometry ne ""} {
|
||||
puts $channel [list set mWindowGeometry $geometry]
|
||||
}
|
||||
close $channel
|
||||
}
|
||||
|
||||
proc archer_geometry_saved {} {
|
||||
set channel [open $::env(ARCHER_PREFS_FILE) r]
|
||||
set contents [read $channel]
|
||||
close $channel
|
||||
if {[regexp -line {^set mWindowGeometry (.+)$} $contents -> geometry]} {
|
||||
return $geometry
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
proc archer_geometry_settle {} {
|
||||
# Allow MapNotify and the deferred geometry restore to reach the event loop.
|
||||
set settle_delay_ms 100
|
||||
after $settle_delay_ms [list set ::archer_geometry_settled 1]
|
||||
vwait ::archer_geometry_settled
|
||||
update
|
||||
::gui::test::require_no_background_errors
|
||||
}
|
||||
|
||||
if {[catch {
|
||||
package require Archer 1.0
|
||||
::gui::test::capture_background_errors
|
||||
wm withdraw .
|
||||
|
||||
set saved_geometry 900x650+120+90
|
||||
set startup_geometry 1000x700+20+30
|
||||
set updated_geometry 950x680+60+70
|
||||
set default_geometry 920x660+40+50
|
||||
|
||||
archer_geometry_preferences $saved_geometry
|
||||
Archer .unmapped
|
||||
::gui::test::require {[wm state .unmapped] eq "withdrawn"} \
|
||||
"startup fixture unexpectedly showed its window"
|
||||
::itcl::delete object .unmapped
|
||||
archer_geometry_settle
|
||||
::gui::test::require {[archer_geometry_saved] eq $saved_geometry} \
|
||||
"closing an unmapped Archer replaced its saved geometry"
|
||||
|
||||
archer_geometry_preferences
|
||||
Archer .unmapped_new
|
||||
::itcl::delete object .unmapped_new
|
||||
archer_geometry_settle
|
||||
::gui::test::require {[archer_geometry_saved] eq ""} \
|
||||
"closing an unmapped Archer saved an unfinished layout size"
|
||||
|
||||
archer_geometry_preferences $saved_geometry
|
||||
Archer .restore
|
||||
.restore configure -geometry $startup_geometry
|
||||
wm geometry .restore $startup_geometry
|
||||
event generate [.restore component hpane] <Map>
|
||||
archer_geometry_settle
|
||||
::gui::test::require {[wm geometry .restore] eq $startup_geometry} \
|
||||
"a child Map event restored the main window's geometry prematurely"
|
||||
|
||||
wm deiconify .restore
|
||||
archer_geometry_settle
|
||||
::gui::test::require {[wm geometry .restore] eq $saved_geometry} \
|
||||
"mapping the main window did not restore its saved geometry"
|
||||
|
||||
wm geometry .restore $updated_geometry
|
||||
event generate [.restore component hpane] <Map>
|
||||
archer_geometry_settle
|
||||
::gui::test::require {[wm geometry .restore] eq $updated_geometry} \
|
||||
"a later child Map event reset the resized main window"
|
||||
wm withdraw .restore
|
||||
::itcl::delete object .restore
|
||||
archer_geometry_settle
|
||||
::gui::test::require {[archer_geometry_saved] eq $updated_geometry} \
|
||||
"closing a previously mapped window did not save its latest geometry"
|
||||
|
||||
archer_geometry_preferences
|
||||
Archer .default
|
||||
wm geometry .default $default_geometry
|
||||
wm deiconify .default
|
||||
archer_geometry_settle
|
||||
::gui::test::require {[wm geometry .default] eq $default_geometry} \
|
||||
"an empty geometry option reset the window to its layout size"
|
||||
wm withdraw .default
|
||||
::itcl::delete object .default
|
||||
archer_geometry_settle
|
||||
} message options]} {
|
||||
puts stderr $message
|
||||
if {[dict exists $options -errorinfo]} {
|
||||
puts stderr [dict get $options -errorinfo]
|
||||
}
|
||||
puts "FAIL: Archer window geometry regression"
|
||||
exit 1
|
||||
}
|
||||
|
||||
puts "PASS: Archer window geometry restoration and startup cancellation"
|
||||
exit 0
|
||||
|
||||
# Local Variables:
|
||||
# tab-width: 8
|
||||
# mode: Tcl
|
||||
# tcl-indent-level: 4
|
||||
# indent-tabs-mode: t
|
||||
# End:
|
||||
# ex: shiftwidth=4 tabstop=8
|
||||
@@ -50,6 +50,18 @@ proc ::mged::xmin::help::exercise_command_manual {top} {
|
||||
::gui::test::require {
|
||||
[llength $selection] == 1 && [$toc get $selection] eq "rt"
|
||||
} "man rt did not select and load the rt manual page"
|
||||
|
||||
event generate $toc <<ListboxSelect>>
|
||||
::mged::gui::test::settle
|
||||
|
||||
man search
|
||||
set html_view [$browser component browser].htmlview
|
||||
set html_widget [$html_view html]
|
||||
$html_view goto {#hierarchy_performance}
|
||||
::mged::gui::test::settle
|
||||
::gui::test::require {[lindex [$html_widget yview] 0] > 0.0} \
|
||||
"manual browser did not follow an in-page hyperlink"
|
||||
|
||||
::gui::test::require {
|
||||
[catch {man xmin_page_that_does_not_exist} message] &&
|
||||
[string first "couldn't find manual page" $message] >= 0
|
||||
@@ -151,10 +163,38 @@ proc ::mged::xmin::help::exercise_html_manual {top} {
|
||||
set mged_browser [file join $::env(GUI_TEST_DIR) missing-browser]
|
||||
::mged::gui::test::invoke $top {Help Manual}
|
||||
set fallback $top.man
|
||||
set viewer $fallback.html
|
||||
::gui::test::require {
|
||||
[winfo exists $fallback] && [winfo ismapped $fallback] &&
|
||||
[string length [string trim [$fallback.text get 1.0 end]]] > 100
|
||||
[winfo exists $viewer] && [$viewer title] eq "Table of Contents"
|
||||
} "Manual did not load MGED's internal HTML viewer"
|
||||
|
||||
set special_path [file join [pwd] {manual path} {contents#1.html}]
|
||||
::gui::test::require {
|
||||
[::mged::manual::local_path \
|
||||
[::mged::manual::file_uri $special_path]] eq \
|
||||
[file normalize $special_path]
|
||||
} "internal Manual viewer did not preserve special path characters"
|
||||
|
||||
set html [$viewer html]
|
||||
set contents [$html text text]
|
||||
::gui::test::require {
|
||||
[string first "Preface" $contents] >= 0 &&
|
||||
[string first "background-color" $contents] < 0
|
||||
} "internal Manual viewer did not render its table of contents"
|
||||
|
||||
$viewer goto mged.html#csg
|
||||
::mged::gui::test::settle
|
||||
set contents [$html text text]
|
||||
::gui::test::require {
|
||||
[$viewer title] eq "Mged User's Manual" &&
|
||||
[string first "Modeling With CSG" $contents] >= 0
|
||||
} "internal Manual viewer did not follow its document links"
|
||||
|
||||
$fallback.f.back invoke
|
||||
::mged::gui::test::settle
|
||||
::gui::test::require {[$viewer title] eq "Table of Contents"} \
|
||||
"internal Manual viewer Back button did not restore the prior page"
|
||||
destroy $fallback
|
||||
}
|
||||
|
||||
|
||||
@@ -1,508 +0,0 @@
|
||||
Remaining STEP Import/Export Work
|
||||
=================================
|
||||
|
||||
Status: 2026-08-05
|
||||
|
||||
|
||||
Purpose
|
||||
-------
|
||||
|
||||
This is the current implementation and qualification plan for the public
|
||||
step-g and g-step converters. It replaces recommended_practices_todo.txt,
|
||||
save_invalid_plan.txt, and step_refactor.txt. It is not a development
|
||||
changelog: completed work is summarized only to define the supported baseline,
|
||||
and the detailed sections describe work which remains.
|
||||
|
||||
Status terms used below are:
|
||||
|
||||
* COMPLETE -- implemented and covered by focused tests;
|
||||
* ACTIVE -- being qualified or reduced from real inputs;
|
||||
* REMAINING -- useful work with a defined scope;
|
||||
* BLOCKED -- requires a missing prerequisite and must not be approximated; and
|
||||
* DEFERRED -- outside the present converter objective.
|
||||
|
||||
A feature is complete only when schema-valid fixtures, permissive and strict
|
||||
policy tests, and structured report assertions prove that geometry or semantics
|
||||
did not disappear. Independent-producer evidence is required wherever a
|
||||
BRL-CAD export and re-import could otherwise repeat the same mistake.
|
||||
|
||||
|
||||
Supported Baseline
|
||||
------------------
|
||||
|
||||
Architecture and schemas
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The public converter architecture is COMPLETE:
|
||||
|
||||
* step-g and g-step are schema-dispatching hosts;
|
||||
* AP203 edition 1, AP203 edition 2, AP214 edition 3, and AP242 editions 1
|
||||
through 4 are bidirectional plugins;
|
||||
* AP242 generated bindings remain edition-specific because the entity sets and
|
||||
some Part 21 physical layouts differ;
|
||||
* the AP242 export alias targets edition 4, while automatic import probes
|
||||
editions 4 through 1;
|
||||
* common conversion behavior is schema-neutral and schema-only behavior is
|
||||
under ap203, ap203e2, ap214, or ap242; and
|
||||
* obsolete schema-specific public converter programs are not built.
|
||||
|
||||
Explicit --schema ap242e1 through ap242e4 selection remains useful when a file
|
||||
uses only the editions' common physical subset. Interim identifiers
|
||||
CONFIGURATION_CONTROL_3D_DESIGN_MIM_LF, CCD_CLA_GVP_AST, and
|
||||
CCD_CLA_GVP_AST_ASD route to AP203e2. Supported multi-schema AP203e2 headers
|
||||
also route correctly; unknown companion schemas remain visible, and genuinely
|
||||
ambiguous supported-AP combinations fail rather than guessing.
|
||||
|
||||
The STEPcode lazy reader supports multiple DATA sections, SCOPE,
|
||||
source-ordered complex components, bounded diagnostics, type census output,
|
||||
and schema-independent retention of unknown configuration records. AP203
|
||||
Recommended Practice 8 fixtures cover referenced, mapped, and replicated
|
||||
shapes, multi-level hierarchy, SHUO and usage semantics, multiple SDR forms,
|
||||
wireframes, configuration data, and SCOPE.
|
||||
|
||||
STEPcode and build tractability
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
BRL-CAD uses STEPcode's compact generated API v2, packed schema modules,
|
||||
precomputed attribute layouts, bounded unity chunks, and generated source
|
||||
manifests. Entity and type chunk limits are independently tunable; 64:8 is the
|
||||
qualified default. STEP plugins retain optimized compilation but disable LTO.
|
||||
|
||||
On the qualification host, a serial AP242e4 binding build without LTO fell
|
||||
from 8m25s and 6.22 GB peak RSS at 64:64 to 6m12s and 1.33 GB at 64:8. NIST
|
||||
CTC 03 showed no measurable conversion benefit from LTO.
|
||||
|
||||
The importer is split into schema-neutral common build units for session and
|
||||
cache handling, document traversal, job scheduling, geometric sets, topology,
|
||||
periodic bands, pullback, seam repair, loop repair, validation, and
|
||||
finalization. Schema plugins consume those objects directly, preserving
|
||||
static Factory registration without a new runtime ABI. A local geometry
|
||||
repair edit therefore recompiles its topical unit and relinks only requested
|
||||
plugins. Revisit these boundaries only in response to a measured dependency,
|
||||
ownership, or rebuild problem.
|
||||
|
||||
Default policy and loss accounting
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The user-facing default is deliberately permissive:
|
||||
|
||||
* safe topology repair is enabled;
|
||||
* exact and strict modes are disabled;
|
||||
* storage-safe invalid BReps are preserved and explicitly tagged;
|
||||
* useful partial output is published with status 1 and a structured report;
|
||||
* --strict is transactional; and
|
||||
* --reject-invalid-objs restores invalid-BRep rejection.
|
||||
|
||||
Recovery order is source-faithful construction, bounded safe repair, tagged
|
||||
modeling inference for a proven source contradiction, marked storage-safe
|
||||
invalid fallback, and finally omission. Scale-bounded inference is attempted
|
||||
only in a detached whole-item retry after the ordinary candidate fails. It is
|
||||
committed only when either:
|
||||
|
||||
* the rebuilt BRep passes normal structural and solidness validation; or
|
||||
* permissive invalid preservation proves that a structurally safe open result
|
||||
corresponds one-for-one to source EDGE_CURVEs with exactly one authored
|
||||
ORIENTED_EDGE use. Such a result remains invalid and is tagged
|
||||
`step:invalidity=authored_open_topology`; it is never presented as a solid.
|
||||
|
||||
Inference is disabled by exact, strict, no-repair, and reject-invalid policy.
|
||||
Reports and affected database objects retain source edge identifiers,
|
||||
discrepancy, safe and inference limits, declared tolerance, inference kind,
|
||||
and explanatory detail. Rejected inference transactions do not contaminate
|
||||
ordinary results or diagnostics.
|
||||
|
||||
Every product-bound representation, representation item, selected BRL-CAD
|
||||
object, and reachable child receives a handled, intentionally non-geometric,
|
||||
skipped, malformed, unsupported, or failed disposition. Import and export
|
||||
distinguish complete, partial, empty, and failed outcomes. The pre-conversion
|
||||
entity census describes the input and does not imply conversion support.
|
||||
|
||||
A preserved invalid BRep is not, by itself, proof of bad authoring. Unless an
|
||||
independent topology proof establishes a source contradiction, it remains an
|
||||
unresolved converter result tagged `step:geometry_status=invalid_preserved`
|
||||
and `step:source_validity=unresolved`. It forces a partial result and remains
|
||||
in the failure ratchet.
|
||||
|
||||
Geometry
|
||||
~~~~~~~~
|
||||
|
||||
The following geometry paths are COMPLETE for their documented subsets:
|
||||
|
||||
* rigid product hierarchies, repeated occurrences, transforms, and shared
|
||||
product/occurrence emission in every supported schema;
|
||||
* optional native CSG import/export in AP203e2, AP214, and AP242, including
|
||||
supported Boolean trees, exact mapped primitives, BRep leaves, transforms,
|
||||
and RECTANGULAR_PYRAMID;
|
||||
* exact compatible extruded and revolved area/face solids and linear or
|
||||
circular SWEPT_DISK_SOLID in AP203e2, AP214, and AP242;
|
||||
* AP242 triangulated faces, simple/complex surface sets, shells, and solids,
|
||||
including strips, fans, pnindex, normals, welding, closure, and winding;
|
||||
* analytic and spline BRep import, periodic and singular topology, voids,
|
||||
bounded repair, transactional inference, and explicit invalid fallback;
|
||||
* solid-BRep export/re-import for sphere, cylinder, cone, torus, box, open
|
||||
NURBS faces with voids, rational Cobb surfaces, and BREP_WITH_VOIDS;
|
||||
* mated librt BRep fallback for EHY, EPA, HYP, PART, PIPE, EXTRUDE, REVOLVE,
|
||||
solid BOT, closed ARS, and supported NMG input;
|
||||
* sketch recovery for reordered or reversed segments, zero-length segments,
|
||||
inner loops, lines, circles, arcs, Beziers, and rational/non-rational NURBS;
|
||||
and
|
||||
* mixed GEOMETRIC_SET curves, points, and CURVE_BOUNDED_SURFACEs with partial
|
||||
set loss accounting and shared bounded pullback validation.
|
||||
|
||||
The detailed native-CSG primitive and operator matrix is maintained in
|
||||
src/conv/step/README.csg.md. AP203e1 has no native-CSG path; non-union AP203e1
|
||||
trees are BLOCKED on reliable Boolean evaluation.
|
||||
|
||||
STEP B-spline curves and surfaces share exact conversion from EXPRESS full knot
|
||||
vectors to OpenNURBS endpoint-truncated storage. Valid unclamped closed
|
||||
surfaces are clamped and only numerically equal seam controls are normalized,
|
||||
subject to dense before/after audits.
|
||||
|
||||
Presentation, units, configuration, and PMI
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
AP203e2, AP214, and AP242 share colour, transparency, style, layer, material,
|
||||
property, validation-value, and recursive unit-graph extraction. Export
|
||||
round-trips product identity and revision, presentation, multiple materials,
|
||||
typed properties, known SI and exact conversion units, derived units,
|
||||
occurrence labels, and reference designators. AP203e1 explicitly reports
|
||||
presentation or property requests it cannot author.
|
||||
|
||||
One constrained safe-mode repair handles an identified producer defect where
|
||||
a positive density's complete unit graph is mass^1 times length^3 instead of
|
||||
mass^1 times length^-3. The value is retained, the denominator dimension is
|
||||
corrected, provenance is recorded, and exact, strict, and no-repair modes
|
||||
reject the graph. This is not a general unit-guessing rule.
|
||||
|
||||
--output-units, --angle-units, and --uncertainty form one schema-neutral export
|
||||
policy covering BReps, native CSG, cone angles, and rigid occurrence
|
||||
translations.
|
||||
|
||||
Configuration data is retained as a typed reference graph with exact Part 21
|
||||
right-hand sides. Cross-schema authoring covers approvals, provenance,
|
||||
assignments, effectivity, alternatives, substitutes, categories, people,
|
||||
organizations, addresses, security classifications, documents, date/time
|
||||
chains, and their legal schema-specific assignment forms.
|
||||
|
||||
DATUM supports point, line, plane, frame, and bounded-target forms, semantic
|
||||
roles, axes, dimensions, identifiers, validation, transforms, and versioned
|
||||
legacy-compatible database storage. ANNOT supports model-space planes,
|
||||
per-segment roles and styles, UTF-8 text, runtime TrueType outlines,
|
||||
transforms, and legacy screen-space compatibility. The hardened private
|
||||
struetype code is shared from libbu; LGPL OSIFont remains runtime data and is
|
||||
not compiled into a binary.
|
||||
|
||||
AP242 PMI stage 1 retains connected semantic, presentation, association, and
|
||||
dependency records with source values, component order, references, product
|
||||
ownership, and native disposition. Exactly resolved point, axis, and plane
|
||||
datums become DATUM objects. Supported graphical content becomes model-space
|
||||
ANNOT geometry; ambiguous content remains semantic-only. Malformed
|
||||
presentation is partial in permissive mode and transactional in strict mode.
|
||||
|
||||
Safety and test infrastructure
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
BRL-CAD and STEPcode support ASan+UBSan and a separate TSan mode. Release and
|
||||
ASan+UBSan have covered the complete STEP/AP range; focused parallel, failure,
|
||||
native-CSG, and BRep-export paths have run under TSan. On the qualification
|
||||
host TSan must run through `setarch x86_64 -R` because its runtime otherwise
|
||||
fails before main. Repeated -j8 versus -j1 output comparison remains part of
|
||||
the determinism gate.
|
||||
|
||||
|
||||
Validation Snapshot
|
||||
-------------------
|
||||
|
||||
In-tree tests include success, malformed, partial, strict, round-trip,
|
||||
sanitizer, and output-validity cases across every supported schema. The last
|
||||
complete focused Release STEP matrix contains 197 tests. Official NIST MBE PMI
|
||||
files are stored under db/nist with documented provenance. Five AP203 MIM
|
||||
files exercise AP203e2, and 17 AP242 files span editions 1 through 4.
|
||||
|
||||
Checksum-pinned, non-redistributed STEP Tools and STEPcode samples cover CATIA,
|
||||
Unigraphics, AutoCAD, I-DEAS, STEPnet, and STEP Tools producers. Important
|
||||
independent modern-schema evidence currently includes:
|
||||
|
||||
* AP242 Leopard2-A6: 31/31 geometry items, 14 products, 338 occurrences,
|
||||
2,425 styles, 13 material identities, and 26 material values;
|
||||
* AP242 Open Source Rover: six product levels, 713 products, 1,390 rigid
|
||||
occurrences, and 569 applied styles; and
|
||||
* AP203e2 ROTATOR_ASM: eight product levels, 126 participating products, 404
|
||||
usages, 671 product-property records, and 3,991 styles.
|
||||
|
||||
The last complete unified corpus baseline is the 1,138-file dry run at
|
||||
`.build/step-corpus-full-j8-validity-ladder-20260804-post-speaker`:
|
||||
|
||||
* schemas: 616 AP203, 375 AP214, four AP203e2, and 143 AP242 files;
|
||||
* files: 1,057 clean; process statuses were 1,064 complete, 69 useful partial,
|
||||
three intentionally empty, one failed, and one SIGSEGV;
|
||||
* geometry: 41,765 of 41,992 attempted items written (99.46 percent), 221 exact
|
||||
omissions, and six unsupported-representation omissions;
|
||||
* invalid fallback: 386 structurally invalid but storage-safe BReps preserved,
|
||||
none rejected;
|
||||
* aggregate wall/conversion time: 36,796.2/34,678.9 seconds; and
|
||||
* maximum per-file RSS: 3.62 GB.
|
||||
|
||||
This remains the comparison baseline, not the current expected result. The
|
||||
crash and several omission groups have since been fixed in selected working
|
||||
runs. Its timing delta from the preceding baseline is not a controlled
|
||||
regression result because host load and available memory differed materially.
|
||||
Preserve both manifests and compare future runs only under similar load.
|
||||
|
||||
The corpus runner records hashes, schemas, converter/plugin/shared-library
|
||||
identities, reports, logs, wall time, peak RSS, host load, memory availability,
|
||||
geometry outcomes, hierarchy depth, metadata, and failure categories. Dry
|
||||
mode still parses, constructs, validates, and repairs geometry; it omits only
|
||||
database serialization. Historical `.g` files are comparative artifacts, not
|
||||
correctness oracles, and must remain intact.
|
||||
|
||||
Selected post-baseline qualifications establish:
|
||||
|
||||
* cargo ship now terminates normally; its two remaining selected omissions are
|
||||
explicit missing required FACE_SURFACE references;
|
||||
* hAVYARD now terminates normally after rejected seam transactions reacquire
|
||||
topology owned by the restored BRep; a direct owned lifetime fixture remains
|
||||
to be added;
|
||||
* SNAP #801290 is a valid solid after a face-scoped horn-torus keyhole repair;
|
||||
* all ten historical T90 omissions serialize: six valid solids, two
|
||||
source-open BReps tagged `authored_open_topology`, and two storage-safe
|
||||
partial fallbacks whose contradictions exceed bounded inference limits;
|
||||
* paired 135-file AP203/AP242 Recurta runs write all 139 attempted items per
|
||||
schema as valid BReps, with three tagged inferences per schema; and
|
||||
* the selected 421-face speaker solid now produces a validated 419-face solid
|
||||
through a whole-item shared-spur transaction.
|
||||
|
||||
The T90 serialized selection is recorded in
|
||||
`.build/t90_omissions_final_write.json`; it attempted and wrote ten of ten,
|
||||
skipped none, and explicitly preserved four invalid objects. The file-level
|
||||
partial status is intentional and does not represent an omission.
|
||||
|
||||
Two measured performance targets remain useful:
|
||||
|
||||
* Recurta `10012.step`, 992 faces: 78.7 seconds in the final qualification;
|
||||
profiling attributes most time to periodic trim-orientation repair and
|
||||
repeated transaction work, not preflight construction; and
|
||||
* chess king, 13,234 faces: about 616.8 seconds and 2.92 GB in its selected
|
||||
clean run.
|
||||
|
||||
|
||||
Remaining Work in Recommended Order
|
||||
-----------------------------------
|
||||
|
||||
1. Close the current corpus ratchet -- ACTIVE
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Complete selected work before starting another expensive unified run:
|
||||
|
||||
1. Add a compact rejected-seam-transaction lifetime fixture, qualify it under
|
||||
ASan+UBSan, and retain the complete hAVYARD real-file check.
|
||||
2. Sample and reduce the largest post-baseline omission sets:
|
||||
|
||||
* Garrett: 49;
|
||||
* 2-litre engine: 49, including 48 GEOMETRIC_SET items;
|
||||
* Open Source Rover: 16;
|
||||
* Epoxy granite VMC: seven; and
|
||||
* BLANCO: five.
|
||||
|
||||
3. Retest the former 58-omission GEOMETRIC_SET group with the completed bounded
|
||||
surface path. The independent b/c/d compatibility files are already
|
||||
qualified except for six d-wireframe boundary/pcurve disagreements.
|
||||
4. Reduce the Leopard2-A6 export/re-import failure at STEP edge #460691. One
|
||||
of 31 exported BReps fails while splitting a pcurve whose candidate
|
||||
fragment is 36.4476 model units from its subedge. This is a geometry
|
||||
interoperability problem, not schema selection or assembly loss.
|
||||
5. Retain MOUSE as a source edge/surface mismatch unless independent geometry
|
||||
proves a narrower correction.
|
||||
6. Revisit T90 #20240 and #20307 only if independent geometry supports a
|
||||
narrower reconstruction. Their marked partial results are preferable to
|
||||
inventing a false solid.
|
||||
7. Remove repeated full-BRep scans and copies from large periodic repair
|
||||
transactions while preserving exact rollback behavior.
|
||||
8. Run focused Release, ASan+UBSan, TSan where applicable, and -j1/-j8
|
||||
determinism gates. Then run the complete 1,138-file qualification.
|
||||
|
||||
Corpus fixes require a compact reproducer, permissive and strict assertions,
|
||||
structured report coverage, and a selected real-file check. Do not introduce
|
||||
broad repair heuristics solely to improve corpus percentages.
|
||||
|
||||
The next unified run is acceptable only if every file reaches a normal terminal
|
||||
report; there is no crash, hang, schema-load failure, corrupt output, or
|
||||
unexplained zero-geometry result; and all omissions and invalid fallbacks are
|
||||
grouped by owned root cause.
|
||||
|
||||
2. Expand independent AP203e2/AP242 evidence -- REMAINING
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Locate and pin independent examples for:
|
||||
|
||||
* AP203e2 material and property association variants;
|
||||
* AP242 materials beyond the Inventor MAKE_FROM_USAGE_OPTION pattern;
|
||||
* mixed representation contexts and large multiple-product files; and
|
||||
* AP203e2/AP242 sweeps, NURBS, shells, voids, and mixed BRep/CSG assemblies.
|
||||
|
||||
Promote small distributable reductions into in-tree fixtures. For external
|
||||
inputs record source, producer, SHA-256, and structured expectations. A
|
||||
BRL-CAD-authored round trip alone cannot close this item.
|
||||
|
||||
3. Complete AP242 semantic PMI and MBD -- REMAINING
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Stage 1 graph retention and exact native point/axis/plane/curve creation are
|
||||
complete. Remaining interpretation and authoring work is to:
|
||||
|
||||
* normalize dimension type/value, tolerance type and magnitude,
|
||||
material-condition modifiers, datum-reference compartments, and zones;
|
||||
* preserve stable product, occurrence, face, edge, and vertex target links;
|
||||
* distinguish semantic-only, graphical-only, and validated combined PMI;
|
||||
* map representable text, leaders, extension/dimension lines, arrowheads,
|
||||
fills, area targets, and supported symbols;
|
||||
* explicitly report graphical content which cannot be mapped faithfully;
|
||||
* author retained semantic and supported presentation records in AP242; and
|
||||
* qualify against NIST PMI and independent AP242 files, not only hand-authored
|
||||
fixtures.
|
||||
|
||||
An ANNOT outline is not proof that original text or tolerance semantics were
|
||||
recovered. Do not infer missing semantics from graphical similarity.
|
||||
|
||||
4. Complete tessellated coverage and optional export -- REMAINING
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Add faithful mappings for tessellated point, curve, wire, geometric-set, and
|
||||
repositioned-item forms. Add explicit optional AP242 BOT export using the
|
||||
shared unit and uncertainty policy. Tessellation must not silently replace
|
||||
the eventual evaluated-BRep default for analytic solids.
|
||||
|
||||
5. Broaden existing-BRep interoperability -- REMAINING
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Reduce the six d-wireframe boundary/pcurve disagreements and independently
|
||||
classify each as an authored tolerance violation or adapter defect. Preserve
|
||||
faithful partial-set output until that proof exists; do not stretch pcurves or
|
||||
widen model uncertainty merely to reach complete counts.
|
||||
|
||||
Expand independent-producer coverage for rational and non-rational NURBS,
|
||||
open shells, nested BREP_WITH_VOIDS, reversed proxies/faces, difficult trims,
|
||||
periodic and singular surfaces, and closed curves. Define a legal policy for
|
||||
one BRL-CAD object containing disconnected material volumes. Add wireframe
|
||||
and surface-curve export for databases without face topology.
|
||||
|
||||
Verification must include STEPcode readback, strict BRL-CAD re-import,
|
||||
topology and material-ray comparison, and an independent STEP validator or
|
||||
consumer where available. Textual identity is insufficient.
|
||||
|
||||
Sketch containment still needs a complete loop hierarchy for disjoint outer
|
||||
regions and nested material islands. REVOLVE should split profiles at sharp
|
||||
C0/tangent discontinuities when needed to avoid pathological wireframe and CDT
|
||||
refinement.
|
||||
|
||||
DSP, deprecated HF, EBM, and VOL are a separate scalability and fidelity
|
||||
project. They require bounded-error patching or faithful faceting, discrete
|
||||
boundary simplification, source-semantic ray/material comparisons, and hard
|
||||
validity, memory, and complexity limits. A single high-degree DSP surface or
|
||||
unbounded voxel-to-BRep expansion is not an acceptable general solution.
|
||||
|
||||
6. Complete product and configuration mappings -- REMAINING
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Define scoped mappings and loss policy for region identifiers,
|
||||
material/shader strings, and selected arbitrary BRL-CAD attributes. Do not
|
||||
conflate opaque attributes with typed STEP properties.
|
||||
|
||||
Broaden normalized named-unit conveniences only where schemas agree; the
|
||||
recursive retained graph remains authoritative. Extend authoring for
|
||||
remaining configuration identities, relationships, complex addresses, and
|
||||
application edges only when required source values exist. Expand
|
||||
import-export-import tests to multiple products, occurrences, shared complex
|
||||
effectivities, and schema-specific graphs.
|
||||
|
||||
7. Resolve assembly and native-CSG edge cases -- REMAINING
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Define explicit policy for scale, mirror, and future non-rigid occurrence
|
||||
transforms. Preserve or explicitly report nested combination identity inside
|
||||
one CSG expression; STEP CSG_SELECT cannot directly use another CSG_SOLID as
|
||||
an operand. Expand stable naming and independent multi-level mixed BRep/CSG
|
||||
evidence across AP203e2, AP214, and AP242.
|
||||
|
||||
NOT and guard/no-op Boolean nodes have no lossless finite STEP CSG encoding.
|
||||
AP242 CYCLIDE_SEGMENT_SOLID remains unsupported because recognizing a faithful
|
||||
BRL-CAD PIPE subset requires additional geometric semantics; similar
|
||||
dimensions alone are insufficient.
|
||||
|
||||
8. Implement remaining exact sweeps -- REMAINING
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Add nonlinear swept-disk directrices only when OpenNURBS can construct a
|
||||
faithful pipe with validated frame continuity and self-intersection handling.
|
||||
Ellipse, spline, and composite-curve cases remain. Implement drafted and
|
||||
trimmed face sweeps only after referenced termination, offset, trim intent,
|
||||
and per-edge draft can be evaluated exactly. Inherited depth or angle is not
|
||||
a faithful fallback.
|
||||
|
||||
|
||||
Blocked and Deferred Work
|
||||
-------------------------
|
||||
|
||||
Evaluated-BRep default export -- BLOCKED
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The broadly interoperable default should eventually evaluate arbitrary
|
||||
Boolean trees into final BReps. Current Boolean evaluation is not reliable
|
||||
enough. Continue evaluator work and validation; when it is ready, make
|
||||
evaluated BRep the default without weakening optional exact native CSG.
|
||||
|
||||
STEPcode upstreaming -- DEFERRED maintenance
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The generated-binding scalability work required by step-g and g-step is
|
||||
complete. Remaining STEPcode work is upstream preparation and maintenance:
|
||||
retain API v1 compatibility where required, and address correctness, cache,
|
||||
generation, or resource defects when measured. Reopen generator scalability
|
||||
only for a demonstrated regression or a new schema exceeding current limits.
|
||||
|
||||
Additional application protocols -- DEFERRED
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Do not build every ISO SMRL schema speculatively. AP224 machining features and
|
||||
AP238 STEP-NC are the strongest future geometry/manufacturing candidates once
|
||||
representative inputs and mapping goals exist. AP209 analysis and AP210
|
||||
electronics require domain-specific semantics. AP232 and AP239 are primarily
|
||||
metadata protocols and are not geometry targets merely because they share
|
||||
integrated resources.
|
||||
|
||||
ISO SMRL and historical STEPmod selection, provenance, and redistribution
|
||||
policy are documented in STEP(5).
|
||||
|
||||
|
||||
Definition of Done
|
||||
------------------
|
||||
|
||||
Every new decoder, emitter, or repair must include, as applicable:
|
||||
|
||||
1. an independent hand-authored or external fixture;
|
||||
2. a supported case and a malformed or explicitly unsupported case;
|
||||
3. permissive and transactional strict assertions;
|
||||
4. structured report checks proving exhaustive loss accounting;
|
||||
5. schema-specific coverage where legal alternatives or physical layouts
|
||||
differ;
|
||||
6. round-trip geometry, topology, semantic, and unit checks;
|
||||
7. Release, sanitizer, and deterministic parallel tests;
|
||||
8. selected corpus reruns for every affected real file; and
|
||||
9. matching step-g, g-step, STEP(5), CSG matrix, and plan documentation.
|
||||
|
||||
The overall STEP effort is broadly robust when:
|
||||
|
||||
* every input and selected output object is exhaustively accounted for;
|
||||
* the unified corpus has no crash, hang, schema-load failure, corrupt output,
|
||||
or unexplained zero-geometry result;
|
||||
* AP203e2 and AP242 have independent producer evidence comparable to the
|
||||
relevant AP203/AP214 baseline;
|
||||
* supported tessellation, rigid assemblies, presentation, configuration, and
|
||||
PMI round-trip with explicit losses;
|
||||
* existing BReps interoperate across analytic, NURBS, periodic, singular,
|
||||
shell, void, orientation, and trim cases with independent consumers;
|
||||
* native CSG remains optional, exact, and explicit about unsupported content;
|
||||
* parallel, sanitizer, deterministic-output, performance, and memory gates
|
||||
remain green; and
|
||||
* reliable Boolean evaluation enables evaluated-BRep default export without
|
||||
regressing native CSG.
|
||||
@@ -53,30 +53,41 @@ endif(BRLCAD_ENABLE_TCL)
|
||||
# Archer plugins
|
||||
set(
|
||||
archer_utility_FILES
|
||||
plugins/Utility/attrGroupsDisplayUtilityP.tcl
|
||||
plugins/Utility/botUtilityP.tcl
|
||||
plugins/Utility/lodUtilityP.tcl
|
||||
plugins/Utility/README
|
||||
plugins/Utility/attrGroupsDisplayUtilityP.tcl
|
||||
plugins/Utility/attrGroupsDisplayUtilityP/AttrGroupsDisplayUtilityP.tcl
|
||||
plugins/Utility/botUtilityP.tcl
|
||||
plugins/Utility/botUtilityP/BotUtilityP.tcl
|
||||
plugins/Utility/lodUtilityP.tcl
|
||||
plugins/Utility/lodUtilityP/LODUtilityP.tcl
|
||||
)
|
||||
set(archer_wizard_FILES
|
||||
plugins/Wizards/humanwizard.tcl
|
||||
plugins/Wizards/humanwizard/HumanWizard.tcl
|
||||
plugins/Wizards/tankwizard.tcl
|
||||
plugins/Wizards/tankwizard/TankWizard.tcl
|
||||
plugins/Wizards/tankwizard/images/tank.png
|
||||
plugins/Wizards/tirewizard.tcl
|
||||
plugins/Wizards/tirewizard/TireWizard.tcl
|
||||
)
|
||||
set(archer_wizard_FILES plugins/Wizards/humanwizard.tcl plugins/Wizards/tankwizard.tcl plugins/Wizards/tirewizard.tcl)
|
||||
|
||||
if(BRLCAD_ENABLE_TCL)
|
||||
brlcad_adddata(archer_utility_FILES plugins/archer/Utility)
|
||||
brlcad_adddata(archer_wizard_FILES plugins/archer/Wizards)
|
||||
# Archer scans only the plugin category directories for registration scripts.
|
||||
# Keep class definitions in their subdirectories so they are not sourced twice.
|
||||
foreach(plugin_file IN LISTS archer_utility_FILES archer_wizard_FILES)
|
||||
get_filename_component(plugin_dir "${plugin_file}" DIRECTORY)
|
||||
file(RELATIVE_PATH plugin_dir "${CMAKE_CURRENT_SOURCE_DIR}/plugins" "${CMAKE_CURRENT_SOURCE_DIR}/${plugin_dir}")
|
||||
brlcad_adddata("${plugin_file}" "plugins/archer/${plugin_dir}")
|
||||
endforeach()
|
||||
brlcad_adddata(plugins/Commands/README plugins/archer/Command)
|
||||
brlcad_adddata(plugins/Core/README plugins/archer/Core)
|
||||
brlcad_adddata(plugins/Utility/attrGroupsDisplayUtilityP/AttrGroupsDisplayUtilityP.tcl plugins/archer/Utility/attrGroupsDisplayUtilityP)
|
||||
brlcad_adddata(plugins/Utility/botUtilityP/BotUtilityP.tcl plugins/archer/Utility/botUtilityP)
|
||||
brlcad_adddata(plugins/Utility/lodUtilityP/LODUtilityP.tcl plugins/archer/Utility/lodUtilityP)
|
||||
brlcad_adddata(plugins/Wizards/humanwizard/HumanWizard.tcl plugins/archer/Wizards/humanwizard)
|
||||
brlcad_adddata(plugins/Wizards/tankwizard/TankWizard.tcl plugins/archer/Wizards/tankwizard)
|
||||
brlcad_adddata(plugins/Wizards/tankwizard/images/tank.png plugins/archer/Wizards/tankwizardIA/images)
|
||||
brlcad_adddata(plugins/Wizards/tirewizard/TireWizard.tcl plugins/archer/Wizards/tirewizard)
|
||||
endif(BRLCAD_ENABLE_TCL)
|
||||
|
||||
set(
|
||||
archer_ignore_files
|
||||
CMakeLists.txt
|
||||
plugins/Commands/README
|
||||
plugins/Core/README
|
||||
${archer_utility_FILES}
|
||||
${archer_wizard_FILES}
|
||||
${archer_txt}
|
||||
|
||||
@@ -109,6 +109,16 @@ brlcad_addexec(g-var g-var.c "librt;libbu;${M_LIBRARY}" FOLDER Conv)
|
||||
brlcad_addexec(g-vrml g-vrml.c "librt;libnmg;libbu;${M_LIBRARY}" FOLDER Conv)
|
||||
|
||||
brlcad_addexec(g-x3d g-x3d.c "librt;libnmg;libbu;${M_LIBRARY}" FOLDER Conv)
|
||||
# GCC cannot model BU_SETJUMP's longjmp path during LTO and reports an
|
||||
# anonymous value as potentially uninitialized.
|
||||
check_linker_flag(
|
||||
"C"
|
||||
"-Wno-error=maybe-uninitialized"
|
||||
G_X3D_LINKER_WNO_MAYBE_UNINITIALIZED
|
||||
)
|
||||
if(G_X3D_LINKER_WNO_MAYBE_UNINITIALIZED)
|
||||
target_link_options(g-x3d PRIVATE -Wno-maybe-uninitialized)
|
||||
endif()
|
||||
|
||||
brlcad_addexec(nastran-g nastran-g.c "libwdb;librt;libnmg;libbu;${M_LIBRARY}" FOLDER Conv)
|
||||
|
||||
|
||||
@@ -326,7 +326,7 @@ SHPOpenLL( const char * pszLayer, const char * pszAccess, SAHooks *psHooks ) {
|
||||
/* -------------------------------------------------------------------- */
|
||||
/* Initialize the info structure. */
|
||||
/* -------------------------------------------------------------------- */
|
||||
SHPHandle psSHP = STATIC_CAST(SHPHandle, calloc(sizeof(SHPInfo),1));
|
||||
SHPHandle psSHP = STATIC_CAST(SHPHandle, calloc(1,sizeof(SHPInfo)));
|
||||
|
||||
psSHP->bUpdated = FALSE;
|
||||
memcpy( &(psSHP->sHooks), psHooks, sizeof(SAHooks) );
|
||||
@@ -1176,7 +1176,7 @@ SHPCreateObject( int nSHPType, int nShapeId, int nParts,
|
||||
psObject->nParts = MAX(1,nParts);
|
||||
|
||||
psObject->panPartStart = STATIC_CAST(int *,
|
||||
calloc(sizeof(int), psObject->nParts));
|
||||
calloc(psObject->nParts, sizeof(int)));
|
||||
psObject->panPartType = STATIC_CAST(int *,
|
||||
malloc(sizeof(int) * psObject->nParts));
|
||||
|
||||
@@ -1205,13 +1205,13 @@ SHPCreateObject( int nSHPType, int nShapeId, int nParts,
|
||||
{
|
||||
const size_t nSize = sizeof(double) * nVertices;
|
||||
psObject->padfX = STATIC_CAST(double *, padfX ? malloc(nSize) :
|
||||
calloc(sizeof(double),nVertices));
|
||||
calloc(nVertices,sizeof(double)));
|
||||
psObject->padfY = STATIC_CAST(double *, padfY ? malloc(nSize) :
|
||||
calloc(sizeof(double),nVertices));
|
||||
calloc(nVertices,sizeof(double)));
|
||||
psObject->padfZ = STATIC_CAST(double *, padfZ && bHasZ ? malloc(nSize) :
|
||||
calloc(sizeof(double),nVertices));
|
||||
calloc(nVertices,sizeof(double)));
|
||||
psObject->padfM = STATIC_CAST(double *, padfM && bHasM ? malloc(nSize) :
|
||||
calloc(sizeof(double),nVertices));
|
||||
calloc(nVertices,sizeof(double)));
|
||||
if( padfX != SHPLIB_NULLPTR )
|
||||
memcpy(psObject->padfX, padfX, nSize);
|
||||
if( padfY != SHPLIB_NULLPTR )
|
||||
|
||||
@@ -5545,8 +5545,8 @@ repair_ambiguous_singular_periodic_loop_branches(ON_Brep *brep,
|
||||
std::vector<int> affected_positions;
|
||||
affected_positions.reserve(arc_positions.size() + 2);
|
||||
affected_positions.push_back(first_position);
|
||||
affected_positions.insert(affected_positions.end(),
|
||||
arc_positions.begin(), arc_positions.end());
|
||||
for (const int position : arc_positions)
|
||||
affected_positions.push_back(position);
|
||||
affected_positions.push_back(second_position);
|
||||
|
||||
for (int shift_sign = -1; shift_sign <= 1 && !repaired;
|
||||
|
||||
@@ -752,7 +752,7 @@ tessellate_part(struct creo_conv_info *cinfo, ProMdl model, struct bu_vls **snam
|
||||
}
|
||||
|
||||
/* Check solidity */
|
||||
success = bg_trimesh_solid((int)vert_tree->curr_vert, (size_t)faces.size() / 3,
|
||||
success = !bg_trimesh_solid((int)vert_tree->curr_vert, (size_t)faces.size() / 3,
|
||||
vert_tree->the_array, &faces[0], NULL);
|
||||
|
||||
/* If it's not solid and we're testing solidity, keep trying... */
|
||||
|
||||
@@ -23,10 +23,20 @@
|
||||
#include "InformationGatherer.h"
|
||||
#include "bu/log.h"
|
||||
|
||||
// flush results, if we have any, then exit
|
||||
static void reportAndExit(const std::string &message, const std::string &result = "")
|
||||
{
|
||||
if (!result.empty())
|
||||
bu_log("%s%s", result.c_str(), result.back() == '\n' ? "" : "\n");
|
||||
|
||||
bu_exit(BRLCAD_ERROR, "%s", message.c_str());
|
||||
}
|
||||
|
||||
|
||||
std::string getCmdPath(std::string exeDir, const char* cmd) {
|
||||
char buf[MAXPATHLEN] = {0};
|
||||
if (!bu_dir(buf, MAXPATHLEN, exeDir.c_str(), cmd, BU_DIR_EXT, NULL))
|
||||
bu_exit(BRLCAD_ERROR, "Couldn't find %s, aborting.\n", cmd);
|
||||
reportAndExit("Couldn't find " + std::string(cmd) + ", aborting.\n");
|
||||
|
||||
return std::string(buf);
|
||||
}
|
||||
@@ -58,13 +68,13 @@ getSurfaceArea(Options* opt, std::map<std::string, std::string> UNUSED(map), std
|
||||
bu_process_create(&p, rtarea_av, BU_PROCESS_HIDE_WINDOW | BU_PROCESS_OUT_EQ_ERR);
|
||||
|
||||
if (bu_process_pid(p) <= 0) {
|
||||
bu_exit(BRLCAD_ERROR, "Problem with getSurfaceArea, aborting\n");
|
||||
reportAndExit("Problem with getSurfaceArea, aborting\n");
|
||||
}
|
||||
|
||||
char buffer[128];
|
||||
std::string result = "";
|
||||
std::string result;
|
||||
int read_cnt = 0;
|
||||
while ((read_cnt = bu_process_read_n(p, BU_PROCESS_STDOUT, 128-1, buffer)) > 0) {
|
||||
while ((read_cnt = bu_process_read_n(p, BU_PROCESS_STDOUT, sizeof(buffer) - 1, buffer)) > 0) {
|
||||
/* NOTE: read does not ensure null-termination, thus buffersize-1 */
|
||||
buffer[read_cnt] = '\0';
|
||||
result += buffer;
|
||||
@@ -80,7 +90,7 @@ getSurfaceArea(Options* opt, std::map<std::string, std::string> UNUSED(map), std
|
||||
try {
|
||||
surfArea += stod(result);
|
||||
} catch (const std::invalid_argument& ia) {
|
||||
bu_exit(BRLCAD_ERROR, "Invalid argument for getSurfaceArea. Got (%s), aborting.\n", ia.what());
|
||||
reportAndExit("Invalid argument for getSurfaceArea. Got (" + std::string(ia.what()) + "), aborting.\n", result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -200,7 +210,7 @@ getVerificationData(struct ged* UNUSED(g), Options* opt, std::map<std::string, s
|
||||
struct bu_process* p;
|
||||
int read_cnt = 0;
|
||||
char buffer[1024] = {0};
|
||||
std::string result = "";
|
||||
std::string result;
|
||||
std::string ncpu(std::to_string(opt->getNCPU()));
|
||||
std::string dFile(opt->getDensityFile());
|
||||
|
||||
@@ -233,21 +243,23 @@ getVerificationData(struct ged* UNUSED(g), Options* opt, std::map<std::string, s
|
||||
bu_process_create(&p, const_cast<const char**>(gqa_av_vec.data()), BU_PROCESS_HIDE_WINDOW | BU_PROCESS_OUT_EQ_ERR);
|
||||
|
||||
if (bu_process_pid(p) <= 0) {
|
||||
bu_exit(BRLCAD_ERROR, "Problem in getVerificationData gqa process creation, aborting\n");
|
||||
reportAndExit("Problem in getVerificationData gqa process creation, aborting\n");
|
||||
}
|
||||
|
||||
result = "";
|
||||
while ((read_cnt = bu_process_read_n(p, BU_PROCESS_STDOUT, 1024-1, buffer)) > 0) {
|
||||
while ((read_cnt = bu_process_read_n(p, BU_PROCESS_STDOUT, sizeof(buffer) - 1, buffer)) > 0) {
|
||||
buffer[read_cnt] = '\0';
|
||||
result += buffer;
|
||||
}
|
||||
|
||||
if (bu_process_wait_n(&p, 0) != 0) {
|
||||
bu_exit(BRLCAD_ERROR, "Problem collecting gqa volume and mass, aborting\n");
|
||||
const int gqa_status = bu_process_wait_n(&p, 0);
|
||||
|
||||
// sanity - we should have clean status code and not the "no density found" message
|
||||
const bool no_density_data = result.find(no_density_msg) != std::string::npos;
|
||||
if (gqa_status != 0 && (!no_density_data || !dFile.empty())) {
|
||||
reportAndExit("Problem collecting gqa volume and mass, aborting\n", result);
|
||||
}
|
||||
|
||||
// make sure we did not get 'no density file' error message
|
||||
if (result.find(no_density_msg) == std::string::npos) {
|
||||
if (!no_density_data) {
|
||||
// NOTE: tmp_val is zeroed at each parseLabeledNumber call
|
||||
if (parseLabeledNumber(tmp_val, result, volume_label))
|
||||
volume += tmp_val;
|
||||
@@ -263,17 +275,17 @@ getVerificationData(struct ged* UNUSED(g), Options* opt, std::map<std::string, s
|
||||
bu_process_create(&p, const_cast<const char**>(gqa_av_vec.data()), BU_PROCESS_HIDE_WINDOW | BU_PROCESS_OUT_EQ_ERR);
|
||||
|
||||
if (bu_process_pid(p) <= 0) {
|
||||
bu_exit(BRLCAD_ERROR, "Problem in getVerificationData gqa process creation, aborting\n");
|
||||
reportAndExit("Problem in getVerificationData gqa process creation, aborting\n");
|
||||
}
|
||||
|
||||
result = "";
|
||||
while ((read_cnt = bu_process_read_n(p, BU_PROCESS_STDOUT, 1024-1, buffer)) > 0) {
|
||||
result.clear();
|
||||
while ((read_cnt = bu_process_read_n(p, BU_PROCESS_STDOUT, sizeof(buffer) - 1, buffer)) > 0) {
|
||||
buffer[read_cnt] = '\0';
|
||||
result += buffer;
|
||||
}
|
||||
|
||||
if (bu_process_wait_n(&p, 0) != 0) {
|
||||
bu_exit(BRLCAD_ERROR, "Problem collecting gqa volume, aborting\n");
|
||||
reportAndExit("Problem collecting gqa volume, aborting\n", result);
|
||||
}
|
||||
|
||||
if (parseLabeledNumber(tmp_val, result, volume_label))
|
||||
@@ -381,7 +393,7 @@ InformationGatherer::getMainComp()
|
||||
ged_exec_exists(g, 2, cmd);
|
||||
std::string res = bu_vls_addr(g->ged_result_str);
|
||||
if (res != "1") {
|
||||
bu_exit(BRLCAD_ERROR, "Could not find component (%s), aborting.\n", topname);
|
||||
reportAndExit("Could not find component (" + topcomp + "), aborting.\n");
|
||||
}
|
||||
|
||||
int entities = getNumEntities(opt->getTopComp());
|
||||
|
||||
@@ -34,6 +34,9 @@
|
||||
|
||||
// OpenCV header files
|
||||
#include <opencv2/opencv.hpp>
|
||||
#if CV_VERSION_MAJOR >= 5
|
||||
#include <opencv2/geometry.hpp>
|
||||
#endif
|
||||
#include <opencv2/core/core.hpp>
|
||||
#include "opencv2/highgui/highgui.hpp"
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ main(int argc, char *argv[])
|
||||
int i, j;
|
||||
int db_index;
|
||||
int c;
|
||||
int ret;
|
||||
const char **av;
|
||||
struct ged *gedp;
|
||||
|
||||
@@ -103,14 +104,14 @@ main(int argc, char *argv[])
|
||||
bu_exit(1, usage, argv[0]);
|
||||
}
|
||||
|
||||
(void)ged_exec_gqa(gedp, j, av);
|
||||
ret = ged_exec_gqa(gedp, j, av);
|
||||
if (bu_vls_strlen(gedp->ged_result_str) > 0)
|
||||
bu_log("%s", bu_vls_addr(gedp->ged_result_str));
|
||||
ged_close(gedp);
|
||||
|
||||
bu_free((void *)av, "av");
|
||||
|
||||
return 0;
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ set(
|
||||
api.c
|
||||
centroid.c
|
||||
check_options.c
|
||||
contour.cpp
|
||||
density.cpp
|
||||
globals.c
|
||||
inside.c
|
||||
@@ -68,7 +69,7 @@ set(
|
||||
cmakefiles(${libanalyze_ignored_files})
|
||||
|
||||
# Note - libanalyze_deps is defined by ${BRLCAD_SOURCE_DIR}/src/source_dirs.cmake
|
||||
brlcad_addlib(libanalyze "${LIBANALYZE_SOURCES}" "" "${CMAKE_CURRENT_BINARY_DIR}"
|
||||
brlcad_addlib(libanalyze "${LIBANALYZE_SOURCES}" "" "${CMAKE_CURRENT_BINARY_DIR};${EIGEN3_INCLUDE_DIR}"
|
||||
PUBLIC_LIBS ${libanalyze_deps}
|
||||
)
|
||||
set_target_properties(libanalyze PROPERTIES VERSION 20.0.1 SOVERSION 20)
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
brlcad_addexec(analyze_contour contour.cpp "libanalyze;libwdb;librt;libbg;libbu" TEST)
|
||||
brlcad_add_test(NAME analyze_contour COMMAND analyze_contour)
|
||||
|
||||
brlcad_addexec(analyze_raydiff raydiff.c "libanalyze;librt;libbu" TEST)
|
||||
brlcad_addexec(analyze_sp solid_partitions.c "libanalyze;librt;libbu" TEST)
|
||||
brlcad_addexec(analyze_nhit nhit.cpp "libanalyze;librt;libbu" TEST_USESDATA)
|
||||
@@ -13,6 +16,7 @@ brlcad_add_test(NAME analyze_densities_std COMMAND analyze_densities std)
|
||||
cmakefiles(
|
||||
CMakeLists.txt
|
||||
arbs.g
|
||||
contour.cpp
|
||||
raydiff.g
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,425 @@
|
||||
/* A N A L Y Z E _ C O N T O U R . C P P
|
||||
* BRL-CAD
|
||||
*
|
||||
* Copyright (c) 2026 United States Government as represented by
|
||||
* the U.S. Army Research Laboratory.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are
|
||||
* met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED.
|
||||
*/
|
||||
/** @file contour.cpp
|
||||
*
|
||||
* Basic topology and resource-limit tests for analyze_mdc().
|
||||
*/
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <climits>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <set>
|
||||
#include <utility>
|
||||
|
||||
#include "bu/app.h"
|
||||
#include "bu/malloc.h"
|
||||
#include "bg/trimesh.h"
|
||||
#include "analyze/contour.h"
|
||||
#include "analyze/polygonize.h"
|
||||
#include "raytrace.h"
|
||||
#include "wdb.h"
|
||||
|
||||
static constexpr double METRIC_RELATIVE_TOLERANCE = 0.10;
|
||||
|
||||
static int failures = 0;
|
||||
|
||||
|
||||
static void
|
||||
expect(bool condition, const char *message)
|
||||
{
|
||||
if (condition)
|
||||
return;
|
||||
|
||||
std::fprintf(stderr, "FAIL: %s\n", message);
|
||||
failures++;
|
||||
}
|
||||
|
||||
|
||||
static void
|
||||
expect_metric(double actual, double expected, const char *object,
|
||||
const char *metric)
|
||||
{
|
||||
double relative_error = std::fabs(actual - expected) / expected;
|
||||
if (actual >= 0.0 && relative_error <= METRIC_RELATIVE_TOLERANCE)
|
||||
return;
|
||||
|
||||
std::fprintf(stderr,
|
||||
"FAIL: %s %s is %.17g, expected %.17g (relative error %.2f%%)\n",
|
||||
object, metric, actual, expected, 100.0 * relative_error);
|
||||
failures++;
|
||||
}
|
||||
|
||||
|
||||
static void
|
||||
check_mesh(struct db_i *dbip, const char *object, int expected_components = 1,
|
||||
double expected_area = 0.0, double expected_volume = 0.0,
|
||||
size_t minimum_faces = 4, int expected_euler = INT_MAX)
|
||||
{
|
||||
int *faces = NULL;
|
||||
point_t *vertices = NULL;
|
||||
size_t face_count = 0;
|
||||
size_t vertex_count = 0;
|
||||
struct analyze_mdc_params params = ANALYZE_MDC_PARAMS_DEFAULT;
|
||||
params.min_depth = 4;
|
||||
params.max_depth = 8;
|
||||
params.max_rays = 0;
|
||||
params.minimum_free_mem = 0;
|
||||
params.max_time = 0;
|
||||
params.verbosity = 0;
|
||||
|
||||
enum analyze_mdc_status status = analyze_mdc(&faces, &face_count,
|
||||
&vertices, &vertex_count, object, dbip, ¶ms);
|
||||
expect(status == ANALYZE_MDC_OK, object);
|
||||
if (status == ANALYZE_MDC_OK) {
|
||||
expect(vertex_count >= 4, "contour has vertices");
|
||||
expect(face_count >= minimum_faces, "contour has expected resolution");
|
||||
expect(bg_trimesh_solid2(static_cast<int>(vertex_count),
|
||||
static_cast<int>(face_count),
|
||||
reinterpret_cast<fastf_t *>(vertices), faces, NULL) == 0,
|
||||
"contour is a closed oriented manifold");
|
||||
if (expected_euler != INT_MAX) {
|
||||
std::set<std::pair<int, int>> edges;
|
||||
for (size_t face = 0; face < face_count; face++) {
|
||||
for (size_t corner = 0; corner < 3; corner++) {
|
||||
int a = faces[3 * face + corner];
|
||||
int b = faces[3 * face + (corner + 1) % 3];
|
||||
edges.insert(std::minmax(a, b));
|
||||
}
|
||||
}
|
||||
long long euler = static_cast<long long>(vertex_count) -
|
||||
static_cast<long long>(edges.size()) +
|
||||
static_cast<long long>(face_count);
|
||||
expect(euler == expected_euler,
|
||||
"contour has the expected Euler characteristic");
|
||||
}
|
||||
if (expected_area > 0.0)
|
||||
expect_metric(bg_trimesh_area(faces, face_count, vertices,
|
||||
vertex_count), expected_area, object, "surface area");
|
||||
if (expected_volume > 0.0)
|
||||
expect_metric(bg_trimesh_volume(faces, face_count, vertices,
|
||||
vertex_count), expected_volume, object, "volume");
|
||||
|
||||
int *face_indices = NULL;
|
||||
int *component_offsets = NULL;
|
||||
int components = bg_trimesh_separate(&face_indices,
|
||||
&component_offsets, faces, static_cast<int>(face_count));
|
||||
expect(components == expected_components,
|
||||
"contour has the expected connected components");
|
||||
if (face_indices)
|
||||
bu_free(face_indices, "MDC test component faces");
|
||||
if (component_offsets)
|
||||
bu_free(component_offsets, "MDC test component offsets");
|
||||
}
|
||||
|
||||
if (faces)
|
||||
bu_free(faces, "MDC test faces");
|
||||
if (vertices)
|
||||
bu_free(vertices, "MDC test vertices");
|
||||
}
|
||||
|
||||
|
||||
static bool
|
||||
make_region(struct rt_wdb *wdbp, const char *region, const char *solid)
|
||||
{
|
||||
struct wmember member;
|
||||
BU_LIST_INIT(&member.l);
|
||||
if (!mk_addmember(solid, &member.l, NULL, WMOP_UNION))
|
||||
return false;
|
||||
return mk_lcomb(wdbp, region, &member, 1, NULL, NULL, NULL, 0) == 0;
|
||||
}
|
||||
|
||||
|
||||
int
|
||||
main(int UNUSED(argc), const char **argv)
|
||||
{
|
||||
bu_setprogname(argv[0]);
|
||||
|
||||
struct db_i *dbip = db_create_inmem();
|
||||
expect(dbip != NULL, "create in-memory database");
|
||||
if (!dbip)
|
||||
return 1;
|
||||
|
||||
struct rt_wdb *wdbp = wdb_dbopen(dbip, RT_WDB_TYPE_DB_INMEM);
|
||||
expect(wdbp != NULL, "open in-memory database");
|
||||
if (!wdbp) {
|
||||
db_close(dbip);
|
||||
return 1;
|
||||
}
|
||||
|
||||
constexpr double sphere_radius = 10.0;
|
||||
constexpr double torus_major_radius = 10.0;
|
||||
constexpr double torus_minor_radius = 2.5;
|
||||
constexpr double slender_torus_major_radius = 100.0;
|
||||
constexpr double slender_torus_minor_radius = 2.0;
|
||||
constexpr double lower_radius = 1.5;
|
||||
constexpr double upper_radius = 1.0;
|
||||
|
||||
point_t center = VINIT_ZERO;
|
||||
point_t minimum = {-8.0, -6.0, -4.0};
|
||||
point_t maximum = {8.0, 6.0, 4.0};
|
||||
expect(mk_sph(wdbp, "sphere.s", center, sphere_radius) == 0,
|
||||
"create test sphere");
|
||||
expect(mk_rpp(wdbp, "box.s", minimum, maximum) == 0,
|
||||
"create test box");
|
||||
point_t thin_minimum = {-5.0, -2.5, -0.05};
|
||||
point_t thin_maximum = {5.0, 2.5, 0.05};
|
||||
expect(mk_rpp(wdbp, "thin.s", thin_minimum, thin_maximum) == 0,
|
||||
"create thin-wall test box");
|
||||
|
||||
point_t tunnel_minimum = {-10.0, -10.0, -10.0};
|
||||
point_t tunnel_maximum = {10.0, 10.0, 10.0};
|
||||
point_t tunnel_base = {0.0, 0.0, -11.0};
|
||||
vect_t tunnel_height = {0.0, 0.0, 22.0};
|
||||
expect(mk_rpp(wdbp, "tunnel_outer.s", tunnel_minimum,
|
||||
tunnel_maximum) == 0, "create tunnel test outer box");
|
||||
expect(mk_rcc(wdbp, "tunnel_void.s", tunnel_base, tunnel_height,
|
||||
1.0) == 0, "create tunnel test void");
|
||||
struct wmember tunnel_members;
|
||||
BU_LIST_INIT(&tunnel_members.l);
|
||||
expect(mk_addmember("tunnel_outer.s", &tunnel_members.l, NULL,
|
||||
WMOP_UNION) != NULL, "add tunnel outer box");
|
||||
expect(mk_addmember("tunnel_void.s", &tunnel_members.l, NULL,
|
||||
WMOP_SUBTRACT) != NULL, "subtract tunnel void");
|
||||
expect(mk_lcomb(wdbp, "tunnel.r", &tunnel_members, 1, NULL, NULL,
|
||||
NULL, 0) == 0, "create tunnel test region");
|
||||
vect_t normal = {0.0, 0.0, 1.0};
|
||||
expect(mk_tor(wdbp, "torus.s", center, normal, torus_major_radius,
|
||||
torus_minor_radius) == 0,
|
||||
"create test torus");
|
||||
expect(mk_tor(wdbp, "slender_torus.s", center, normal,
|
||||
slender_torus_major_radius, slender_torus_minor_radius) == 0,
|
||||
"create high-aspect torus");
|
||||
|
||||
point_t tgc_base = {-4.0, -3.0, -6.0};
|
||||
vect_t tgc_height = {2.0, 1.0, 12.0};
|
||||
vect_t tgc_a = {5.0, 0.0, 0.0};
|
||||
vect_t tgc_b = {0.0, 3.0, 0.0};
|
||||
vect_t tgc_c = {2.5, 0.0, 0.0};
|
||||
vect_t tgc_d = {0.0, 1.5, 0.0};
|
||||
expect(mk_tgc(wdbp, "tgc.s", tgc_base, tgc_height, tgc_a, tgc_b,
|
||||
tgc_c, tgc_d) == 0, "create test TGC");
|
||||
|
||||
point_t concave_arb[8] = {
|
||||
{500.0, -500.0, -500.0},
|
||||
{500.0, 500.0, -500.0},
|
||||
{500.0, -169.922, -304.688},
|
||||
{500.0, -500.0, 500.0},
|
||||
{400.0, -500.0, -500.0},
|
||||
{400.0, 500.0, -500.0},
|
||||
{400.0, -169.922, -304.688},
|
||||
{400.0, -500.0, 500.0}
|
||||
};
|
||||
expect(mk_arb8(wdbp, "concave.s",
|
||||
reinterpret_cast<const fastf_t *>(concave_arb)) == 0,
|
||||
"create sharp concave ARB");
|
||||
|
||||
point_t lower_center = {-8.0, -5.0, -3.0};
|
||||
point_t upper_center = {9.0, 4.0, 6.0};
|
||||
expect(mk_sph(wdbp, "lower.s", lower_center, lower_radius) == 0,
|
||||
"create lower disconnected sphere");
|
||||
expect(mk_sph(wdbp, "upper.s", upper_center, upper_radius) == 0,
|
||||
"create upper disconnected sphere");
|
||||
struct wmember members;
|
||||
BU_LIST_INIT(&members.l);
|
||||
expect(mk_addmember("lower.s", &members.l, NULL, WMOP_UNION) != NULL,
|
||||
"add lower disconnected sphere");
|
||||
expect(mk_addmember("upper.s", &members.l, NULL, WMOP_UNION) != NULL,
|
||||
"add upper disconnected sphere");
|
||||
expect(mk_lcomb(wdbp, "disconnected.r", &members, 1, NULL, NULL, NULL,
|
||||
0) == 0, "create disconnected region");
|
||||
|
||||
constexpr double overlap_radius = 5.0;
|
||||
constexpr double overlap_offset = 3.0;
|
||||
point_t overlap_left_center = {-overlap_offset, 0.0, 0.0};
|
||||
point_t overlap_right_center = {overlap_offset, 0.0, 0.0};
|
||||
expect(mk_sph(wdbp, "overlap_left.s", overlap_left_center,
|
||||
overlap_radius) == 0, "create left overlapping sphere");
|
||||
expect(mk_sph(wdbp, "overlap_right.s", overlap_right_center,
|
||||
overlap_radius) == 0, "create right overlapping sphere");
|
||||
expect(make_region(wdbp, "overlap_left.r", "overlap_left.s"),
|
||||
"create left overlapping region");
|
||||
expect(make_region(wdbp, "overlap_right.r", "overlap_right.s"),
|
||||
"create right overlapping region");
|
||||
struct wmember overlap_members;
|
||||
BU_LIST_INIT(&overlap_members.l);
|
||||
expect(mk_addmember("overlap_left.r", &overlap_members.l, NULL,
|
||||
WMOP_UNION) != NULL, "add left overlapping region");
|
||||
expect(mk_addmember("overlap_right.r", &overlap_members.l, NULL,
|
||||
WMOP_UNION) != NULL, "add right overlapping region");
|
||||
expect(mk_lcomb(wdbp, "overlap.c", &overlap_members, 0, NULL, NULL,
|
||||
NULL, 0) == 0, "create overlapping-region combination");
|
||||
|
||||
vect_t box_size;
|
||||
VSUB2(box_size, maximum, minimum);
|
||||
|
||||
const double sphere_area = 4.0 * M_PI * sphere_radius * sphere_radius;
|
||||
const double sphere_volume = 4.0 * M_PI * sphere_radius * sphere_radius *
|
||||
sphere_radius / 3.0;
|
||||
const double box_area = 2.0 * (box_size[X] * box_size[Y] +
|
||||
box_size[X] * box_size[Z] + box_size[Y] * box_size[Z]);
|
||||
const double box_volume = box_size[X] * box_size[Y] * box_size[Z];
|
||||
vect_t thin_size;
|
||||
VSUB2(thin_size, thin_maximum, thin_minimum);
|
||||
const double thin_area = 2.0 * (thin_size[X] * thin_size[Y] +
|
||||
thin_size[X] * thin_size[Z] + thin_size[Y] * thin_size[Z]);
|
||||
const double thin_volume =
|
||||
thin_size[X] * thin_size[Y] * thin_size[Z];
|
||||
const double torus_area = 4.0 * M_PI * M_PI * torus_major_radius *
|
||||
torus_minor_radius;
|
||||
const double torus_volume = 2.0 * M_PI * M_PI * torus_major_radius *
|
||||
torus_minor_radius * torus_minor_radius;
|
||||
const double slender_torus_area = 4.0 * M_PI * M_PI *
|
||||
slender_torus_major_radius * slender_torus_minor_radius;
|
||||
const double disconnected_area = 4.0 * M_PI *
|
||||
(lower_radius * lower_radius + upper_radius * upper_radius);
|
||||
const double disconnected_volume = 4.0 * M_PI *
|
||||
(lower_radius * lower_radius * lower_radius +
|
||||
upper_radius * upper_radius * upper_radius) / 3.0;
|
||||
const double overlap_distance = 2.0 * overlap_offset;
|
||||
const double overlap_cap_height = overlap_radius -
|
||||
0.5 * overlap_distance;
|
||||
const double overlap_intersection_volume = M_PI *
|
||||
(4.0 * overlap_radius + overlap_distance) *
|
||||
std::pow(2.0 * overlap_radius - overlap_distance, 2.0) / 12.0;
|
||||
const double overlap_area = 8.0 * M_PI * overlap_radius *
|
||||
overlap_radius - 4.0 * M_PI * overlap_radius * overlap_cap_height;
|
||||
const double overlap_volume = 8.0 * M_PI * overlap_radius *
|
||||
overlap_radius * overlap_radius / 3.0 -
|
||||
overlap_intersection_volume;
|
||||
|
||||
check_mesh(dbip, "sphere.s", 1, sphere_area, sphere_volume);
|
||||
check_mesh(dbip, "box.s", 1, box_area, box_volume);
|
||||
check_mesh(dbip, "thin.s", 1, thin_area, thin_volume, 10000);
|
||||
check_mesh(dbip, "torus.s", 1, torus_area, torus_volume, 4, 0);
|
||||
check_mesh(dbip, "slender_torus.s", 1, slender_torus_area,
|
||||
0.0, 4, 0);
|
||||
check_mesh(dbip, "tunnel.r", 1, 0.0, 0.0, 4, 0);
|
||||
check_mesh(dbip, "tgc.s");
|
||||
check_mesh(dbip, "concave.s");
|
||||
check_mesh(dbip, "disconnected.r", 2, disconnected_area,
|
||||
disconnected_volume);
|
||||
check_mesh(dbip, "overlap.c", 1, overlap_area, overlap_volume);
|
||||
|
||||
{
|
||||
int *coarse_faces = NULL;
|
||||
point_t *coarse_vertices = NULL;
|
||||
size_t coarse_face_count = 0;
|
||||
size_t coarse_vertex_count = 0;
|
||||
struct analyze_mdc_params coarse = ANALYZE_MDC_PARAMS_DEFAULT;
|
||||
coarse.min_depth = 3;
|
||||
coarse.max_depth = 3;
|
||||
coarse.max_rays = 50000;
|
||||
coarse.minimum_free_mem = 0;
|
||||
coarse.max_time = 0;
|
||||
enum analyze_mdc_status coarse_status = analyze_mdc(&coarse_faces,
|
||||
&coarse_face_count, &coarse_vertices, &coarse_vertex_count,
|
||||
"tunnel.r", dbip, &coarse);
|
||||
expect(coarse_status == ANALYZE_MDC_AMBIGUOUS,
|
||||
"under-resolved tunnel requests refinement");
|
||||
expect(!coarse_faces && !coarse_vertices && !coarse_face_count &&
|
||||
coarse_vertex_count == 0,
|
||||
"under-resolved tunnel leaves outputs empty");
|
||||
if (coarse_faces)
|
||||
bu_free(coarse_faces, "coarse MDC test faces");
|
||||
if (coarse_vertices)
|
||||
bu_free(coarse_vertices, "coarse MDC test vertices");
|
||||
}
|
||||
|
||||
{
|
||||
int *compatibility_faces = NULL;
|
||||
point_t *compatibility_vertices = NULL;
|
||||
int compatibility_face_count = 0;
|
||||
int compatibility_vertex_count = 0;
|
||||
point_t unused_seed = VINIT_ZERO;
|
||||
struct analyze_polygonize_params compatibility =
|
||||
ANALYZE_POLYGONIZE_PARAMS_DEFAULT;
|
||||
compatibility.max_time = 0;
|
||||
compatibility.max_cycle_time = 0;
|
||||
compatibility.minimum_free_mem = 0;
|
||||
|
||||
int status = analyze_polygonize(&compatibility_faces,
|
||||
&compatibility_face_count, &compatibility_vertices,
|
||||
&compatibility_vertex_count, 1.0, unused_seed, "sphere.s",
|
||||
dbip, &compatibility);
|
||||
expect(status == 0, "polygonize compatibility interface");
|
||||
if (status == 0) {
|
||||
expect(compatibility_face_count >= 4,
|
||||
"compatibility contour has faces");
|
||||
expect(compatibility_vertex_count >= 4,
|
||||
"compatibility contour has vertices");
|
||||
expect(bg_trimesh_solid2(compatibility_vertex_count,
|
||||
compatibility_face_count,
|
||||
reinterpret_cast<fastf_t *>(compatibility_vertices),
|
||||
compatibility_faces, NULL) == 0,
|
||||
"compatibility contour is a closed oriented manifold");
|
||||
}
|
||||
if (compatibility_faces)
|
||||
bu_free(compatibility_faces, "compatibility test faces");
|
||||
if (compatibility_vertices)
|
||||
bu_free(compatibility_vertices, "compatibility test vertices");
|
||||
|
||||
compatibility.max_time = 1;
|
||||
compatibility.time_offset = 1;
|
||||
expect(analyze_polygonize(&compatibility_faces,
|
||||
&compatibility_face_count, &compatibility_vertices,
|
||||
&compatibility_vertex_count, 1.0, unused_seed, "sphere.s",
|
||||
dbip, &compatibility) == 2,
|
||||
"compatibility interface preserves timeout status");
|
||||
expect(!compatibility_faces && !compatibility_vertices &&
|
||||
!compatibility_face_count && !compatibility_vertex_count,
|
||||
"compatibility timeout leaves outputs empty");
|
||||
}
|
||||
|
||||
int *faces = NULL;
|
||||
point_t *vertices = NULL;
|
||||
size_t face_count = 0;
|
||||
size_t vertex_count = 0;
|
||||
struct analyze_mdc_params limited = ANALYZE_MDC_PARAMS_DEFAULT;
|
||||
limited.min_depth = 4;
|
||||
limited.max_depth = 4;
|
||||
limited.max_rays = 1;
|
||||
limited.minimum_free_mem = 0;
|
||||
expect(analyze_mdc(&faces, &face_count, &vertices, &vertex_count,
|
||||
"sphere.s", dbip, &limited) == ANALYZE_MDC_RAY_LIMIT,
|
||||
"ray limit is enforced during sampling");
|
||||
expect(!faces && !vertices && !face_count && !vertex_count,
|
||||
"failure leaves outputs empty");
|
||||
|
||||
expect(analyze_mdc(NULL, &face_count, &vertices, &vertex_count,
|
||||
"sphere.s", dbip, NULL) == ANALYZE_MDC_INVALID_INPUT,
|
||||
"invalid output pointer is rejected");
|
||||
|
||||
db_close(dbip);
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Local Variables:
|
||||
* mode: C++
|
||||
* tab-width: 8
|
||||
* c-basic-offset: 4
|
||||
* indent-tabs-mode: t
|
||||
* c-file-style: "stroustrup"
|
||||
* End:
|
||||
* ex: shiftwidth=4 tabstop=8
|
||||
*/
|
||||