Compare commits

..

2 Commits

Author SHA1 Message Date
Andrej730 05a3004ef0 test 2025-04-01 19:06:44 +05:00
Andrej730 fe5afaf172 black . 2025-04-01 18:42:47 +05:00
954 changed files with 11929 additions and 30405 deletions
+30
View File
@@ -0,0 +1,30 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Error Description and Steps to Reproduce**
<!--
Describe what problem occurred and what you expected to happen instead.
1. To reproduce this, open file '...'
2. Click on '....'
3. See error
-->
**Attachments**
<!--
If applicable, add screenshots to help explain your problem. Please also drag-drop any files necessary to show the error (rename the file extension from .ifc to .txt to upload). Private files can be uploaded to https://ifcopenshell.org/upload.html - only viewed by core developers and will be deleted afterwards.
-->
**Debug information**
<!--
If this is in Bonsai, paste the output from the Copy Debug Information option in Bonsai. It can be found under Quality and Coordination -> Quality Control -> Debug. If this is a general software issue, if relevant include details about IfcOpenShell version, operating system, Python version, etc.
-->
-23
View File
@@ -1,23 +0,0 @@
name: Bug Report
description: Crashes, error messages, and broken features
type: bug
body:
- type: textarea
attributes:
label: Bug Description
placeholder: |
Describe what problem occurred and what you expected to happen instead.
1. To reproduce this, open file '...'
2. Click on '....'
3. See error
- type: textarea
attributes:
label: Attachments
description: "Private files can be uploaded to https://ifcopenshell.org/upload.html - only viewed by core developers and will be deleted afterwards. Please submit your report first then upload private files afterwards."
placeholder: "If applicable, add screenshots to help explain your problem. Please also drag-drop any files necessary to show the error (rename the file extension from .ifc to .txt to upload)."
- type: textarea
attributes:
label: Debug and Error Output
description: "If this is in Bonsai, paste the output from the Copy Debug Information option in Bonsai. It can be found under Quality and Coordination -> Quality Control -> Debug. If this is a general software issue, if relevant include details about IfcOpenShell version, operating system, Python version, etc."
render: yes
-8
View File
@@ -1,8 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: Community Forums
url: https://community.osarch.org/
about: Have a question? Want to discuss an idea? Try the OSArch forums instead.
- name: Live Chat
url: https://osarch.org/chat
about: Have a really confusing issue? Want real-time support?
+15
View File
@@ -0,0 +1,15 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Feature Description**
<!--
Describe a feature you'd like us to add. If it's not obvious, explain why this feature is awesome. Note that feature requests must be specific and measurable.
-->
@@ -1,8 +0,0 @@
name: Feature Request
description: Suggest a new feature or improvement
type: Feature
body:
- type: textarea
attributes:
label: Feature Description
placeholder: "Describe a feature you'd like us to add, or a change to the user interface, design, or workflow for usability. If it's not obvious, explain why this feature is awesome. Note that feature requests must be specific and measurable."
+15 -5
View File
@@ -44,11 +44,7 @@ jobs:
brew update
brew install git bison autoconf automake libffi cmake findutils
echo "$(brew --prefix findutils)/libexec/gnubin" >> $GITHUB_PATH
- name: Install aws cli
run: |
python -m pip install awscli
- name: Unpack Dependencies
run: |
install_root=$(find ./build -maxdepth 4 -name install)
@@ -121,6 +117,20 @@ jobs:
aws-secret-access-key: ${{ secrets.AWS_UPLOAD_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Overwrite aws cli
run: |
# Error: The `brew link` step did not complete successfully
# The formula built, but is not symlinked into /usr/local
# Could not symlink bin/idle3
# Target /usr/local/bin/idle3
# already exists. You may want to remove it:
# rm '/usr/local/bin/idle3'
#
# To force the link and overwrite all conflicting files:
# brew link --overwrite python@3.13
# https://github.com/rust-lang/rustup/pull/3989/files
brew install --overwrite awscli | true
- name: Upload .zip archives to S3
run: |
aws s3 cp ~/output s3://ifcopenshell-builds/ --recursive
+5 -5
View File
@@ -19,7 +19,7 @@ jobs:
with:
submodules: recursive
repository: pyodide/pyodide
ref: '0.28.0a3'
ref: '0.26.4'
token: ${{ secrets.BUILD_REPO_TOKEN }}
path: pyodide
@@ -29,18 +29,18 @@ jobs:
sed -i s/0.8.0/$VERSION/g IfcOpenShell/pyodide/meta.yaml
sed -i s/0.8.0/$VERSION/g IfcOpenShell/pyodide/setup.py
echo '#!/usr/bin/bash' > script.sh
echo 'cd pyodide' >> script.sh
echo 'make' >> script.sh
echo 'cd pyodide' > script.sh
echo 'make && pip install ./pyodide-build' >> script.sh
echo 'cd ..' >> script.sh
echo 'mkdir -p packages/ifcopenshell' >> script.sh
echo 'cp IfcOpenShell/pyodide/meta.yaml packages/ifcopenshell' >> script.sh
echo 'PYODIDE_ROOT=/src/pyodide \' >> script.sh
echo 'PATH=/src/pyodide/emsdk/emsdk:/src/pyodide/emsdk/emsdk/node/22.16.0_64bit/bin:/src/pyodide/emsdk/emsdk/upstream/emscripten:$PATH \' >> script.sh
echo 'PATH=/src/pyodide/emsdk/emsdk:/src/pyodide/emsdk/emsdk/node/20.18.0_64bit/bin:/src/pyodide/emsdk/emsdk/upstream/emscripten:$PATH \' >> script.sh
echo 'pyodide build-recipes ifcopenshell --install' >> script.sh
chmod +x script.sh
sed -i s/--tty// pyodide/run_docker
pyodide/run_docker ./script.sh
mv dist/ifcopenshell-$VERSION-py3-none-any.whl dist/ifcopenshell-$VERSION+${GITHUB_SHA:0:7}-cp313-cp313-emscripten_4_0_9_wasm32.whl
mv dist/ifcopenshell-$VERSION-py3-none-any.whl dist/ifcopenshell-$VERSION+${GITHUB_SHA:0:7}-cp312-cp312-emscripten_3_1_58_wasm32.whl
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
bzip2 patch mesa-libGL-devel libffi-devel fontconfig-devel \
sqlite-devel bzip2-devel zlib-devel openssl-devel xz-devel \
readline-devel ncurses-devel libffi-devel libuuid-devel git-lfs \
findutils xz
findutils
git config --global --add safe.directory '*'
- name: Install aws cli
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
bzip2 patch mesa-libGL-devel libffi-devel fontconfig-devel \
sqlite-devel bzip2-devel zlib-devel openssl-devel xz-devel \
readline-devel ncurses-devel libffi-devel libuuid-devel git-lfs \
findutils xz
findutils
git config --global --add safe.directory '*'
- name: Install aws cli
+11 -19
View File
@@ -5,7 +5,7 @@ on:
jobs:
build_ifcopenshell:
runs-on: windows-2022
runs-on: windows-2019
strategy:
fail-fast: false
matrix:
@@ -21,7 +21,7 @@ jobs:
uses: actions/checkout@v3
with:
repository: IfcOpenShell/build-outputs
path: _deps-vs2022-x64-installed
path: _deps-vs2019-x64-installed
ref: windows-${{ matrix.arch }}
lfs: true
token: ${{ secrets.BUILD_REPO_TOKEN }}
@@ -40,7 +40,7 @@ jobs:
- name: Unpack Dependencies
run: |
cd _deps-vs2022-x64-installed
cd _deps-vs2019-x64-installed
Get-ChildItem -Path . -Filter 'cache-*.zip' | ForEach-Object {
7z x $_.FullName
}
@@ -53,17 +53,17 @@ jobs:
for /f "tokens=1,2,3 delims=." %%a in ("%PYTHON_VERSION%") do (
set PY_VER_MAJOR_MINOR=%%a%%b
)
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat"
call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvars64.bat"
SET IFCOS_INSTALL_PYTHON=FALSE
cd win
echo y | call build-deps.cmd vs2022-x64 Release
echo y | call build-deps.cmd vs2019-x64 Release
SET PYTHONHOME=C:\Python\${{ matrix.python }}
call run-cmake.bat vs2022-x64 -DENABLE_BUILD_OPTIMIZATIONS=On -DGLTF_SUPPORT=ON -DADD_COMMIT_SHA=ON -DVERSION_OVERRIDE=ON
call install-ifcopenshell.bat vs2022-x64 Release
call run-cmake.bat vs2019-x64 -DENABLE_BUILD_OPTIMIZATIONS=On -DGLTF_SUPPORT=ON -DADD_COMMIT_SHA=ON -DVERSION_OVERRIDE=ON
call install-ifcopenshell.bat vs2019-x64 Release
- name: Pack Dependencies
run: |
cd _deps-vs2022-x64-installed
cd _deps-vs2019-x64-installed
Get-ChildItem -Path . -Directory | ForEach-Object {
$cacheFile = "cache-$($_.Name).zip"
echo $cacheFile
@@ -74,7 +74,7 @@ jobs:
- name: Commit and Push Changes to Build Repository
run: |
cd _deps-vs2022-x64-installed
cd _deps-vs2019-x64-installed
git config user.name "IfcOpenBot"
git config user.email "ifcopenbot@ifcopenshell.org"
git add *.zip
@@ -90,7 +90,7 @@ jobs:
if ("${{ matrix.python }}" -eq "3.9.11") {
# only for the first python version the executables are assembled for upload
cd _installed-vs2022-x64/bin
cd _installed-vs2019-x64/bin
Get-ChildItem -Path . | ForEach-Object {
echo $_
$exe = $_.Name
@@ -118,13 +118,5 @@ jobs:
aws-region: us-east-1
- name: Upload .zip Archives to S3
env:
AWS_DEBUG: 1
AWS_RETRY_MODE: standard
AWS_MAX_ATTEMPTS: 3
run: |
dir "$env:USERPROFILE\output"
foreach ($zip in Get-ChildItem -Path "$env:USERPROFILE\output" -Filter *.zip) {
aws s3 cp "$($zip.FullName)" s3://ifcopenshell-builds/ --debug
Start-Sleep -Seconds 5
}
aws s3 cp $env:USERPROFILE\output s3://ifcopenshell-builds/ --recursive
+49 -40
View File
@@ -4,6 +4,9 @@ on:
push:
pull_request:
env:
PYTHON_VERSION: "3.12"
jobs:
lint-formatting:
runs-on: ubuntu-latest
@@ -14,51 +17,57 @@ jobs:
- name: Action - install python
uses: actions/setup-python@v5.3.0
with:
python-version: "3.9"
python-version: "${{ env.PYTHON_VERSION }}"
- name: Action - install python
uses: actions/setup-python@v5.3.0
with:
python-version: "3.11"
- name: Install dependencies
- name: Step 1 - install dependencies
shell: bash
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
uv tool install ruff
uv tool install black
python3 -m pip install --upgrade pip
python3 -m pip install 'black>=24.10.0'
# black doesn't catch all syntax errors, so we check them explicitly.
- name: Check syntax errors
id: syntax-errors
# NOTE: This would suffice, however it is less informative in terms of the 3 possible outcomes
# - name: QA Step - check linting
# shell: bash
# id: linting
# run: |
# python3 -m black .
# QA STEP
- name: QA Step - check linting
shell: bash
id: linting
run: |
ERROR=0
python3.9 -W error -m compileall -q src/ifcopenshell-python || ERROR=1
python3.11 -W error -m compileall -q src/bonsai || ERROR=1
exit $ERROR
python3 -m black --check . \
&& exit 0 \
|| (echo "exit_code=$?" >> "$GITHUB_OUTPUT" && exit 1);
continue-on-error: true
- name: Black formatter
id: black
run: |
uvx black --diff --check .
continue-on-error: true
# OUTCOME 1 of QA STEP
- name: QA Step - no linting errors
if: steps.linting.outcome == 'success'
shell: bash
run: |-
echo "::notice::QA step linting succeeded"
exit 0;
- name: Ruff check
id: ruff
run: |
uvx ruff check
continue-on-error: true
# OUTCOME 2i of QA STEP
- name: QA Step - unprettified code with no syntax errors
if: steps.linting.outputs.exit_code == 1
shell: bash
run: |-
echo "::group::QA step succeeded with warnings"
echo "::warning::one or more files contains unformatted code but no syntax errors";
echo "::notice::please run the linter before pushing!";
echo "::endgroup::"
exit 0;
- name: Final check
run: |
ERROR=0
if [ "${{ steps.syntax-errors.outcome }}" != "success" ]; then
echo "::error::Syntax errors check failed, see 'syntax-errors' step for the details." && ERROR=1
fi
if [ "${{ steps.black.outcome }}" != "success" ]; then
echo "::error::Black formatting check failed, see 'black' step for the details." && ERROR=1
fi
if [ "${{ steps.ruff.outcome }}" != "success" ]; then
echo "::error::Ruff check failed, see 'ruff' step for the details." && ERROR=1
fi
exit $ERROR
# OUTCOME 2ii of QA STEP
- name: QA Step - code contains syntax errors
if: steps.linting.outputs.exit_code == 123
shell: bash
run: |-
echo "::group::QA step failed"
echo "::error::one or more files contains syntax errors";
echo "::notice::please run the linter and fix syntax errors before pushing!";
echo "::endgroup::"
exit 1;
+2 -3
View File
@@ -96,7 +96,7 @@ jobs:
uses: actions/checkout@v2
with:
repository: IfcOpenShell/bonsai_unstable_repo
token: ${{ secrets.IFCOPENBOT_TOKEN }}
token: ${{ secrets.IOS_TO_BLENDER_REPO }}
path: bonsai_unstable_repo
- name: Download Blender and run critical tests
@@ -104,7 +104,7 @@ jobs:
# Ensure Bonsai and ifcsverchok enable/disable works before uploading to extensions repo.
# Download Blender.
wget -q -O blender.tar.xz https://download.blender.org/release/Blender4.4/blender-4.4.0-linux-x64.tar.xz
wget -q -O blender.tar.xz https://download.blender.org/release/Blender4.3/blender-4.3.2-linux-x64.tar.xz
tar -xf blender.tar.xz
# Setup Blender.
@@ -175,7 +175,6 @@ jobs:
cd IfcOpenShell/src/bonsai
pip install pytest-blender
pip install pytest-bdd
blender --background --python scripts/setup_pytest.py
blender --python-expr "import bonsai; print(bonsai.bbim_semver); import ifcopenshell; print(ifcopenshell.version)" --background
make test
@@ -35,11 +35,11 @@ jobs:
- name: Version + date str
id: verdate
run: echo "verdate=${{ steps.version.outputs.version }}alpha${{ steps.date.outputs.date }}" >> $GITHUB_OUTPUT
run: echo "verdate='${{ steps.version.outputs.version }}alpha${{ steps.date.outputs.date }}'" >> $GITHUB_OUTPUT
test:
name: ${{ matrix.platform.distver }}-${{ matrix.pyver.name }}
name: ${{ matrix.platform.distver }}-${{ matrix.pyver.name }}-${{ matrix.variant }}
needs: activate
runs-on: ${{ matrix.platform.distver }}
defaults:
@@ -48,67 +48,57 @@ jobs:
strategy:
fail-fast: false
matrix:
variant: [novtk, all]
pyver: [
# { name: py311, distver: '3.11'},
{ name: py312, distver: '3.12'}
]
platform: [
{ name: win, distver: windows-latest, pkg_dir: 'win-64' },
{ name: linux, distver: ubuntu-latest, pkg_dir: 'linux-64' },
{ name: macOS-arm, distver: macos-latest, pkg_dir: 'osx-arm64' },
{ name: macOS-x86, distver: macos-13, pkg_dir: 'osx-64' }
{ name: macOS, distver: macos-latest, pkg_dir: 'osx-64' }
]
steps:
- name: Set Swap Space
- name: Set up swap space
if: runner.os == 'Linux'
uses: pierotofy/set-swap-space@master
uses: pierotofy/set-swap-space@v1.0
with:
swap-size-gb: 10
- name: set ARTIFACTS ENV vars
shell: bash
run: |
pwd
if [[ "$RUNNER_OS" == "Windows" ]]; then
echo "ARTIFACTS_DIR=D:/a/artifacts" >> $GITHUB_ENV
elif [[ "$RUNNER_OS" == "macOS" ]]; then
echo "ARTIFACTS_DIR=/Users/runner/work/artifacts" >> $GITHUB_ENV
elif [[ "$RUNNER_OS" == "Linux" ]]; then
echo "ARTIFACTS_DIR=/home/runner/work/artifacts" >> $GITHUB_ENV
fi
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Download and extract MacOSX SDK
if: ${{ matrix.platform.name == 'macOS-x86' }}
if: ${{ runner.os == 'macOS' }}
run: |
curl -L https://github.com/phracker/MacOSX-SDKs/releases/download/11.3/MacOSX10.13.sdk.tar.xz | tar -xvJf - -C /Users/runner/work/
- uses: mamba-org/setup-micromamba@v2 # https://github.com/mamba-org/setup-micromamba
- uses: mamba-org/setup-micromamba@v1 # https://github.com/mamba-org/setup-micromamba
with:
environment-name: test-env
create-args: >-
python=3.12
python=3.11
anaconda-client
rattler-build
boa
- name: create conda package dist dir
run: |
mkdir -p ${{ env.ARTIFACTS_DIR }}
mkdir -p ${{ github.workspace }}/dist
- name: build & test ifcopenshell
run: |
rattler-build build -r conda/recipe.yaml --output-dir '${{ env.ARTIFACTS_DIR }}'
conda mambabuild . --python ${{ matrix.pyver.distver }} --no-remove-work-dir --output-folder '${{ github.workspace }}/dist' --variants='{variant: ${{ matrix.variant }}}'
working-directory: ./conda
env:
VERSION_OVERRIDE: ${{ needs.activate.outputs.verdate }}
- name: upload to anaconda
if: ${{ matrix.platform.name == 'win' }}
run: |
anaconda -t ${{ secrets.ANACONDA_TOKEN }} upload --force --user ifcopenshell '${{ env.ARTIFACTS_DIR }}/${{ matrix.platform.pkg_dir }}/*.conda'
anaconda -t ${{ secrets.ANACONDA_TOKEN }} upload --force --user ifcopenshell '${{ github.workspace }}/dist/${{ matrix.platform.pkg_dir }}/*.tar.bz2'
- name: upload to anaconda
if: ${{ matrix.platform.name != 'win' }}
run: |
anaconda -t ${{ secrets.ANACONDA_TOKEN }} upload --force --user ifcopenshell ${{ env.ARTIFACTS_DIR }}/${{ matrix.platform.pkg_dir }}/*.conda
anaconda -t ${{ secrets.ANACONDA_TOKEN }} upload --force --user ifcopenshell ${{ github.workspace }}/dist/${{ matrix.platform.pkg_dir }}/*.tar.bz2
+6 -7
View File
@@ -9,7 +9,7 @@ on:
jobs:
activate:
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
if: |
github.repository == 'IfcOpenShell/IfcOpenShell' &&
!startsWith(github.event.head_commit.message, 'Release ') &&
@@ -18,7 +18,7 @@ jobs:
- run: echo ok go
build:
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
needs: activate
steps:
- uses: actions/checkout@v2
@@ -35,7 +35,7 @@ jobs:
-
name: ccache
uses: hendrikmuhs/ccache-action@v1.2
uses: hendrikmuhs/ccache-action@v1
-
name: Build ifcopenshell
@@ -44,7 +44,6 @@ jobs:
cmake \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_STANDARD=17 \
-DCMAKE_INSTALL_PREFIX=$PWD/install/ \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH=/usr \
@@ -76,7 +75,7 @@ jobs:
make package
working-directory: build
- name: Upload
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v2
with:
# Artifact name
name: ifcos-artifacts
@@ -84,7 +83,7 @@ jobs:
path: build/assets/Ifc*
deliver:
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
needs: build
name: Docker Build, Tag, Push
@@ -119,6 +118,6 @@ jobs:
repository: aecgeeks/ifcopenshell
# Since the dispatch is set to `tag`, `github.ref_name` should evaluate to the pushed tag
# On a workflow dispatch, `ref_name` will take on the value from the dispatch payload
tags: aecgeeks/ifcopenshell:${{ github.ref_name }}${{ github.ref_name == github.event.repository.default_branch && ',aecgeeks/ifcopenshell:22.04' }}
tags: aecgeeks/ifcopenshell:${{ github.ref_name }}${{ github.ref_name == github.event.repository.default_branch && ',aecgeeks/ifcopenshell:latest' }}
file: ./Dockerfile
push: true
@@ -44,8 +44,6 @@ jobs:
}
steps:
- uses: actions/checkout@v2
with:
submodules: recursive
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
@@ -39,8 +39,6 @@ jobs:
}
steps:
- uses: actions/checkout@v2
with:
submodules: recursive
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
+6 -16
View File
@@ -82,7 +82,6 @@ jobs:
cmake \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_STANDARD=17 \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH=/usr \
-DCMAKE_SYSTEM_PREFIX_PATH=/usr \
@@ -128,21 +127,12 @@ jobs:
cd ../src/ifcopenshell-python
mv ifcopenshell ifcopenshell-local # Force testing on installed module
pip install -e ../ifcpatch --no-deps # Needed for sql.py tests.
ERROR=0
make test || ERROR=1
cd ../bcf && make test || ERROR=1
make test
cd ../bcf && make test
pip install requests
cd ../bsdd && make test || ERROR=1
cd ../bsdd && make test
pip install deepdiff
cd ../ifcdiff && make test || ERROR=1
cd ../ifcpatch && make test || ERROR=1
cd ../ifcdiff && make test
cd ../ifcpatch && make test
pip install -e ../ifctester --no-deps
cd ../ifctester && make test || ERROR=1
# Run mathutils related tests at the end to ensure no other code is relying on mathutils.
cd ../ifcopenshell-python
pip install mathutils
make test-mathutils || ERROR=1
if [ $ERROR -ne 0 ]; then
echo "One or more tests failed";
exit 1;
fi
cd ../ifctester && make test
-2
View File
@@ -4,7 +4,6 @@ on:
push:
tags:
- 'v[0-9].[0-9].[0-9]*'
workflow_dispatch:
jobs:
activate:
@@ -46,7 +45,6 @@ jobs:
mkdir build && cd build
cmake \
-DCMAKE_INSTALL_PREFIX=$PWD/install/ \
-DCMAKE_CXX_STANDARD=17 \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH=/usr \
-DCMAKE_SYSTEM_PREFIX_PATH=/usr \
+2 -3
View File
@@ -86,13 +86,12 @@ src/bonsai/bonsai/bim/data/build/
src/bonsai/bonsai/bim/data/gantt/index.html
src/bonsai/bonsai/bim/data/gantt/jsgantt.js
src/bonsai/bonsai/bim/data/gantt/jsgantt.css
src/bonsai/bonsai/bim/data/webui/static/js/jquery.min.js
src/bonsai/drawings
src/bonsai/layouts
# ifcopenshell swig and compiled files
src/ifcopenshell-python/ifcopenshell/_ifcopenshell_wrapper*.so
src/ifcopenshell-python/ifcopenshell/_ifcopenshell_wrapper.so
src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.py
# apple
@@ -107,4 +106,4 @@ src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.py
src/bonsai/bonsai/bim/schema/Brick.ttl
bonsaiDecoratorForLoads.code-workspace
dev_environment.bat
+1 -1
View File
@@ -1 +1 @@
0.8.3
0.8.2
+1 -8
View File
@@ -18,12 +18,7 @@
################################################################################
cmake_minimum_required(VERSION 3.21)
if (NOT DEFINED CMAKE_CXX_STANDARD)
set(CMAKE_CXX_STANDARD 17)
endif()
if (CMAKE_CXX_STANDARD LESS 17)
message(FATAL_ERROR "C++17 or newer is required.")
endif()
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) # not necessary, but encouraged
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
@@ -1294,8 +1289,6 @@ endif()
list(APPEND CPACK_SOURCE_IGNORE_FILES
"/\\\\.git"
"/build/"
"/.pytest_cache/"
"/__pycache__/"
)
set(CPACK_SOURCE_INSTALLED_DIRECTORIES "${CMAKE_SOURCE_DIR}/..;/")
set(CPACK_PACKAGE_NAME "${PROJECT_NAME}-${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}${EXTRA_VERSION}")
-5
View File
@@ -9,7 +9,6 @@ set LIBXML2="%LIBRARY_PREFIX%/lib/libxml2.lib"
cmake -G "Ninja" ^
-D SCHEMA_VERSIONS="2x3;4;4x1;4x3_add2" ^
-D CMAKE_BUILD_TYPE:STRING=Release ^
-D CMAKE_CXX_STANDARD=17 ^
-D CMAKE_INSTALL_PREFIX:FILEPATH="%LIBRARY_PREFIX%" ^
-D CMAKE_PREFIX_PATH:FILEPATH="%LIBRARY_PREFIX%" ^
-D CMAKE_SYSTEM_PREFIX_PATH:FILEPATH="%LIBRARY_PREFIX%" ^
@@ -50,7 +49,3 @@ if errorlevel 1 exit 1
ninja install -j 1
if errorlevel 1 exit 1
python %RECIPE_DIR%/update_version_init.py %PKG_VERSION% %SP_DIR%/ifcopenshell/__init__.py
if errorlevel 1 exit 1
+1 -4
View File
@@ -16,7 +16,6 @@ cmake ${CMAKE_ARGS} -G Ninja \
-DSCHEMA_VERSIONS="2x3;4;4x1;4x3_add2" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=$PREFIX \
-DCMAKE_CXX_STANDARD=17 \
${CMAKE_PLATFORM_FLAGS[@]} \
-DCMAKE_PREFIX_PATH=$PREFIX \
-DCMAKE_SYSTEM_PREFIX_PATH=$PREFIX \
@@ -47,6 +46,4 @@ cmake ${CMAKE_ARGS} -G Ninja \
ninja
ninja install -j 1
python "${RECIPE_DIR}/update_version_init.py" "${PKG_VERSION}" "${SP_DIR}/ifcopenshell/__init__.py"
ninja install -j 1
+16 -16
View File
@@ -1,5 +1,6 @@
python:
- 3.12
variant:
- novtk
- all
occt:
- 7.8.1
@@ -23,15 +24,14 @@ cxx_compiler_version:
- '12' # [linux]
- '16' # [osx]
c_stdlib_version:
- 2.17 # [linux]
- 10.13 # [osx and x86_64]
- 11.0 # [osx and arm64]
- '2.12' # [linux]
- '10.13' # [osx]
hdf5:
- 1.14.6
- 1.14.3
libboost_devel:
- '1.86'
- '1.84'
libxml2:
- 2.13
- '2'
mpfr:
- '4'
gmp:
@@ -47,12 +47,12 @@ target_platform:
- win-64 # [win]
- linux-64 # [linux]
- osx-64 # [osx]
macos_machine: # [osx]
- x86_64-apple-darwin13.4.0 # [osx and x86_64]
- arm64-apple-darwin20.0.0 # [osx and arm64]
MACOSX_DEPLOYMENT_TARGET: # [osx]
- 11.0 # [osx and arm64]
- 10.13 # [osx and x86_64]
macos_machine:
- x86_64-apple-darwin13.4.0
MACOSX_DEPLOYMENT_TARGET:
- '10.13'
MACOSX_SDK_VERSION:
- '10.13'
CONDA_BUILD_SYSROOT: # [osx]
- "/Users/runner/work/MacOSX10.13.sdk" # [osx and x86_64]
CONDA_BUILD_SYSROOT:
- "/Users/runner/work/MacOSX10.13.sdk" # [osx]
+33 -48
View File
@@ -1,78 +1,64 @@
context:
version: ${{ env.get("VERSION_OVERRIDE", default="0.8.2.1") }}
build: 1
{% set version = environ.get('VERSION_OVERRIDE', '0.8.0') %}
{% set build = 0 %}
# Higher number -> Always prioritize "novtk" variant over "all" variant
{% set build = build + 200 %} # [variant == "novtk"]
{% set build = build + 100 %} # [variant == "all"]
package:
name: ifcopenshell
version: ${{ version }}
version: {{ version }}
source:
- path: ..
path: ../
build:
number: ${{ build }}
dynamic_linking:
binary_relocation: ${{ true if osx }}
number: {{ build }}
skip: true # [py<39]
binary_relocation: false # [osx]
string: py{{ CONDA_PY }}_{{ variant }}_h{{ PKG_HASH }}_{{ build }}
run_exports:
- {{ pin_subpackage('ifcopenshell', max_pin='x.x.x') }} *{{ variant }}*
requirements:
build:
- if: build_platform != target_platform
then:
- python
- cross-python_${{ target_platform }}
- cmake <4
- cmake
- ninja
- swig >=4.1.1
- ${{ stdlib("c") }}
- ${{ compiler('c') }}
- ${{ compiler('cxx') }}
- {{ stdlib("c") }}
- {{ compiler('c') }}
- {{ compiler('cxx') }}
host:
- python
- libboost-devel
- occt
- occt *=*{{ variant }}*
- libxml2
- cgal-cpp
- hdf5
- eigen
- mpfr
- nlohmann_json
- gmp
- gmp # [unix]
- mpir # [win]
- zlib
run:
- python
- shapely
- typing_extensions
- ${{ pin_compatible('occt', upper_bound='x.x.x') }}
- ${{ pin_compatible('cgal-cpp', upper_bound='x.x') }}
run_exports:
- ${{ pin_subpackage('ifcopenshell', upper_bound='x.x.x') }}
- {{ pin_compatible('occt', max_pin='x.x.x') }} *{{ variant }}*
- {{ pin_compatible('cgal-cpp', max_pin='x.x') }}
test:
imports:
- ifcopenshell
tests:
- python:
imports:
- ifcopenshell
pip_check: false
- script:
- python -c "import ifcopenshell; assert ifcopenshell.version == '${{ version }}', 'print(ifcopenshell.version)'"
requirements:
run:
- occt * *novtk* # Ensure that even though compiled against OCCT with VTK, it can still run with OCCT without VTK
- pytest
- python-dateutil
- xmlschema
- xsdata
- lxml
- isodate
- lark
- networkx
- tabulate
- shapely
about:
home: https://ifcopenshell.org
license: LGPL-3.0-or-later
license_file: COPYING
summary: IfcOpenShell is a library to support the IFC file format
summary: 'IfcOpenShell is a library to support the IFC file format'
description: |
IfcOpenShell
============
@@ -307,9 +293,8 @@ about:
[MSYS2]: https://msys2.github.io/ "MSYS2"
[win/readme.md]: https://github.com/IfcOpenShell/IfcOpenShell/tree/master/win/readme.md "win/readme.md"
[nix/build-all.py]: https://github.com/IfcOpenShell/IfcOpenShell/tree/master/nix/build-all.py "nix/build-all.py"
homepage: https://ifcopenshell.org
repository: https://github.com/IfcOpenShell/IfcOpenShell
documentation: https://ifcopenshell.org/
doc_url: https://ifcopenshell.org/
dev_url: https://github.com/IfcOpenShell/IfcOpenShell
extra:
recipe-maintainers:
-32
View File
@@ -1,32 +0,0 @@
import re
import argparse
from pathlib import Path
def update_version(file_path: str, version: str) -> None:
"""Update the version string in the given __init__.py file."""
file_path = Path(file_path)
# Read the file and replace the version
file_contents = file_path.read_text(encoding="utf-8")
new_contents = re.sub(r'version = "0\.0\.0"', f'version = "{version}"', file_contents)
# Write the updated contents back to the file
file_path.write_text(new_contents, encoding="utf-8")
print(f"Updated version in {file_path} to {version}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Update the version string in a Python file.")
parser.add_argument(
"version",
type=str,
help="The version string to replace '0.0.0' with (e.g., '1.2.3')."
)
parser.add_argument(
"file",
type=str,
help="The path to the __init__.py file where the version will be updated."
)
args = parser.parse_args()
update_version(args.file, args.version)
+13 -40
View File
@@ -69,7 +69,6 @@ import sysconfig
import ssl
ssl._create_default_https_context = ssl._create_unverified_context
import time
from urllib.request import urlretrieve
@@ -89,14 +88,14 @@ OCE_VERSION = "0.18.3"
OCCT_VERSION = "7.8.1"
BOOST_VERSION = "1.86.0"
PCRE_VERSION = "8.41"
LIBXML2_VERSION = "2.13.8"
LIBXML2_VERSION = "2.9.11"
SWIG_VERSION = "4.0.2"
OPENCOLLADA_VERSION = "v1.6.68"
HDF5_VERSION = "1.12.1"
GMP_VERSION = "6.2.1"
MPFR_VERSION = "3.1.6" # latest is 4.1.0
CGAL_VERSION = "5.6.x-branch"
CGAL_VERSION = "5.3"
USD_VERSION = "23.05"
TBB_VERSION = "2021.9.0"
@@ -324,7 +323,7 @@ def run_autoconf(arg1, configure_args, cwd):
if "wasm" in flags:
wasm.append("emconfigure")
run([*wasm, "/bin/sh", "../configure", *(["--host=wasm32"] if "wasm" in flags and not any(s.startswith('--host') for s in configure_args) else []), *configure_args, f"--prefix={prefix}"], cwd=cwd)
run([*wasm, "/bin/sh", "../configure"] + configure_args + [f"--prefix={prefix}"], cwd=cwd)
def run_cmake(arg1, cmake_args, cmake_dir=None, cwd=None):
@@ -337,7 +336,7 @@ def run_cmake(arg1, cmake_args, cmake_dir=None, cwd=None):
if "wasm" in flags:
wasm.append("emcmake")
run([*wasm, "cmake", P, *cmake_args, f"-DCMAKE_BUILD_TYPE={BUILD_CFG}", f"-DBUILD_SHARED_LIBS={OFF_ON[not BUILD_STATIC]}", f"-DCMAKE_CXX_FLAGS='{os.environ['CXXFLAGS']}'", f"-DCMAKE_C_FLAGS='{os.environ['CFLAGS']}'", f"-DCMAKE_SHARED_LINKER_FLAGS={os.environ['LDFLAGS']}"], cwd=cwd)
run([*wasm, "cmake", P, *cmake_args, f"-DCMAKE_BUILD_TYPE={BUILD_CFG}", f"-DBUILD_SHARED_LIBS={OFF_ON[not BUILD_STATIC]}"], cwd=cwd)
def git_clone_or_pull_repository(clone_url, target_dir, revision=None):
@@ -363,7 +362,7 @@ def git_clone_or_pull_repository(clone_url, target_dir, revision=None):
run([git, "checkout", revision], cwd=target_dir)
def build_dependency(name, mode, build_tool_args, download_url, download_name, download_tool=download_tool_default, revision=None, patch=None, shell=None, pre_compile_subs=[], additional_files={}, no_append_name=False, **kwargs):
def build_dependency(name, mode, build_tool_args, download_url, download_name, download_tool=download_tool_default, revision=None, patch=None, additional_files={}, no_append_name=False, **kwargs):
"""Handles building of dependencies with different tools (which are
distinguished with the `mode` argument. `build_tool_args` is expected to be
a list which is necessary in order to not mess up quoting of compiler and
@@ -386,14 +385,7 @@ def build_dependency(name, mode, build_tool_args, download_url, download_name, d
download_path = os.path.join(build_dir, download_name)
if not os.path.exists(download_path):
for _ in range(3):
try:
urlretrieve(url, os.path.join(build_dir, download_path))
break
except ConnectionError as e:
print(e, "... retrying...")
time.sleep(30.)
continue
urlretrieve(url, os.path.join(build_dir, download_path))
else:
logger.info(f"Download '{download_path}' already exists, assuming it's an undamaged download and that it has been extracted if possible, skipping")
elif download_tool == download_tool_git:
@@ -412,8 +404,6 @@ def build_dependency(name, mode, build_tool_args, download_url, download_name, d
compr = "gz"
elif download_name.endswith(".tar.bz2"):
compr = "bz2"
elif download_name.endswith(".tar.xz"):
compr = "xz"
else:
raise RuntimeError("fix source for new download type")
download_tarfile = tarfile.open(name=download_tarfile_path, mode=f"r:{compr}")
@@ -440,9 +430,6 @@ def build_dependency(name, mode, build_tool_args, download_url, download_name, d
except Exception as e:
# Assert that the patch has already been applied
run(["patch", "-p1", "--batch", "--reverse", "--dry-run", "-i", patch_abs], cwd=extract_dir)
if shell is not None:
sp.run(shell, shell=True, check=True, cwd=extract_dir)
if mode == "ctest":
run(["ctest", "-S", "HDF5config.cmake,BUILD_GENERATOR=Unix", "-C", BUILD_CFG, "-V", "-O", "hdf5.log"], cwd=extract_dir)
@@ -464,12 +451,6 @@ def build_dependency(name, mode, build_tool_args, download_url, download_name, d
run_cmake(name, build_tool_args, cwd=extract_build_dir)
else:
raise ValueError()
for fn, before, after in pre_compile_subs:
with open(os.path.join(extract_dir, fn), 'r') as f:
s = f.read()
s = s.replace(before, after)
with open(os.path.join(extract_dir, fn), 'w') as f:
f.write(s)
logger.info(f"\rBuilding {name}... ")
run([make, f"-j{IFCOS_NUM_BUILD_PROCS}", "VERBOSE=1"], cwd=extract_build_dir)
logger.info(f"\rInstalling {name}... ")
@@ -497,6 +478,9 @@ ADDITIONAL_ARGS = []
if platform.system() == "Darwin":
ADDITIONAL_ARGS = [f"-mmacosx-version-min={TOOLSET}"] + ADDITIONAL_ARGS
if "wasm" in flags:
ADDITIONAL_ARGS.extend(("-sWASM_BIGINT", "-fwasm-exceptions"))
# If the linker supports GC sections, set it up to reduce binary file size
# -fPIC is required for the shared libraries to work
@@ -507,11 +491,7 @@ CFLAGS = os.environ.get("CFLAGS", "")
LDFLAGS = os.environ.get("LDFLAGS", "")
ADDITIONAL_ARGS_STR = " ".join(ADDITIONAL_ARGS)
if "wasm" in flags:
CFLAGS_MINIMAL = CXXFLAGS_MINIMAL = CFLAGS = CXXFLAGS = os.environ['SIDE_MODULE_CFLAGS']
LDFLAGS = os.environ['SIDE_MODULE_LDFLAGS']
elif sp.call([bash, "-c", "ld --gc-sections 2>&1 | grep -- --gc-sections &> /dev/null"]) != 0:
if "wasm" not in flags and sp.call([bash, "-c", "ld --gc-sections 2>&1 | grep -- --gc-sections &> /dev/null"]) != 0:
CXXFLAGS_MINIMAL = f"{CXXFLAGS} {PIC} {ADDITIONAL_ARGS_STR}"
CFLAGS_MINIMAL = f"{CFLAGS} {PIC} {ADDITIONAL_ARGS_STR}"
if BUILD_STATIC:
@@ -537,7 +517,6 @@ if "lto" in flags:
locals()[f] += f" -flto={IFCOS_NUM_BUILD_PROCS}"
os.environ["CXXFLAGS"] = CXXFLAGS
os.environ["CPPFLAGS"] = CXXFLAGS
os.environ["CFLAGS"] = CFLAGS
os.environ["LDFLAGS"] = LDFLAGS
@@ -674,8 +653,8 @@ if "libxml2" in targets:
"--without-iconv",
"--without-lzma"
],
download_url=f"https://download.gnome.org/sources/libxml2/{'.'.join(LIBXML2_VERSION.split('.')[0:2])}/",
download_name=f"libxml2-{LIBXML2_VERSION}.tar.xz"
download_url="ftp://xmlsoft.org/libxml2/",
download_name=f"libxml2-{LIBXML2_VERSION}.tar.gz"
)
if "OpenCOLLADA" in targets:
@@ -708,11 +687,9 @@ if "OpenCOLLADA" in targets:
if "python" in targets and not USE_CURRENT_PYTHON_VERSION and "wasm" not in flags:
# Python should not be built with -fvisibility=hidden, from experience that introduces segfaults
OLD_CPP_FLAGS = os.environ["CPPFLAGS"]
OLD_CXX_FLAGS = os.environ["CXXFLAGS"]
OLD_C_FLAGS = os.environ["CFLAGS"]
os.environ["CXXFLAGS"] = CXXFLAGS_MINIMAL
os.environ["CPPFLAGS"] = CXXFLAGS_MINIMAL
os.environ["CFLAGS"] = CFLAGS_MINIMAL
# On OSX a dynamic python library is built or it would not be compatible
@@ -742,7 +719,6 @@ if "python" in targets and not USE_CURRENT_PYTHON_VERSION and "wasm" not in flag
):
raise e
os.environ["CPPFLAGS"] = OLD_CPP_FLAGS
os.environ["CXXFLAGS"] = OLD_CXX_FLAGS
os.environ["CFLAGS"] = OLD_C_FLAGS
@@ -788,7 +764,6 @@ if "cgal" in targets:
name=f"gmp-{GMP_VERSION}",
mode="autoconf",
build_tool_args=[ENABLE_FLAG, DISABLE_FLAG, "--with-pic", *gmp_args],
pre_compile_subs=([("build/config.h", "HAVE_OBSTACK_VPRINTF 1", "HAVE_OBSTACK_VPRINTF 0")] if "wasm" in flags else []),
download_url="https://ftp.gnu.org/gnu/gmp/",
download_name=f"gmp-{GMP_VERSION}.tar.bz2"
)
@@ -817,7 +792,7 @@ if "cgal" in targets:
download_url="https://github.com/CGAL/cgal.git",
download_name="cgal",
download_tool=download_tool_git,
revision=CGAL_VERSION
revision=f"v{CGAL_VERSION}"
)
if "usd" in targets:
@@ -867,7 +842,6 @@ os.makedirs(executables_dir, exist_ok=True)
cmake_args = [
"-DCMAKE_CXX_STANDARD=" "17",
"-DUSE_MMAP=" "OFF",
"-DBUILD_EXAMPLES=" "OFF",
"-DBUILD_SHARED_LIBS=" +OFF_ON[not BUILD_STATIC],
@@ -973,7 +947,6 @@ if "IfcOpenShell-Python" in targets:
if "wasm" in flags:
ADDITIONAL_ARGS = f"-Wl,-undefined,suppress -sSIDE_MODULE=2 -sEXPORTED_FUNCTIONS=_PyInit__ifcopenshell_wrapper"
os.environ["CPPFLAGS"] = f"{CXXFLAGS_MINIMAL} {ADDITIONAL_ARGS}"
os.environ["CXXFLAGS"] = f"{CXXFLAGS_MINIMAL} {ADDITIONAL_ARGS}"
os.environ["CFLAGS"] = f"{CFLAGS_MINIMAL} {ADDITIONAL_ARGS}"
os.environ["LDFLAGS"] = f"{LDFLAGS} {ADDITIONAL_ARGS}"
+1 -1
View File
@@ -7,7 +7,7 @@ source:
build:
script: |
BUILD_CFG=Release python nix/build-all.py --without-hdf5 --without-opencollada --without-swig --without-pcre -v --wasm --py313 IfcOpenShell-Python
BUILD_CFG=Release python nix/build-all.py --without-hdf5 --without-opencollada --without-swig --without-pcre -v --wasm --py312 IfcOpenShell-Python
mv package/ifcopenshell .
cp pyodide/setup.py .
+24 -49
View File
@@ -1,62 +1,37 @@
[tool.black]
line-length = 120
include = "src/.*.pyi?$"
include = '''
src/(
bcf
|bcfserver
|bonsai
|bsdd
|foundationserver
|ifc2ca
|ifc4d
|ifc5d
|ifcbimtester
|ifcblender
|ifccityjson
|ifcclash
|ifccsv
|ifcdiff
|ifcfm
|ifcpatch
|ifctester
|ifcopenshell-python
|ifcsverchok
|opencdeserver
)/.*.py$
'''
extend-exclude = '''
src/ifcopenshell-python/ifcopenshell/express/*
|src/ifcopenshell-python/ifcopenshell/mvd/*
|src/ifcopenshell-python/ifcopenshell/simple_spf/*
|src/ifc2ca/templates/*
|src/ifcconvert/cityjson/*
|src/svgfill
|src/exterior-shell-extractor
|src/pyodide
'''
[tool.pyright]
reportInvalidTypeForm = false
disableBytesTypePromotions = true
reportUnnecessaryTypeIgnoreComment = true
# Define here general ruff settings,
# then they will be inherited projects .toml files.
# This allows using assuming different Python version for different projects.
[tool.ruff]
exclude = [
# Submodules.
"src/ifcconvert/cityjson",
"src/ifcopenshell-python/ifcopenshell/express",
"src/ifcopenshell-python/ifcopenshell/mvd",
"src/ifcopenshell-python/ifcopenshell/simple_spf",
"src/svgfill",
#
# Unformatted.
"src/exterior-shell-extractor",
# Incompatible with linter.
"src/ifc2ca/templates",
]
[tool.ruff.lint]
select = [
# Default Ruff rules.
# "E4", # imports
# "E7", # statements
"E9", # io errors
# "F", # pyflakes
#
"FA", # future annotations
"UP", # pyupgrade
"RUF015", # next() > list_comprehension[0]
]
ignore = [
"FA100", # Conflicts with Blender using annotations for props definitions.
# Maybe will enable later:
"UP007", # Optional to X | Y
"UP045", # Optional to X | None
"UP015", # Unnecessary mode argument
"UP028", # yield for -> yield from
"UP030", # implicit references for positional format fields
"UP031", # Replace % with .format
"UP032", # Replace .format with f-string
# Deprecated Ruff rules.
"UP038", # Use X | Y in isinstance
]
+1 -1
View File
@@ -20,7 +20,7 @@ def get_extensions_attributes(extensions: Extensions) -> dict[str, AttributeData
- subattribute name"""
possible_attributes = {}
for field in fields(type(extensions)):
field_type = field.type.__args__[0] # pyright: ignore [reportAttributeAccessIssue]
field_type = field.type.__args__[0] # type: ignore [reportAttributeAccessIssue]
subfield = next(iter(fields(field_type)))
xsd_name = subfield.metadata["name"]
possible_attributes[field.name] = AttributeData(field_type, subfield.name, xsd_name)
+1 -2
View File
@@ -6,7 +6,6 @@ Copyright (c) 2017-2020 Anthon van der Neut, Ruamel bvba
original idea from https://stackoverflow.com/a/19722365/1307905
"""
from __future__ import annotations
import zipfile
from io import BytesIO
from os import PathLike
@@ -48,7 +47,7 @@ class InMemoryZipFile:
def data(self) -> bytes:
return self.in_memory_data.getvalue()
def __enter__(self) -> InMemoryZipFile:
def __enter__(self) -> "InMemoryZipFile":
return self
def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
+5 -6
View File
@@ -1,6 +1,5 @@
"""BCF XML V2 handler."""
from __future__ import annotations
import uuid
import warnings
import zipfile
@@ -32,7 +31,7 @@ class BcfXml:
self._extension_schema: Optional[bytes] = None
self._zip_file = self._load_zip_file()
def __enter__(self) -> BcfXml:
def __enter__(self) -> "BcfXml":
return self
def __exit__(self, *args: Any) -> None:
@@ -100,7 +99,7 @@ class BcfXml:
extensions = mdl_extensions.Extensions()
xs = "{http://www.w3.org/2001/XMLSchema}"
root = etree.parse(io.BytesIO(self.extension_schema))
root = etree.parse(io.BytesIO((self.extension_schema)))
attrs = bcf.agnostic.extensions.get_extensions_attributes(extensions)
xsd_to_attrs = {v.subattr_xsd_name: k for k, v in attrs.items()}
@@ -149,7 +148,7 @@ class BcfXml:
return topics
@classmethod
def load(cls, filename: Path, xml_handler: Optional[AbstractXmlParserSerializer] = None) -> Optional[BcfXml]:
def load(cls, filename: Path, xml_handler: Optional[AbstractXmlParserSerializer] = None) -> Optional["BcfXml"]:
"""
Create a BcfXml object from a file.
@@ -173,7 +172,7 @@ class BcfXml:
cls,
project_name: Optional[str] = None,
xml_handler: Optional[AbstractXmlParserSerializer] = None,
) -> BcfXml:
) -> "BcfXml":
"""
Create a new BcfXml object.
@@ -262,7 +261,7 @@ class BcfXml:
)
# region Deprecated methods
def new_project(self) -> BcfXml:
def new_project(self) -> "BcfXml":
"""Deprecated method."""
warnings.warn("new_project is deprecated, use create_new instead.", DeprecationWarning)
return self.create_new()
+16 -20
View File
@@ -14,20 +14,16 @@
# Currently extensions support for v2 is only read-only.
import sys
from dataclasses import dataclass, field, fields
from typing import Optional
from typing import List, NamedTuple, Optional
DATACLASS_KWARGS = {} if sys.version_info < (3, 10) else {"slots": True, "kw_only": True}
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ExtensionsPriorities:
class Meta:
global_type = False
priority: list[str] = field(
priority: List[str] = field(
default_factory=list,
metadata={
"name": "Priority",
@@ -39,12 +35,12 @@ class ExtensionsPriorities:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ExtensionsSnippetTypes:
class Meta:
global_type = False
snippet_type: list[str] = field(
snippet_type: List[str] = field(
default_factory=list,
metadata={
"name": "SnippetType",
@@ -56,12 +52,12 @@ class ExtensionsSnippetTypes:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ExtensionsStages:
class Meta:
global_type = False
stage: list[str] = field(
stage: List[str] = field(
default_factory=list,
metadata={
"name": "Stage",
@@ -73,12 +69,12 @@ class ExtensionsStages:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ExtensionsTopicLabels:
class Meta:
global_type = False
topic_label: list[str] = field(
topic_label: List[str] = field(
default_factory=list,
metadata={
"name": "TopicLabel",
@@ -90,12 +86,12 @@ class ExtensionsTopicLabels:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ExtensionsTopicStatuses:
class Meta:
global_type = False
topic_status: list[str] = field(
topic_status: List[str] = field(
default_factory=list,
metadata={
"name": "TopicStatus",
@@ -107,12 +103,12 @@ class ExtensionsTopicStatuses:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ExtensionsTopicTypes:
class Meta:
global_type = False
topic_type: list[str] = field(
topic_type: List[str] = field(
default_factory=list,
metadata={
"name": "TopicType",
@@ -124,12 +120,12 @@ class ExtensionsTopicTypes:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ExtensionsUsers:
class Meta:
global_type = False
user: list[str] = field(
user: List[str] = field(
default_factory=list,
metadata={
"name": "UserIdType",
@@ -141,7 +137,7 @@ class ExtensionsUsers:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Extensions:
topic_types: Optional[ExtensionsTopicTypes] = field(
default=None,
+18 -21
View File
@@ -1,13 +1,10 @@
import sys
from dataclasses import dataclass, field
from typing import Optional
from typing import List, Optional
from xsdata.models.datatype import XmlDateTime
DATACLASS_KWARGS = {} if sys.version_info < (3, 10) else {"slots": True, "kw_only": True}
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class BimSnippet:
reference: str = field(
metadata={
@@ -41,7 +38,7 @@ class BimSnippet:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class CommentViewpoint:
class Meta:
global_type = False
@@ -56,7 +53,7 @@ class CommentViewpoint:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class HeaderFile:
class Meta:
global_type = False
@@ -112,7 +109,7 @@ class HeaderFile:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class TopicDocumentReference:
class Meta:
global_type = False
@@ -150,7 +147,7 @@ class TopicDocumentReference:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class TopicRelatedTopic:
class Meta:
global_type = False
@@ -165,7 +162,7 @@ class TopicRelatedTopic:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ViewPoint:
viewpoint: Optional[str] = field(
default=None,
@@ -201,7 +198,7 @@ class ViewPoint:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Comment:
date: XmlDateTime = field(
metadata={
@@ -261,9 +258,9 @@ class Comment:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Header:
file: list[HeaderFile] = field(
file: List[HeaderFile] = field(
default_factory=list,
metadata={
"name": "File",
@@ -274,9 +271,9 @@ class Header:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Topic:
reference_link: list[str] = field(
reference_link: List[str] = field(
default_factory=list,
metadata={
"name": "ReferenceLink",
@@ -308,7 +305,7 @@ class Topic:
"namespace": "",
},
)
labels: list[str] = field(
labels: List[str] = field(
default_factory=list,
metadata={
"name": "Labels",
@@ -388,7 +385,7 @@ class Topic:
"namespace": "",
},
)
document_reference: list[TopicDocumentReference] = field(
document_reference: List[TopicDocumentReference] = field(
default_factory=list,
metadata={
"name": "DocumentReference",
@@ -396,7 +393,7 @@ class Topic:
"namespace": "",
},
)
related_topic: list[TopicRelatedTopic] = field(
related_topic: List[TopicRelatedTopic] = field(
default_factory=list,
metadata={
"name": "RelatedTopic",
@@ -428,7 +425,7 @@ class Topic:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Markup:
header: Optional[Header] = field(
default=None,
@@ -446,7 +443,7 @@ class Markup:
"required": True,
}
)
comment: list[Comment] = field(
comment: List[Comment] = field(
default_factory=list,
metadata={
"name": "Comment",
@@ -454,7 +451,7 @@ class Markup:
"namespace": "",
},
)
viewpoints: list[ViewPoint] = field(
viewpoints: List[ViewPoint] = field(
default_factory=list,
metadata={
"name": "Viewpoints",
+2 -5
View File
@@ -1,11 +1,8 @@
import sys
from dataclasses import dataclass, field
from typing import Optional
DATACLASS_KWARGS = {} if sys.version_info < (3, 10) else {"slots": True, "kw_only": True}
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Project:
name: Optional[str] = field(
default=None,
@@ -24,7 +21,7 @@ class Project:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ProjectExtension:
project: Optional[Project] = field(
default=None,
+1 -4
View File
@@ -1,11 +1,8 @@
import sys
from dataclasses import dataclass, field
from typing import Optional
DATACLASS_KWARGS = {} if sys.version_info < (3, 10) else {"slots": True, "kw_only": True}
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Version:
detailed_version: Optional[str] = field(
default=None,
+26 -29
View File
@@ -1,9 +1,6 @@
import sys
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional
DATACLASS_KWARGS = {} if sys.version_info < (3, 10) else {"slots": True, "kw_only": True}
from typing import List, Optional
class BitmapFormat(Enum):
@@ -11,7 +8,7 @@ class BitmapFormat(Enum):
JPG = "JPG"
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Component:
originating_system: Optional[str] = field(
default=None,
@@ -38,7 +35,7 @@ class Component:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Direction:
x: float = field(
metadata={
@@ -63,7 +60,7 @@ class Direction:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Point:
x: float = field(
metadata={
@@ -88,7 +85,7 @@ class Point:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ViewSetupHints:
spaces_visible: Optional[bool] = field(
default=None,
@@ -113,7 +110,7 @@ class ViewSetupHints:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ClippingPlane:
location: Point = field(
metadata={
@@ -131,12 +128,12 @@ class ClippingPlane:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ComponentColoringColor:
class Meta:
global_type = False
component: list[Component] = field(
component: List[Component] = field(
default_factory=list,
metadata={
"name": "Component",
@@ -154,9 +151,9 @@ class ComponentColoringColor:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ComponentSelection:
component: list[Component] = field(
component: List[Component] = field(
default_factory=list,
metadata={
"name": "Component",
@@ -166,12 +163,12 @@ class ComponentSelection:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ComponentVisibilityExceptions:
class Meta:
global_type = False
component: list[Component] = field(
component: List[Component] = field(
default_factory=list,
metadata={
"name": "Component",
@@ -181,7 +178,7 @@ class ComponentVisibilityExceptions:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Line:
start_point: Point = field(
metadata={
@@ -199,7 +196,7 @@ class Line:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class OrthogonalCamera:
"""
Attributes
@@ -239,7 +236,7 @@ class OrthogonalCamera:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class PerspectiveCamera:
"""
Attributes
@@ -284,7 +281,7 @@ class PerspectiveCamera:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class VisualizationInfoBitmap:
class Meta:
global_type = False
@@ -333,9 +330,9 @@ class VisualizationInfoBitmap:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ComponentColoring:
color: list[ComponentColoringColor] = field(
color: List[ComponentColoringColor] = field(
default_factory=list,
metadata={
"name": "Color",
@@ -345,7 +342,7 @@ class ComponentColoring:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ComponentVisibility:
exceptions: Optional[ComponentVisibilityExceptions] = field(
default=None,
@@ -363,12 +360,12 @@ class ComponentVisibility:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class VisualizationInfoClippingPlanes:
class Meta:
global_type = False
clipping_plane: list[ClippingPlane] = field(
clipping_plane: List[ClippingPlane] = field(
default_factory=list,
metadata={
"name": "ClippingPlane",
@@ -377,12 +374,12 @@ class VisualizationInfoClippingPlanes:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class VisualizationInfoLines:
class Meta:
global_type = False
line: list[Line] = field(
line: List[Line] = field(
default_factory=list,
metadata={
"name": "Line",
@@ -392,7 +389,7 @@ class VisualizationInfoLines:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Components:
view_setup_hints: Optional[ViewSetupHints] = field(
default=None,
@@ -424,7 +421,7 @@ class Components:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class VisualizationInfo:
"""
VisualizationInfo documentation.
@@ -465,7 +462,7 @@ class VisualizationInfo:
"type": "Element",
},
)
bitmap: list[VisualizationInfoBitmap] = field(
bitmap: List[VisualizationInfoBitmap] = field(
default_factory=list,
metadata={
"name": "Bitmap",
+1 -2
View File
@@ -1,6 +1,5 @@
"""BCF XML V2 Topic handler."""
from __future__ import annotations
import datetime
import tempfile
import uuid
@@ -153,7 +152,7 @@ class TopicHandler:
topic_type: str = "",
topic_status: str = "",
xml_handler: Optional[AbstractXmlParserSerializer] = None,
) -> TopicHandler:
) -> "TopicHandler":
"""
Create a new BCF topic.
+1 -2
View File
@@ -1,7 +1,6 @@
import uuid
import zipfile
from typing import Any, Optional, Literal, Union
from collections.abc import Iterable
from typing import Any, Iterable, Optional, Literal, Union
import numpy as np
from ifcopenshell import entity_instance
+21 -21
View File
@@ -21,11 +21,11 @@ import http.server
import os
import tempfile
import time
import urllib.parse
import urllib
import uuid
import webbrowser
from re import A
from typing import Any, Optional
from typing import Any, Optional, Tuple
import requests
@@ -40,7 +40,7 @@ class OAuthReceiver(http.server.BaseHTTPRequestHandler):
self.send_response(200)
self.send_header("Content-type", "text/plain")
self.end_headers()
self.wfile.write(b"You have now authenticated :) You may now close this browser window.")
self.wfile.write("You have now authenticated :) You may now close this browser window.".encode("utf-8"))
class FoundationClient:
@@ -174,7 +174,7 @@ class BcfClient:
except requests.exceptions.HTTPError as e:
print(f"message: {response.reason}' '{response.status_code}' '{ e }")
def post(self, endpoint: str, data: Any = None, params: Any = None) -> tuple[int, str]:
def post(self, endpoint: str, data: Any = None, params: Any = None) -> Tuple[int, str]:
headers = {
"Authorization": f"Bearer {self.foundation_client.get_access_token()}",
"Content-type": "application/json",
@@ -192,7 +192,7 @@ class BcfClient:
print(f"message: {response.reason}' '{response.status_code}, {errh}")
return response.status_code, response.reason
def put(self, endpoint: str, data: Any = None, params: Any = None) -> tuple[int, str]:
def put(self, endpoint: str, data: Any = None, params: Any = None) -> Tuple[int, str]:
headers = {
"Authorization": f"Bearer {self.foundation_client.get_access_token()}",
"Content-type": "application/json",
@@ -210,7 +210,7 @@ class BcfClient:
print(f"message: {response.reason}' '{response.status_code}, {errh}")
return response.status_code, response.reason
def delete(self, endpoint: str, params: Any = None) -> tuple[int, str]:
def delete(self, endpoint: str, params: Any = None) -> Tuple[int, str]:
headers = {
"Authorization": f"Bearer {self.foundation_client.get_access_token()}",
"Content-type": "application/json",
@@ -237,7 +237,7 @@ class BcfClient:
},
)
def update_project(self, project_id: str = "", data: Any = None) -> tuple[int, str]:
def update_project(self, project_id: str = "", data: Any = None) -> Tuple[int, str]:
url = f"{self.baseurl}/projects/{project_id}"
headers = {"Authorization": f"Bearer {self.foundation_client.get_access_token()}"}
resp = requests.put(url, headers=headers, data=data)
@@ -276,16 +276,16 @@ class BcfClient:
},
)
def create_topic(self, project_id: str = "", data: Any = None) -> tuple[int, str]:
def create_topic(self, project_id: str = "", data: Any = None) -> Tuple[int, str]:
return self.post(f"/projects/{project_id}/topics", data=data)
def update_topic(self, project_id: str = "", topic_id: str = "", data: Any = None) -> tuple[int, str]:
def update_topic(self, project_id: str = "", topic_id: str = "", data: Any = None) -> Tuple[int, str]:
return self.put(f"/projects/{project_id}/topics/{topic_id}", data=data)
def delete_topic(self, project_id: str = "", topic_id: str = "") -> tuple[int, str]:
def delete_topic(self, project_id: str = "", topic_id: str = "") -> Tuple[int, str]:
return self.delete(f"/projects/{project_id}/topics/{topic_id}")
def get_snippet(self, project_id: str = "", topic_id: str = "") -> tuple[int, str]:
def get_snippet(self, project_id: str = "", topic_id: str = "") -> Tuple[int, str]:
headers = {
"Authorization": f"Bearer {self.foundation_client.get_access_token()}",
"Content-type": "application/octet-stream",
@@ -332,7 +332,7 @@ class BcfClient:
topic_id: str = "",
data: Any = None,
params: Any = None,
) -> tuple[int, str]:
) -> Tuple[int, str]:
return self.put(
f"/projects/{project_id}/topics/{topic_id}/files",
data=data,
@@ -347,7 +347,7 @@ class BcfClient:
topic_id: str = "",
data: Any = None,
params: Any = None,
) -> tuple[int, str]:
) -> Tuple[int, str]:
return self.post(
f"/projects/{project_id}/topics/{topic_id}/comments",
data=data,
@@ -363,7 +363,7 @@ class BcfClient:
},
)
def delete_comment(self, project_id: str = "", topic_id: str = "", comment_id: str = "") -> tuple[int, str]:
def delete_comment(self, project_id: str = "", topic_id: str = "", comment_id: str = "") -> Tuple[int, str]:
return self.delete(f"/projects/{project_id}/topics/{topic_id}/comments/{comment_id}")
def update_comment(
@@ -372,7 +372,7 @@ class BcfClient:
topic_id: str = "",
comment_id: str = "",
data: Any = None,
) -> tuple[int, str]:
) -> Tuple[int, str]:
return self.put(
f"/projects/{project_id}/topics/{topic_id}/comments/{comment_id}",
data=data,
@@ -387,7 +387,7 @@ class BcfClient:
},
)
def create_viewpoints(self, project_id: str = "", topic_id: str = "", data: Any = None) -> tuple[int, str]:
def create_viewpoints(self, project_id: str = "", topic_id: str = "", data: Any = None) -> Tuple[int, str]:
return self.post(
f"/projects/{project_id}/topics/{topic_id}/viewpoints",
data=data,
@@ -408,7 +408,7 @@ class BcfClient:
project_id: str = "",
topic_id: str = "",
viewpoint_id: str = "",
) -> tuple[int, str]:
) -> Tuple[int, str]:
return self.delete(
f"/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}",
)
@@ -478,7 +478,7 @@ class BcfClient:
project_id: str = "",
topic_id: str = "",
data: Any = None,
) -> tuple[int, str]:
) -> Tuple[int, str]:
return self.put(
f"/projects/{project_id}/topics/{topic_id}/related_topics",
data=data,
@@ -498,7 +498,7 @@ class BcfClient:
project_id: str = "",
topic_id: str = "",
data: Any = None,
) -> tuple[int, str]:
) -> Tuple[int, str]:
return self.post(
f"/projects/{project_id}/topics/{topic_id}/document_references",
data=data,
@@ -510,7 +510,7 @@ class BcfClient:
topic_id: str = "",
document_reference_id: str = "",
data: Any = None,
) -> tuple[int, str]:
) -> Tuple[int, str]:
return self.put(
f"/projects/{project_id}/topics/{topic_id}/document_references/{document_reference_id}",
data=data,
@@ -543,7 +543,7 @@ class BcfClient:
return response.status_code
def get_document(self, project_id: str = "", topic_id: str = "", document_id: str = "") -> tuple[int, str]:
def get_document(self, project_id: str = "", topic_id: str = "", document_id: str = "") -> Tuple[int, str]:
headers = {
"Authorization": f"Bearer {self.foundation_client.get_access_token()}",
"Content-type": "application/octet-stream",
+4 -5
View File
@@ -1,6 +1,5 @@
"""BCF XML V3 handlers."""
from __future__ import annotations
import uuid
import warnings
import zipfile
@@ -31,7 +30,7 @@ class BcfXml:
self._documents: Optional[DocumentsHandler] = None
self._zip_file = self._load_zip_file()
def __enter__(self) -> BcfXml:
def __enter__(self) -> "BcfXml":
return self
def __exit__(self, *args: Any) -> None:
@@ -117,7 +116,7 @@ class BcfXml:
return self._documents
@classmethod
def load(cls, filename: Path, xml_handler: Optional[AbstractXmlParserSerializer] = None) -> Optional[BcfXml]:
def load(cls, filename: Path, xml_handler: Optional[AbstractXmlParserSerializer] = None) -> Optional["BcfXml"]:
"""
Create a BcfXml object from a file.
@@ -142,7 +141,7 @@ class BcfXml:
project_name: Optional[str] = None,
extensions: Optional[mdl.Extensions] = None,
xml_handler: Optional[AbstractXmlParserSerializer] = None,
) -> BcfXml:
) -> "BcfXml":
"""
Create a new BcfXml object.
@@ -240,7 +239,7 @@ class BcfXml:
)
# region Deprecated methods
def new_project(self) -> BcfXml:
def new_project(self) -> "BcfXml":
"""Deprecated method."""
warnings.warn("new_project is deprecated, use create_new instead.", DeprecationWarning)
return self.create_new()
+5 -8
View File
@@ -1,11 +1,8 @@
import sys
from dataclasses import dataclass, field
from typing import Optional
DATACLASS_KWARGS = {} if sys.version_info < (3, 10) else {"slots": True, "kw_only": True}
from typing import List, Optional
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Document:
filename: str = field(
metadata={
@@ -37,12 +34,12 @@ class Document:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class DocumentInfoDocuments:
class Meta:
global_type = False
document: list[Document] = field(
document: List[Document] = field(
default_factory=list,
metadata={
"name": "Document",
@@ -52,7 +49,7 @@ class DocumentInfoDocuments:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class DocumentInfo:
documents: Optional[DocumentInfoDocuments] = field(
default=None,
+16 -19
View File
@@ -1,16 +1,13 @@
import sys
from dataclasses import dataclass, field
from typing import Optional
DATACLASS_KWARGS = {} if sys.version_info < (3, 10) else {"slots": True, "kw_only": True}
from typing import List, Optional
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ExtensionsPriorities:
class Meta:
global_type = False
priority: list[str] = field(
priority: List[str] = field(
default_factory=list,
metadata={
"name": "Priority",
@@ -22,12 +19,12 @@ class ExtensionsPriorities:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ExtensionsSnippetTypes:
class Meta:
global_type = False
snippet_type: list[str] = field(
snippet_type: List[str] = field(
default_factory=list,
metadata={
"name": "SnippetType",
@@ -39,12 +36,12 @@ class ExtensionsSnippetTypes:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ExtensionsStages:
class Meta:
global_type = False
stage: list[str] = field(
stage: List[str] = field(
default_factory=list,
metadata={
"name": "Stage",
@@ -56,12 +53,12 @@ class ExtensionsStages:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ExtensionsTopicLabels:
class Meta:
global_type = False
topic_label: list[str] = field(
topic_label: List[str] = field(
default_factory=list,
metadata={
"name": "TopicLabel",
@@ -73,12 +70,12 @@ class ExtensionsTopicLabels:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ExtensionsTopicStatuses:
class Meta:
global_type = False
topic_status: list[str] = field(
topic_status: List[str] = field(
default_factory=list,
metadata={
"name": "TopicStatus",
@@ -90,12 +87,12 @@ class ExtensionsTopicStatuses:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ExtensionsTopicTypes:
class Meta:
global_type = False
topic_type: list[str] = field(
topic_type: List[str] = field(
default_factory=list,
metadata={
"name": "TopicType",
@@ -107,12 +104,12 @@ class ExtensionsTopicTypes:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ExtensionsUsers:
class Meta:
global_type = False
user: list[str] = field(
user: List[str] = field(
default_factory=list,
metadata={
"name": "User",
@@ -124,7 +121,7 @@ class ExtensionsUsers:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Extensions:
topic_types: Optional[ExtensionsTopicTypes] = field(
default=None,
+25 -28
View File
@@ -1,13 +1,10 @@
import sys
from dataclasses import dataclass, field
from typing import Optional
from typing import List, Optional
from xsdata.models.datatype import XmlDateTime
DATACLASS_KWARGS = {} if sys.version_info < (3, 10) else {"slots": True, "kw_only": True}
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class BimSnippet:
reference: str = field(
metadata={
@@ -47,7 +44,7 @@ class BimSnippet:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class CommentViewpoint:
class Meta:
global_type = False
@@ -62,7 +59,7 @@ class CommentViewpoint:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class DocumentReference:
document_guid: Optional[str] = field(
default=None,
@@ -103,7 +100,7 @@ class DocumentReference:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class File:
filename: Optional[str] = field(
default=None,
@@ -160,12 +157,12 @@ class File:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class TopicLabels:
class Meta:
global_type = False
label: list[str] = field(
label: List[str] = field(
default_factory=list,
metadata={
"name": "Label",
@@ -177,12 +174,12 @@ class TopicLabels:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class TopicReferenceLinks:
class Meta:
global_type = False
reference_link: list[str] = field(
reference_link: List[str] = field(
default_factory=list,
metadata={
"name": "ReferenceLink",
@@ -194,7 +191,7 @@ class TopicReferenceLinks:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class TopicRelatedTopicsRelatedTopic:
class Meta:
global_type = False
@@ -209,7 +206,7 @@ class TopicRelatedTopicsRelatedTopic:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ViewPoint:
viewpoint: Optional[str] = field(
default=None,
@@ -249,7 +246,7 @@ class ViewPoint:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Comment:
date: XmlDateTime = field(
metadata={
@@ -315,12 +312,12 @@ class Comment:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class HeaderFiles:
class Meta:
global_type = False
file: list[File] = field(
file: List[File] = field(
default_factory=list,
metadata={
"name": "File",
@@ -330,12 +327,12 @@ class HeaderFiles:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class TopicDocumentReferences:
class Meta:
global_type = False
document_reference: list[DocumentReference] = field(
document_reference: List[DocumentReference] = field(
default_factory=list,
metadata={
"name": "DocumentReference",
@@ -345,12 +342,12 @@ class TopicDocumentReferences:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class TopicRelatedTopics:
class Meta:
global_type = False
related_topic: list[TopicRelatedTopicsRelatedTopic] = field(
related_topic: List[TopicRelatedTopicsRelatedTopic] = field(
default_factory=list,
metadata={
"name": "RelatedTopic",
@@ -360,12 +357,12 @@ class TopicRelatedTopics:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class TopicViewpoints:
class Meta:
global_type = False
view_point: list[ViewPoint] = field(
view_point: List[ViewPoint] = field(
default_factory=list,
metadata={
"name": "ViewPoint",
@@ -375,7 +372,7 @@ class TopicViewpoints:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Header:
files: Optional[HeaderFiles] = field(
default=None,
@@ -387,12 +384,12 @@ class Header:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class TopicComments:
class Meta:
global_type = False
comment: list[Comment] = field(
comment: List[Comment] = field(
default_factory=list,
metadata={
"name": "Comment",
@@ -402,7 +399,7 @@ class TopicComments:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Topic:
reference_links: Optional[TopicReferenceLinks] = field(
default=None,
@@ -599,7 +596,7 @@ class Topic:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Markup:
header: Optional[Header] = field(
default=None,
+2 -5
View File
@@ -1,11 +1,8 @@
import sys
from dataclasses import dataclass, field
from typing import Optional
DATACLASS_KWARGS = {} if sys.version_info < (3, 10) else {"slots": True, "kw_only": True}
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Project:
name: Optional[str] = field(
default=None,
@@ -28,7 +25,7 @@ class Project:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ProjectInfo:
project: Project = field(
metadata={
+1 -4
View File
@@ -1,10 +1,7 @@
import sys
from dataclasses import dataclass, field
DATACLASS_KWARGS = {} if sys.version_info < (3, 10) else {"slots": True, "kw_only": True}
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Version:
version_id: str = field(
metadata={
+28 -32
View File
@@ -1,10 +1,6 @@
import sys
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional
DATACLASS_KWARGS = {} if sys.version_info < (3, 10) else {"slots": True, "kw_only": True}
from typing import List, Optional
class BitmapFormat(Enum):
@@ -12,7 +8,7 @@ class BitmapFormat(Enum):
JPG = "jpg"
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Component:
originating_system: Optional[str] = field(
default=None,
@@ -43,7 +39,7 @@ class Component:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Direction:
x: float = field(
metadata={
@@ -68,7 +64,7 @@ class Direction:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Point:
x: float = field(
metadata={
@@ -93,7 +89,7 @@ class Point:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ViewSetupHints:
spaces_visible: bool = field(
default=False,
@@ -118,7 +114,7 @@ class ViewSetupHints:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Bitmap:
format: BitmapFormat = field(
metadata={
@@ -166,7 +162,7 @@ class Bitmap:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ClippingPlane:
location: Point = field(
metadata={
@@ -184,12 +180,12 @@ class ClippingPlane:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ComponentColoringColorComponents:
class Meta:
global_type = False
component: list[Component] = field(
component: List[Component] = field(
default_factory=list,
metadata={
"name": "Component",
@@ -199,9 +195,9 @@ class ComponentColoringColorComponents:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ComponentSelection:
component: list[Component] = field(
component: List[Component] = field(
default_factory=list,
metadata={
"name": "Component",
@@ -210,12 +206,12 @@ class ComponentSelection:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ComponentVisibilityExceptions:
class Meta:
global_type = False
component: list[Component] = field(
component: List[Component] = field(
default_factory=list,
metadata={
"name": "Component",
@@ -224,7 +220,7 @@ class ComponentVisibilityExceptions:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Line:
start_point: Point = field(
metadata={
@@ -242,7 +238,7 @@ class Line:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class OrthogonalCamera:
"""
Attributes
@@ -292,7 +288,7 @@ class OrthogonalCamera:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class PerspectiveCamera:
"""
Attributes
@@ -348,7 +344,7 @@ class PerspectiveCamera:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ComponentColoringColor:
class Meta:
global_type = False
@@ -370,7 +366,7 @@ class ComponentColoringColor:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ComponentVisibility:
view_setup_hints: Optional[ViewSetupHints] = field(
default=None,
@@ -395,12 +391,12 @@ class ComponentVisibility:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class VisualizationInfoBitmaps:
class Meta:
global_type = False
bitmap: list[Bitmap] = field(
bitmap: List[Bitmap] = field(
default_factory=list,
metadata={
"name": "Bitmap",
@@ -409,12 +405,12 @@ class VisualizationInfoBitmaps:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class VisualizationInfoClippingPlanes:
class Meta:
global_type = False
clipping_plane: list[ClippingPlane] = field(
clipping_plane: List[ClippingPlane] = field(
default_factory=list,
metadata={
"name": "ClippingPlane",
@@ -423,12 +419,12 @@ class VisualizationInfoClippingPlanes:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class VisualizationInfoLines:
class Meta:
global_type = False
line: list[Line] = field(
line: List[Line] = field(
default_factory=list,
metadata={
"name": "Line",
@@ -437,9 +433,9 @@ class VisualizationInfoLines:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ComponentColoring:
color: list[ComponentColoringColor] = field(
color: List[ComponentColoringColor] = field(
default_factory=list,
metadata={
"name": "Color",
@@ -448,7 +444,7 @@ class ComponentColoring:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Components:
selection: Optional[ComponentSelection] = field(
default=None,
@@ -473,7 +469,7 @@ class Components:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class VisualizationInfo:
"""
VisualizationInfo documentation.
+3 -4
View File
@@ -1,6 +1,5 @@
"""BCF XML V3 Topic handler."""
from __future__ import annotations
import datetime
import uuid
import zipfile
@@ -92,12 +91,12 @@ class TopicHandler:
self._bim_snippet = value
@property
def viewpoints(self) -> dict[str, VisualizationInfoHandler]:
def viewpoints(self) -> dict[str, "VisualizationInfoHandler"]:
if self._viewpoints is None:
self._viewpoints = self._load_viewpoints()
return self._viewpoints
def _load_viewpoints(self) -> dict[str, VisualizationInfoHandler]:
def _load_viewpoints(self) -> dict[str, "VisualizationInfoHandler"]:
if self._topic_dir and self.topic.viewpoints and (viewpoints := self.topic.viewpoints.view_point):
return VisualizationInfoHandler.from_topic_viewpoints(self._topic_dir, viewpoints)
return {}
@@ -140,7 +139,7 @@ class TopicHandler:
topic_type: str = "",
topic_status: str = "",
xml_handler: Optional[AbstractXmlParserSerializer] = None,
) -> TopicHandler:
) -> "TopicHandler":
"""
Create a new BCF topic.
+1 -2
View File
@@ -1,7 +1,6 @@
import uuid
import zipfile
from typing import Any, Optional, Literal, Union
from collections.abc import Iterable
from typing import Any, Iterable, Optional, Literal, Union
import numpy as np
from ifcopenshell import entity_instance
+3 -3
View File
@@ -1,6 +1,6 @@
"""XML Parser and Serializer factories."""
from typing import Optional, Protocol, TypeVar
from typing import Optional, Protocol, Type, TypeVar
from xsdata.formats.dataclass.context import XmlContext
from xsdata.formats.dataclass.parsers import XmlParser
@@ -29,7 +29,7 @@ T = TypeVar("T")
class AbstractXmlParserSerializer(Protocol):
"""XML Parser and serializer wrapper."""
def parse(self, xml: bytes, clazz: type[T]) -> T:
def parse(self, xml: bytes, clazz: Type[T]) -> T:
"""
Parse an XML file to an object.
@@ -61,7 +61,7 @@ class XmlParserSerializer:
self.parser = build_xml_parser(self.context)
self.serializer = build_serializer(self.context)
def parse(self, xml: bytes, clazz: type[T]) -> T:
def parse(self, xml: bytes, clazz: Type[T]) -> T:
"""
Parse an XML file to an object.
-1
View File
@@ -16,7 +16,6 @@ dependencies = [
"xsdata>=24.4",
"numpy",
"ifcopenshell",
"requests",
]
version = "0.0.0"
classifiers = [
+4 -7
View File
@@ -16,7 +16,6 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
SHELL := sh
PYTHON:=python3.11
PIP:=pip3.11
PATCH:=patch
@@ -87,7 +86,7 @@ BLENDER_PLATFORM:=windows-x64
endif
# Current build commit hash.
OLD:=ec13294
OLD:=c49ca69
.PHONY: bump
bump:
ifndef NEW
@@ -138,7 +137,7 @@ endif
# odfpy doesn't come with its own wheel, so whee'l (get it?) create it ourselves!
cd build && . env/$(VENV_ACTIVATE) && $(PIP) wheel odfpy --wheel-dir=./wheels
# Required by IFCCityJSON
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download "cjio >=0.8, <0.10" --dest=./wheels
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download cjio --dest=./wheels
# Required in general for sorting all sorts of stuff in a nice way
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download natsort --dest=./wheels
# Provides express rule validation for ifcopenshell.validate
@@ -193,15 +192,13 @@ else
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download pyradiance $(PYPI_PLATFORM) --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --dest=./wheels
endif
# jQuery is required for web UI functionality
cd build/bonsai/bim/data/webui/static/js/ && wget https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js
# Provides jsgantt-improved supports for web-based construction sequencing gantt charts
cd build/bonsai/bim/data/gantt/ && wget https://raw.githubusercontent.com/jsGanttImproved/jsgantt-improved/master/dist/jsgantt.js
cd build/bonsai/bim/data/gantt/ && wget https://raw.githubusercontent.com/jsGanttImproved/jsgantt-improved/master/dist/jsgantt.css
# Provides IFCJSON functionality
cd build && wget -O ifc2json.zip https://github.com/IFCJSON-Team/IFC2JSON_python/archive/refs/heads/master.zip
# TODO: replace with main repo if https://github.com/IFCJSON-Team/IFC2JSON_python/pull/3 is merged.
cd build && wget -O ifc2json.zip https://github.com/Moult/IFC2JSON_python/archive/refs/heads/feature-ios-v0.8.0.zip
cd build && unzip ifc2json.zip && rm ifc2json.zip
# IFCJSON doesn't have pyproject.toml, so we use python command.
cd build && . env/$(VENV_ACTIVATE) && cd IFC2JSON_python-*/file_converters && \
+11 -27
View File
@@ -28,10 +28,6 @@ IN_BLENDER = sys.modules.get("bpy", None)
if IN_BLENDER:
import bpy
# This file is executed twice - first as a bonsai-extension
# and then as a bonsai-package.
IN_PACKAGE = __package__ == "bonsai"
import re
import platform
import traceback
@@ -40,8 +36,7 @@ import uuid
import shutil
from collections import deque
from pathlib import Path
from typing import Union, Any
from collections.abc import Generator
from typing import Union, Any, Generator
last_commit_hash = "8888888"
@@ -223,21 +218,17 @@ if IN_BLENDER:
info["binary_python_version"] = version
return info
def update_commit_data() -> None:
try:
import git
try:
import git
global last_commit_hash
global last_commit_date
path = Path(__file__).resolve().parent
repo = git.Repo(str(path), search_parent_directories=True)
last_commit_hash = repo.head.object.hexsha
last_commit_date = repo.head.object.committed_datetime.isoformat()
except:
pass
if IN_PACKAGE:
update_commit_data()
# We can't just use __file__ as bonsai/__init__.py is typically not symlinked
# as Blender have errors symlinking main addon package file.
path = Path(__file__).resolve().parent
repo = git.Repo(str(path), search_parent_directories=True)
last_commit_hash = repo.head.object.hexsha
last_commit_date = repo.head.object.committed_datetime.isoformat()
except:
pass
try:
import ifcopenshell.api
@@ -253,12 +244,6 @@ if IN_BLENDER:
ifcopenshell.api.add_pre_listener("*", "action_logger", log_api)
def purge_cache():
"""Purge cache left from previous session (e.g. after reload or update)."""
import bonsai.tool as tool
tool.Blender.get_bonsai_version.cache_clear()
def register():
if platform.system() == "Windows":
clean_up_dlls_safe_links()
@@ -276,7 +261,6 @@ if IN_BLENDER:
bonsai.REINSTALLED_BBIM_VERSION = current_version
bonsai.bim.register()
purge_cache()
def unregister():
if platform.system() == "Windows":
+5 -14
View File
@@ -20,10 +20,8 @@ import os
import bpy
import bpy.utils.previews
import importlib
from bpy_extras.io_utils import ImportHelper, ExportHelper
from . import handler, ui, prop, operator
from typing import Union
from collections.abc import Callable
from typing import Callable, Union
try:
from bonsai.translations import translations_dict
@@ -97,16 +95,14 @@ for name in modules.keys():
classes = [
operator.AddIfcFile,
operator.BIM_OT_add_section_plane,
operator.BIM_OT_delete_object,
operator.BIM_OT_remove_section_plane,
operator.BIM_OT_select_entity,
operator.BIM_OT_select_entity_by_guid,
operator.BIM_OT_select_object,
operator.BIM_OT_show_description,
operator.BIM_OT_multiple_file_selector,
operator.BIM_OT_attribute_add_subitem,
operator.BIM_OT_attribute_remove_subitem,
operator.ClippingPlaneCutWithCappings,
operator.CloseBlendWarning,
operator.CloseError,
@@ -118,16 +114,15 @@ classes = [
operator.OpenUpstream,
operator.OpenUri,
operator.ReloadIfcFile,
operator.RemoveIfcFile,
operator.RevertClippingPlaneCut,
operator.SelectDir,
operator.SelectIfcFile,
operator.SelectURIAttribute,
operator.SetTab,
operator.SwitchTab,
operator.ShowSystemInfo,
prop.StrProperty,
operator.BIM_OT_enum_property_search, # /!\ Register AFTER prop.StrProperty
operator.BIM_OT_attribute_search_values,
prop.ObjProperty,
prop.MultipleFileSelect,
prop.Attribute,
@@ -136,6 +131,7 @@ classes = [
prop.BIMProperties,
prop.IfcParameter,
prop.PsetQto,
prop.GlobalId,
prop.BIMObjectProperties,
prop.BIMCollectionProperties,
prop.BIMMeshProperties,
@@ -153,10 +149,10 @@ classes = [
ui.BIM_PT_tab_new_project_wizard,
ui.BIM_PT_tab_project_info,
ui.BIM_PT_tab_spatial,
ui.BIM_PT_tab_grouping_and_filtering,
ui.BIM_PT_tab_project_setup,
ui.BIM_PT_tab_geometry,
ui.BIM_PT_tab_stakeholders,
ui.BIM_PT_tab_grouping_and_filtering,
# Object information
ui.BIM_PT_tab_object_metadata,
ui.BIM_PT_tab_misc,
@@ -225,12 +221,7 @@ def on_register(scene):
def register():
for cls in classes:
# Prevent crashes in Blender 4.4.0, see #6420.
if issubclass(cls, (ImportHelper, ExportHelper)):
assert getattr(cls, "bl_description", "") or cls.__doc__, cls
bpy.utils.register_class(cls)
bpy.app.handlers.depsgraph_update_post.append(on_register)
bpy.app.handlers.undo_post.append(handler.undo_post)
bpy.app.handlers.redo_post.append(handler.redo_post)
@@ -47,6 +47,8 @@
"scene.render.simplify_gpencil_shader_fx": true,
"scene.render.simplify_gpencil_tint": true,
"scene.render.simplify_gpencil_view_fill": true,
"scene.render.simplify_shadows": 1.0,
"scene.render.simplify_shadows_render": 1.0,
"scene.render.simplify_subdivision": 6,
"scene.render.simplify_subdivision_render": 6,
"scene.render.simplify_volumes": 1.0,
@@ -145,7 +147,7 @@
"scene.display.shading.use_scene_world_render": false,
"scene.display.shading.use_studiolight_view_rotation": true,
"scene.display.shading.use_world_space_lighting": false,
"scene.display.shading.wireframe_color_type": "THEME",
"scene.display.shading.wireframe_color_type": "MATERIAL",
"scene.display.shading.xray_alpha": 0.5,
"scene.display.shading.xray_alpha_wireframe": 0.0,
"scene.display.light_direction": [
@@ -160,6 +162,7 @@
"scene.display.shadow_focus": 0.0,
"scene.display.shadow_shift": 0.10000000149011612,
"scene.display.viewport_aa": "FXAA",
"space.overlay.backwire_opacity": 0.5,
"space.overlay.bone_wire_alpha": 1.0,
"space.overlay.display_handle": "SELECTED",
"space.overlay.fade_inactive_alpha": 0.4000000059604645,
@@ -186,6 +189,7 @@
"space.overlay.show_edge_crease": true,
"space.overlay.show_edge_seams": true,
"space.overlay.show_edge_sharp": true,
"space.overlay.show_edges": false,
"space.overlay.show_extra_edge_angle": false,
"space.overlay.show_extra_edge_length": false,
"space.overlay.show_extra_face_angle": false,
@@ -204,6 +208,7 @@
"space.overlay.show_motion_paths": true,
"space.overlay.show_object_origins": false,
"space.overlay.show_object_origins_all": false,
"space.overlay.show_occlude_wire": false,
"space.overlay.show_onion_skins": false,
"space.overlay.show_ortho_grid": true,
"space.overlay.show_outline_selected": true,
@@ -293,6 +298,8 @@
"scene.render.simplify_gpencil_shader_fx": true,
"scene.render.simplify_gpencil_tint": true,
"scene.render.simplify_gpencil_view_fill": true,
"scene.render.simplify_shadows": 1.0,
"scene.render.simplify_shadows_render": 1.0,
"scene.render.simplify_subdivision": 6,
"scene.render.simplify_subdivision_render": 6,
"scene.render.simplify_volumes": 1.0,
@@ -391,7 +398,7 @@
"scene.display.shading.use_scene_world_render": false,
"scene.display.shading.use_studiolight_view_rotation": true,
"scene.display.shading.use_world_space_lighting": false,
"scene.display.shading.wireframe_color_type": "THEME",
"scene.display.shading.wireframe_color_type": "MATERIAL",
"scene.display.shading.xray_alpha": 0.5,
"scene.display.shading.xray_alpha_wireframe": 0.0,
"scene.display.light_direction": [
@@ -406,6 +413,7 @@
"scene.display.shadow_focus": 0.0,
"scene.display.shadow_shift": 0.10000000149011612,
"scene.display.viewport_aa": "FXAA",
"space.overlay.backwire_opacity": 0.5,
"space.overlay.bone_wire_alpha": 1.0,
"space.overlay.display_handle": "SELECTED",
"space.overlay.fade_inactive_alpha": 0.4000000059604645,
@@ -432,6 +440,7 @@
"space.overlay.show_edge_crease": true,
"space.overlay.show_edge_seams": true,
"space.overlay.show_edge_sharp": true,
"space.overlay.show_edges": false,
"space.overlay.show_extra_edge_angle": false,
"space.overlay.show_extra_edge_length": false,
"space.overlay.show_extra_face_angle": false,
@@ -450,6 +459,7 @@
"space.overlay.show_motion_paths": true,
"space.overlay.show_object_origins": false,
"space.overlay.show_object_origins_all": false,
"space.overlay.show_occlude_wire": false,
"space.overlay.show_onion_skins": false,
"space.overlay.show_ortho_grid": true,
"space.overlay.show_outline_selected": true,
@@ -499,10 +509,6 @@
0.05087608844041824,
0.05087608844041824
],
"scene.eevee.use_shadows": true,
"scene.eevee.shadow_ray_count": 1,
"scene.eevee.shadow_step_count": 6,
"scene.eevee.shadow_resolution_scale": 1.0,
"scene.render.bake_bias": 0.0010000000474974513,
"scene.render.bake_margin": 16,
"scene.render.bake_margin_type": "ADJACENT_FACES",
@@ -514,7 +520,7 @@
"scene.render.border_min_x": 0.0,
"scene.render.border_min_y": 0.0,
"scene.render.dither_intensity": 1.0,
"scene.render.engine": "BLENDER_EEVEE_NEXT",
"scene.render.engine": "BLENDER_EEVEE",
"scene.render.film_transparent": false,
"scene.render.filter_size": 1.5,
"scene.render.fps": 24,
@@ -543,6 +549,8 @@
"scene.render.simplify_gpencil_shader_fx": true,
"scene.render.simplify_gpencil_tint": true,
"scene.render.simplify_gpencil_view_fill": true,
"scene.render.simplify_shadows": 1.0,
"scene.render.simplify_shadows_render": 1.0,
"scene.render.simplify_subdivision": 6,
"scene.render.simplify_subdivision_render": 6,
"scene.render.simplify_volumes": 1.0,
@@ -641,7 +649,7 @@
"scene.display.shading.use_scene_world_render": false,
"scene.display.shading.use_studiolight_view_rotation": true,
"scene.display.shading.use_world_space_lighting": false,
"scene.display.shading.wireframe_color_type": "THEME",
"scene.display.shading.wireframe_color_type": "MATERIAL",
"scene.display.shading.xray_alpha": 0.5,
"scene.display.shading.xray_alpha_wireframe": 0.0,
"scene.display.light_direction": [
@@ -656,6 +664,7 @@
"scene.display.shadow_focus": 0.0,
"scene.display.shadow_shift": 0.10000000149011612,
"scene.display.viewport_aa": "FXAA",
"space.overlay.backwire_opacity": 0.5,
"space.overlay.bone_wire_alpha": 1.0,
"space.overlay.display_handle": "SELECTED",
"space.overlay.fade_inactive_alpha": 0.4000000059604645,
@@ -682,6 +691,7 @@
"space.overlay.show_edge_crease": true,
"space.overlay.show_edge_seams": true,
"space.overlay.show_edge_sharp": true,
"space.overlay.show_edges": false,
"space.overlay.show_extra_edge_angle": false,
"space.overlay.show_extra_edge_length": false,
"space.overlay.show_extra_face_angle": false,
@@ -700,6 +710,7 @@
"space.overlay.show_motion_paths": true,
"space.overlay.show_object_origins": true,
"space.overlay.show_object_origins_all": false,
"space.overlay.show_occlude_wire": false,
"space.overlay.show_onion_skins": false,
"space.overlay.show_ortho_grid": true,
"space.overlay.show_outline_selected": true,
@@ -1,11 +1,11 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION($,'2;1');
FILE_NAME('EPset_Drawing.ifc','2020-01-01T00:00:00',$,$,'EPset_Drawing','EPset_Drawing',$);
FILE_DESCRIPTION((),'2;1');
FILE_NAME('EPset_Drawing.ifc','2020-01-01T00:00:00',(),(),'EPset_Drawing','EPset_Drawing',$);
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation/DRAWING',(#23,#22,#27,#24,#19,#12,#26,#9,#8,#7,#6,#4,#18,#11,#5,#20,#25,#14,#10,#17,#28,#16,#3,#21,#13,#15,#2));
#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation/DRAWING',(#2,#3,#4,#5,#6,#7,#8,#9,#10,#11,#12,#13,#14,#15,#16,#17,#18,#19,#20,#21,#22,#23,#24,#25,#26,#27));
#2=IFCSIMPLEPROPERTYTEMPLATE('23JavTMk98ZxXhrUEnjAcf',$,'TargetView','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#3=IFCSIMPLEPROPERTYTEMPLATE('1yVWUt5H9DAOuu0OaMMLpe',$,'Scale','The scale of this drawing represented as a numerator and denominator, such as 1/100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#4=IFCSIMPLEPROPERTYTEMPLATE('3gsuPBtU93b8f0gg1pjkq6',$,'HumanScale','The scale of this drawing in human readable format, such as 1:100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
@@ -14,7 +14,7 @@ DATA;
#7=IFCSIMPLEPROPERTYTEMPLATE('2j2ZEZR8X5tONm7kli5hM6',$,'HasLinework','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#8=IFCSIMPLEPROPERTYTEMPLATE('1ttChRysH9UuEX2FeMj5Hu',$,'HasAnnotation','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#9=IFCSIMPLEPROPERTYTEMPLATE('2NPPxuABv1huDTVh32TFgw',$,'GlobalReferencing','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#10=IFCSIMPLEPROPERTYTEMPLATE('10hT_1zrzEbRRKMXYAWvtD',$,'Metadata','Comma separated list of selector expressions to evaluate for each drawing elementand add results to their ''class'' attribute.\X2\000A\X0\E.g. ''Name, id'' would add to ''class'' value similar to ''Name-Wall id-1220''.\X2\000A\X0\Then it can be used to applied css styles based on the resulting class.\X2\000A\X0\If attribute is not present on the element, then it won''t be added to it''s ''class''.',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#10=IFCSIMPLEPROPERTYTEMPLATE('10hT_1zrzEbRRKMXYAWvtD',$,'Metadata','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#11=IFCSIMPLEPROPERTYTEMPLATE('3Z0BXPSG5CWgtI33ioV7aj',$,'Include','Selector expression to include ifc elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#12=IFCSIMPLEPROPERTYTEMPLATE('1RVts_g3PAw98PJA2yL3bO',$,'Exclude','Selector expression to exclude ifc elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#13=IFCSIMPLEPROPERTYTEMPLATE('0c1$8NpYDEaBiJrj16jHIo',$,'Stylesheet','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
@@ -32,6 +32,5 @@ DATA;
#25=IFCSIMPLEPROPERTYTEMPLATE('3LHwCrOcb6Y8ozfJZ7Ay$c',$,'LineworkMode','Method to use for line work',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#26=IFCSIMPLEPROPERTYTEMPLATE('2iwERDOW55Pf4hCbuFRe1Q',$,'FillMode','Method to fill areas seen in projection',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#27=IFCSIMPLEPROPERTYTEMPLATE('1YF$qLzBzF19Io8aB2N8cE',$,'CutMode','Method for cutting geometry',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#28=IFCSIMPLEPROPERTYTEMPLATE('1YSnFzurrEyRNtoLdmmddP',$,'BringToFront','The objects with these SVG classes will render in front of all other objects.Ex: IfcBeam, IfcColumn',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
ENDSEC;
END-ISO-10303-21;
@@ -1 +0,0 @@
SVG [mustache](https://mustache.github.io/) templates that will be used for sheets and fill be filled with infromation from the sheet's IfcDocumentInformation attributes (e.g. Identification, Name, Revision, etc).
@@ -21,7 +21,7 @@
/>
<script
type="text/javascript"
src="/static/js/jquery.min.js"
src="https://code.jquery.com/jquery-3.6.0.min.js"
></script>
<script
type="text/javascript"
@@ -19,7 +19,7 @@
/>
<script
type="text/javascript"
src="/static/js/jquery.min.js"
src="https://code.jquery.com/jquery-3.6.0.min.js"
></script>
<script
type="text/javascript"
@@ -24,7 +24,7 @@
/>
<script
type="text/javascript"
src="/static/js/jquery.min.js"
src="https://code.jquery.com/jquery-3.6.0.min.js"
></script>
<script
type="text/javascript"
@@ -17,7 +17,7 @@
/>
<script
type="text/javascript"
src="/static/js/jquery.min.js"
src="https://code.jquery.com/jquery-3.6.0.min.js"
></script>
<script
type="text/javascript"
@@ -21,7 +21,7 @@
/>
<script
type="text/javascript"
src="/static/js/jquery.min.js"
src="https://code.jquery.com/jquery-3.6.0.min.js"
></script>
<script
type="text/javascript"
+35 -1
View File
@@ -47,6 +47,7 @@ class IfcExporter:
self.set_header()
IfcStore.update_cache()
self.sync_all_objects()
self.sync_edited_objects()
tool.Project.save_linked_models_to_ifc()
extension = self.ifc_export_settings.output_file.split(".")[-1].lower()
if extension == "ifczip":
@@ -76,7 +77,11 @@ class IfcExporter:
def set_header(self):
self.file.wrapped_data.header.file_name.name = os.path.basename(self.ifc_export_settings.output_file)
self.file.wrapped_data.header.file_name.time_stamp = (
datetime.datetime.utcnow().replace(tzinfo=datetime.UTC).astimezone().replace(microsecond=0).isoformat()
datetime.datetime.utcnow()
.replace(tzinfo=datetime.timezone.utc)
.astimezone()
.replace(microsecond=0)
.isoformat()
)
self.file.wrapped_data.header.file_name.preprocessor_version = "IfcOpenShell {}".format(ifcopenshell.version)
self.file.wrapped_data.header.file_name.originating_system = "{} {}".format(
@@ -96,12 +101,41 @@ class IfcExporter:
result = self.sync_object_placement(obj)
if result:
results.append(result)
result = self.sync_object_material(obj)
# TODO: sync_object_material always returns None
# so it's never really appended
if result:
results.append(result)
except ReferenceError:
pass # The object is likely deleted
return results
def sync_edited_objects(self) -> list[ifcopenshell.entity_instance]:
results: list[ifcopenshell.entity_instance] = []
for obj in IfcStore.edited_objs.copy():
if not obj:
continue
if not tool.Blender.is_valid_data_block(obj):
continue
element = tool.Ifc.get_entity(obj)
if element:
results.append(element)
bpy.ops.bim.update_representation(obj=obj.name)
IfcStore.edited_objs.clear()
return results
def sync_object_material(self, obj: bpy.types.Object) -> None:
if not obj.data or not isinstance(obj.data, bpy.types.Mesh):
return
if not self.has_changed_materials(obj):
return
bpy.ops.bim.update_representation(obj=obj.name)
def has_changed_materials(self, obj: bpy.types.Object) -> bool:
mprops = tool.Geometry.get_mesh_props(obj.data)
checksum = mprops.material_checksum
return checksum != tool.Geometry.get_material_checksum(obj)
def sync_object_placement(self, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
element = self.file.by_id(tool.Blender.get_object_bim_props(obj).ifc_definition_id)
if tool.Geometry.is_scaled(obj):
+14 -65
View File
@@ -24,18 +24,16 @@ import ifcopenshell.util.unit
import ifcopenshell.api.owner.settings
import bonsai.bim
import bonsai.tool as tool
import weakref
from bpy.app.handlers import persistent
from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.model.data import AuthoringData
from bonsai.bim.module.aggregate.decorator import AggregateDecorator
from bonsai.bim.module.georeference.decorator import GeoreferenceDecorator
from bonsai.bim.module.model.decorator import WallAxisDecorator, SlabDirectionDecorator, BoundingBoxDecorator
from bonsai.bim.module.model.decorator import WallAxisDecorator, SlabDirectionDecorator
from bonsai.bim.module.nest.decorator import NestDecorator
from mathutils import Vector
from math import cos
from typing import Union
from collections.abc import Callable
from typing import Union, Callable
cwd = os.path.dirname(os.path.realpath(__file__))
@@ -258,73 +256,31 @@ def redo_post(scene: bpy.types.Scene) -> None:
tool.Ifc.rebuild_element_maps()
# Cache is important as those entities will be retrieved very often,
# for every IfcOwnerHistory creation or update.
class SettingsCache:
APPLICATION_ID: Union[int, None] = None
USER_ID: Union[int, None] = None
_file: Union[weakref.ReferenceType[ifcopenshell.file], None] = None
@classmethod
def get_file(cls) -> Union[ifcopenshell.file, None]:
if cls._file is None:
return None
return cls._file()
@classmethod
def set_file(cls, file: ifcopenshell.file) -> None:
cls._file = weakref.ref(file)
def get_application(ifc: ifcopenshell.file) -> ifcopenshell.entity_instance:
if SettingsCache.get_file() is ifc and SettingsCache.APPLICATION_ID is not None:
try:
app = ifc.by_id(SettingsCache.APPLICATION_ID)
return app
except RuntimeError:
pass
# Use only main part from the version to avoid flooding advanced users projects with IfcApplications.
version = tool.Blender.get_bonsai_version().split("-")[0]
identifier = f"Bonsai-{version}"
# TODO: cache this for even faster application retrieval. It honestly makes a difference on long scripts.
version = tool.Blender.get_bonsai_version()
for element in ifc.by_type("IfcApplication"):
if element.ApplicationIdentifier == identifier:
if element.ApplicationIdentifier == "Bonsai" and element.Version == version:
return element
application_developer = next((org for org in ifc.by_type("IfcOrganization") if org.Name == "IfcOpenShell"), None)
application = ifcopenshell.api.owner.add_application(
return ifcopenshell.api.run(
"owner.add_application",
ifc,
application_developer=application_developer,
version=version,
application_full_name="Bonsai",
application_identifier=identifier,
application_identifier="Bonsai",
)
SettingsCache.APPLICATION_ID = application.id()
SettingsCache.set_file(ifc)
return application
def get_user(ifc: ifcopenshell.file) -> Union[ifcopenshell.entity_instance, None]:
# TODO: cache this for even faster application retrieval. It honestly makes a difference on long scripts.
if SettingsCache.get_file() is ifc and SettingsCache.USER_ID is not None:
try:
user = ifc.by_id(SettingsCache.USER_ID)
return user
except RuntimeError:
pass
if pao := next(iter(ifc.by_type("IfcPersonAndOrganization")), None):
SettingsCache.USER_ID = pao.id()
SettingsCache.set_file(ifc)
return pao
elif ifc.schema == "IFC2X3":
if (person := next(iter(ifc.by_type("IfcPerson")), None)) is None:
person = ifcopenshell.api.owner.add_person(ifc)
person = tool.Ifc.run("owner.add_person")
if (organization := next(iter(ifc.by_type("IfcOrganization")), None)) is None:
organization = ifcopenshell.api.owner.add_organisation(ifc)
pao = ifcopenshell.api.owner.add_person_and_organisation(ifc, person=person, organisation=organization)
SettingsCache.USER_ID = pao.id()
SettingsCache.set_file(ifc)
organization = tool.Ifc.run("owner.add_organisation")
pao = tool.Ifc.run("owner.add_person_and_organisation", person=person, organisation=organization)
return pao
@@ -391,14 +347,9 @@ def load_post(scene):
# Bonsai overlays
georeference_props = tool.Georeference.get_georeference_props()
aggregate_props = tool.Aggregate.get_aggregate_props()
nest_props = tool.Nest.get_nest_props()
aggregate_props = bpy.context.scene.BIMAggregateProperties
nest_props = bpy.context.scene.BIMNestProperties
model_props = tool.Model.get_model_props()
GeoreferenceDecorator.uninstall()
AggregateDecorator.uninstall()
NestDecorator.uninstall()
WallAxisDecorator.uninstall()
SlabDirectionDecorator.uninstall()
if georeference_props.should_visualise:
GeoreferenceDecorator.install(bpy.context)
if aggregate_props.aggregate_decorator:
@@ -409,10 +360,8 @@ def load_post(scene):
WallAxisDecorator.install(bpy.context)
if model_props.show_slab_direction:
SlabDirectionDecorator.install(bpy.context)
if model_props.show_bounding_box:
BoundingBoxDecorator.install(bpy.context)
if preferences.should_use_snap and (scene := bpy.context.scene):
if scene := bpy.context.scene:
# Snapping is off by default in Blender, but in BIM, it's more useful to be on
scene.tool_settings.use_snap = True
# Match default Bonsai snaps
+25 -105
View File
@@ -27,8 +27,7 @@ import ifcopenshell.util.element
import ifcopenshell.util.unit
from ifcopenshell.util.doc import get_attribute_doc, get_predefined_type_doc, get_property_doc
import bonsai.tool as tool
from typing import Optional, Any, Union, TYPE_CHECKING
from collections.abc import Callable, Iterable
from typing import Optional, Callable, Any, Union, Iterable, TYPE_CHECKING
if TYPE_CHECKING:
import bonsai.bim.prop
@@ -39,9 +38,6 @@ if TYPE_CHECKING:
# - None - property should be imported by default workflow
# - True - setting value for imported attribute should be skipped
# - False - property should be skipped entirely from import
# Second argument is optional,
# because ImportCallback might be called for attributes that are not created by default
# (e.g. IFC entity attributes).
ImportCallback = Callable[[str, Optional[bonsai.bim.prop.Attribute], dict[str, Any]], Union[bool, None]]
# ExportCallback return values:
# - True - property should be skipped entirely from export
@@ -55,54 +51,30 @@ def draw_attributes(
copy_operator: Optional[str] = None,
popup_active_attribute: Optional[bonsai.bim.prop.Attribute] = None,
callback: Optional[Callable[[bonsai.bim.prop.Attribute, bpy.types.UILayout], None]] = None,
*,
enable_search: bool = False,
) -> None:
"""Draw editable UI for prop.Attributes.
You can set attribute active in popup with `active_attribute`
meaning you will be able to type into attribute's field without having to click
on it first
:param enable_search: Add search button to string, integer, and float attributes
"""
for attribute in props:
row = layout.row(align=True)
if attribute == popup_active_attribute:
row.activate_init = True
draw_attribute(attribute, row, copy_operator, enable_search=enable_search)
draw_attribute(attribute, row, copy_operator)
if callback:
callback(attribute, row)
def draw_attribute(
attribute: bonsai.bim.prop.Attribute,
layout: bpy.types.UILayout,
copy_operator: Optional[str] = None,
enable_search: bool = False,
attribute: bonsai.bim.prop.Attribute, layout: bpy.types.UILayout, copy_operator: Optional[str] = None
) -> None:
value_name = attribute.get_value_name(display_only=True)
if value_name == "enum_value":
prop_with_search(layout, attribute, "enum_value", text=attribute.name)
elif value_name == "filepath_value":
attribute.filepath_value.layout_file_select(layout, filter_glob=attribute.filter_glob, text=attribute.name)
elif value_name == "subitems_values":
col = layout.column()
layout = col.row(align=True)
layout.label(text=f"{attribute.name}:")
data_path = tool.Blender.get_full_data_path(attribute, value_name)
for i, item in enumerate(attribute.subitems_values, 1):
row = col.row(align=True)
row.alignment = "EXPAND"
row.prop(item, "name", text=f"# {i}")
op = row.operator("bim.attribute_remove_subitem", text="", icon="X")
op.data_path = data_path
op.index = i - 1
op = layout.operator("bim.attribute_add_subitem", icon="ADD", text="")
op.data_path = data_path
elif attribute.name in ("ScheduleDuration", "ActualDuration", "FreeFloat", "TotalFloat"):
props = tool.Sequence.get_work_schedule_props()
for item in props.durations_attributes:
@@ -123,32 +95,17 @@ def draw_attribute(
text=attribute.display_name,
)
if attribute.special_type == "URI":
if attribute.is_uri:
op = layout.operator("bim.select_uri_attribute", text="", icon="FILE_FOLDER")
op.attribute_data_path = tool.Blender.get_full_data_path(attribute)
op.data_path = attribute.path_from_id("string_value")
elif attribute.special_type in ("DATE", "DATETIME"):
op = layout.operator("bim.datepicker", text="", icon="TIME")
op.target_prop = attribute.path_from_id("string_value")
op.include_time = attribute.special_type == "DATETIME"
if enable_search and attribute.data_type in ("string", "integer", "float"):
op = layout.operator("bim.attribute_search_values", text="", icon="VIEWZOOM")
op.attribute_name = attribute.name
op.attribute_ifc_class = attribute.ifc_class
op.data_path = tool.Blender.get_full_data_path(attribute, value_name)
op.data_type = attribute.data_type
if attribute.is_optional:
layout.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="")
if attribute.use_explorer_ui:
op = layout.operator("bim.explorer_show_ui_popup", text="", icon="ZOOM_SELECTED")
op.ifc_class = attribute.ifc_class
op.attribute_name = attribute.name
op.data_path = tool.Blender.get_full_data_path(attribute, value_name)
if ifc_id := attribute.get_value():
op.preselect_ifc_id = int(ifc_id)
if attribute.name == "GlobalId":
layout.operator("bim.generate_global_id", icon="FILE_REFRESH", text="")
elif copy_operator:
@@ -156,24 +113,29 @@ def draw_attribute(
op.name = attribute.name
# TODO: rename to 'import_attributes'.
def import_attributes(
ifc_class: str,
props: bpy.types.bpy_prop_collection_idprop[Attribute],
data: dict[str, Any],
callback: Optional[ImportCallback] = None,
) -> None:
schema = tool.Ifc.schema()
for attribute in schema.declaration_by_name(ifc_class).all_attributes():
import_attribute(attribute, props, data, callback=callback)
# A more elegant attribute importer signature, intended to supersede import_attributes
def import_attributes2(
element: Union[str, ifcopenshell.entity_instance],
props: bpy.types.bpy_prop_collection_idprop[Attribute],
callback: Optional[ImportCallback] = None,
) -> None:
"""
:param element: Entity or IFC class string.
"""
info: dict[str, Any]
if isinstance(element, str):
assert (entity := tool.Ifc.schema().declaration_by_name(element).as_entity())
attributes = entity.all_attributes()
attributes = tool.Ifc.schema().declaration_by_name(element).as_entity().all_attributes()
info = {a.name(): None for a in attributes}
info["type"] = element
else:
assert (entity := element.wrapped_data.declaration().as_entity())
attributes = entity.all_attributes()
attributes = element.wrapped_data.declaration().as_entity().all_attributes()
info = element.get_info()
for attribute in attributes:
import_attribute(attribute, props, info, callback=callback)
@@ -187,19 +149,15 @@ def import_attribute(
) -> None:
data_type = ifcopenshell.util.attribute.get_primitive_type(attribute)
# Complex data types (aggregates and entities) are handled only by callback.
if data_type == ("list", "string"):
data_type = "list[string]"
if isinstance(data_type, tuple) or data_type == "entity":
callback(attribute.name(), None, data) if callback else None
return
new = props.add()
new: bonsai.bim.prop.Attribute = props.add()
new.name = attribute.name()
new.is_null = data[attribute.name()] is None
new.is_optional = attribute.optional()
new.data_type = data_type if isinstance(data_type, str) else ""
new.ifc_class = data["type"]
is_handled_by_callback = callback(attribute.name(), new, data) if callback else None
data_type = new.data_type # Allow callback to override data type.
@@ -209,9 +167,8 @@ def import_attribute(
props.remove(len(props) - 1)
elif data_type == "string":
new.string_value = "" if new.is_null else str(data[attribute.name()]).replace("\n", "\\n")
attribute_type = attribute.type_of_attribute()
if attribute_type._is("IfcURIReference"):
new.special_type = "URI"
if attribute.type_of_attribute().declared_type().name() == "IfcURIReference":
new.is_uri = True
elif attribute.type_of_attribute()._is("IfcDate"):
new.special_type = "DATE"
elif attribute.type_of_attribute()._is("IfcDateTime"):
@@ -245,12 +202,6 @@ def import_attribute(
if enum_value is not None:
new.enum_value = enum_value
elif data_type == "list[string]":
value: Union[list[str], None] = data[attribute.name()]
if value:
for item in value:
new.subitems_values.add().name = str(item).replace("\n", "\\n")
add_attribute_description(new, data)
add_attribute_min_max(attribute, new)
@@ -318,16 +269,6 @@ def export_attributes(
return attributes
def process_exported_entity_attribute(attributes: dict[str, Any], attribute_names: list[str]) -> None:
for attribute_name in attribute_names:
entity_id = attributes[attribute_name]
if entity_id is None:
# Maybe it was removed by now and enum is invalid.
del attributes[attribute_name]
else:
attributes[attribute_name] = tool.Ifc.get().by_id(int(entity_id))
ENUM_ITEMS_DATA = Union[bpy.types.PropertyGroup, bpy.types.ID, bpy.types.Operator, bpy.types.OperatorProperties]
@@ -410,23 +351,8 @@ def get_enum_items(
return items
def draw_expandable_panel(
layout: bpy.types.UILayout,
context: bpy.types.Context,
label: str,
ui_func: Callable[[bpy.types.UILayout, bpy.types.Context], None],
default_closed: bool = True,
*,
panel_id: str = "",
) -> None:
"""
:param panel_id: Optional unique identifier for the panel.
By default is matching ``label``, but if more than one panel with the same name is used,
then ``panel_id`` can be provided explicitly to ensure panels can be expanded/collapsed separately.
"""
if not panel_id:
panel_id = label
header, panel = layout.panel(panel_id, default_closed=default_closed)
def draw_expandable_panel(layout, context, label: str, ui_func, default_closed: bool = True):
header, panel = layout.panel(label, default_closed=default_closed)
header.label(text=label)
if panel:
ui_func(panel, context)
@@ -466,16 +392,10 @@ def draw_filter(
row = layout.row(align=True)
row.label(text=f"{len(data.data['saved_searches'])} Saved Searches")
row.operator("bim.select_entity_by_guid", text="", icon="CON_OBJECTSOLVER")
if data.data["saved_searches"]:
row.operator("bim.load_search", text="", icon="IMPORT").module = module
row.operator("bim.save_search", text="", icon="EXPORT").module = module
if module != "search":
if module == "drawing_include":
row.operator("bim.edit_element_filter", icon="CHECKMARK", text="").filter_mode = "INCLUDE"
if module == "drawing_exclude":
row.operator("bim.edit_element_filter", icon="CHECKMARK", text="").filter_mode = "EXCLUDE"
row.operator("bim.enable_editing_element_filter", icon="CANCEL", text="").filter_mode = "NONE"
row = layout.row(align=True)
row.operator("bim.add_filter_group", text="Add Search Group", icon="ADD").module = module
row.operator("bim.edit_filter_query", text="", icon="FILTER").module = module
+3 -33
View File
@@ -31,11 +31,9 @@ import ifcopenshell.ifcopenshell_wrapper
import bonsai
import bonsai.bim.handler
import bonsai.tool as tool
from ifcopenshell.file import UndoSystemError
from pathlib import Path
from bonsai.tool.brick import BrickStore
from typing import Union, Optional, TypedDict, NotRequired, Literal
from collections.abc import Callable
from typing import Set, Union, Optional, TypedDict, Callable, NotRequired
IFC_CONNECTED_TYPE = Union[bpy.types.Material, bpy.types.Object]
@@ -70,7 +68,7 @@ class IfcStore:
cache_path: Optional[str] = None
id_map: dict[int, IFC_CONNECTED_TYPE] = {}
guid_map: dict[str, IFC_CONNECTED_TYPE] = {}
edited_objs: set[bpy.types.Object] = set()
edited_objs: Set[bpy.types.Object] = set()
pset_template_path: str = ""
pset_template_file: Optional[ifcopenshell.file] = None
classification_path: str = ""
@@ -419,32 +417,6 @@ class IfcStore:
props = tool.Blender.get_object_bim_props(obj)
props.ifc_definition_id = 0
@staticmethod
def get_ifc_file_undo_callback(callback_type: Literal["UNDO", "REDO"]):
def callback(_) -> None:
try:
if callback_type == "UNDO":
tool.Ifc.get().undo()
else:
tool.Ifc.get().redo()
except Exception as e:
# Persistent callbacks errors are not visible from UI and we set `last_error`
# to make it visible.
error_msg = ""
# In theory it should always be UndoSystemError, but just to be safe.
if isinstance(e, UndoSystemError):
error_msg += "Undo transaction operations:\n"
transaction = e.transaction
for operation in transaction.operations:
error_msg += f"- {str(operation)}\n"
# Show it in system console too, not just in last message.
print(error_msg)
error_msg += traceback.format_exc()
bonsai.last_error = error_msg
raise
return callback
@staticmethod
def execute_ifc_operator(
operator: tool.Ifc.Operator,
@@ -474,9 +446,7 @@ class IfcStore:
if tool.Ifc.get():
tool.Ifc.get().end_transaction()
IfcStore.add_transaction_operation(
operator,
rollback=IfcStore.get_ifc_file_undo_callback("UNDO"),
commit=IfcStore.get_ifc_file_undo_callback("REDO"),
operator, rollback=lambda d: tool.Ifc.get().undo(), commit=lambda d: tool.Ifc.get().redo()
)
if BrickStore.graph is not None: # `if BrickStore.graph` by itself takes ages.
BrickStore.end_transaction()
+29 -69
View File
@@ -29,7 +29,6 @@ import numpy.typing as npt
import multiprocessing
import ifcopenshell
import ifcopenshell.geom
import ifcopenshell.ifcopenshell_wrapper as W
import ifcopenshell.util.unit
import ifcopenshell.util.element
import ifcopenshell.util.geolocation
@@ -39,8 +38,7 @@ import ifcopenshell.util.shape
import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore, IFC_CONNECTED_TYPE
from bonsai.tool.loader import OBJECT_DATA_TYPE
from typing import Union, Optional, Any, Literal
from collections.abc import Iterable
from typing import Dict, Union, Optional, Any, Literal
from ifcopenshell.util.shape import MatrixType
@@ -49,7 +47,7 @@ class MaterialCreator:
obj: bpy.types.Object
def __init__(self, ifc_import_settings: IfcImportSettings, ifc_importer: IfcImporter):
self.styles: dict[int, bpy.types.Material] = {}
self.styles: Dict[int, bpy.types.Material] = {}
self.parsed_meshes: set[str] = set()
self.ifc_import_settings = ifc_import_settings
self.ifc_importer = ifc_importer
@@ -200,16 +198,11 @@ class MaterialCreator:
class IfcImporter:
file: ifcopenshell.file
"""Either provided by user as an attribute or will be loaded from ``input_file`` during ``execute()``."""
elements: set[ifcopenshell.entity_instance]
"""Set of IfcElements to import. Excluding ``gross_elements`` and ```native_elements``."""
def __init__(self, ifc_import_settings: IfcImportSettings):
self.ifc_import_settings = ifc_import_settings
tool.Loader.set_settings(ifc_import_settings)
self.diff = None
self.file: ifcopenshell.file = None
self.project = None
self.has_existing_project = False
# element guids to blender collections mapping
@@ -302,11 +295,11 @@ class IfcImporter:
tool.Spatial.run_spatial_import_spatial_decomposition()
if default_container := tool.Spatial.guess_default_container():
tool.Spatial.set_default_container(default_container)
tool.Loader.setup_active_bsdd_classification()
self.update_progress(100)
bpy.context.window_manager.progress_end()
def process_context_filter(self) -> None:
"""Setup contexts. Necessary for importing elements representations."""
contexts = self.file.by_type("IfcGeometricRepresentationContext")
if len(contexts) > 100: # Probably something strange happening. Encountered from Revizto.
print("Warning! Excessive contexts were found and merged where applicable.")
@@ -326,15 +319,16 @@ class IfcImporter:
tool.Loader.settings.gross_context_settings = tool.Loader.create_settings(is_gross=True)
def process_element_filter(self) -> None:
elements: list[ifcopenshell.entity_instance]
if self.ifc_import_settings.has_filter:
elements = list(self.ifc_import_settings.elements)
self.elements = self.ifc_import_settings.elements
if isinstance(self.elements, set):
self.elements = list(self.elements)
# TODO: enable filtering for annotations
else:
if self.file.schema in ("IFC2X3", "IFC4"):
elements = self.file.by_type("IfcElement") + self.file.by_type("IfcProxy")
self.elements = self.file.by_type("IfcElement") + self.file.by_type("IfcProxy")
else:
elements = self.file.by_type("IfcElement")
self.elements = self.file.by_type("IfcElement")
drawing_groups = [g for g in self.file.by_type("IfcGroup") if g.ObjectType == "DRAWING"]
drawing_annotations = set()
@@ -344,16 +338,16 @@ class IfcImporter:
self.annotations = set([a for a in self.file.by_type("IfcAnnotation")])
self.annotations -= drawing_annotations
elements = [e for e in elements if not e.is_a("IfcFeatureElement") or e.is_a("IfcSurfaceFeature")]
self.elements = [e for e in self.elements if not e.is_a("IfcFeatureElement") or e.is_a("IfcSurfaceFeature")]
if self.ifc_import_settings.element_limit_mode == "UNLIMITED":
self.elements = set(elements)
self.elements = set(self.elements)
else:
offset = self.ifc_import_settings.element_offset
offset_limit = offset + self.ifc_import_settings.element_limit
self.elements = set(elements[offset:offset_limit])
self.elements = set(self.elements[offset:offset_limit])
if self.ifc_import_settings.has_filter or self.ifc_import_settings.element_limit_mode != "UNLIMITED":
self.element_types = {t for e in self.elements if (t := ifcopenshell.util.element.get_type(e))}
self.element_types = set([ifcopenshell.util.element.get_type(e) for e in self.elements])
else:
self.element_types = set(self.file.by_type("IfcTypeProduct"))
@@ -391,7 +385,6 @@ class IfcImporter:
return results
def parse_native_elements(self) -> None:
# TODO: move to `process_element_filter` to incapsulate all `self.elements` logic.
if not self.ifc_import_settings.should_load_geometry:
return
if not self.file.by_type("IfcSweptDiskSolid"):
@@ -429,8 +422,7 @@ class IfcImporter:
rep = representation
while True:
# Tekla 2023 has missing items, though it's invalid IFC.
if rep.Items and len(rep.Items) == 1 and rep.Items[0].is_a("IfcMappedItem"):
if len(rep.Items) == 1 and rep.Items[0].is_a("IfcMappedItem"):
rep_matrix = ifcopenshell.util.placement.get_mappeditem_transformation(rep.Items[0])
if not np.allclose(rep_matrix, np.eye(4)):
matrix = rep_matrix @ matrix
@@ -597,7 +589,6 @@ class IfcImporter:
return self.create_generic_sqlite_elements(elements)
if self.ifc_import_settings.should_load_geometry:
elements = elements.copy() # Prevent argument mutation.
context_settings = (
tool.Loader.settings.gross_context_settings if is_gross else tool.Loader.settings.context_settings
)
@@ -662,7 +653,7 @@ class IfcImporter:
settings: Optional[ifcopenshell.geom.main.settings] = None,
) -> set[ifcopenshell.entity_instance]:
checkpoint = time.time()
results: set[ifcopenshell.entity_instance] = set()
results = set()
if not products:
return results
@@ -726,15 +717,9 @@ class IfcImporter:
def create_structural_point_connections(self):
for product in self.file.by_type("IfcStructuralPointConnection"):
# TODO: make this based off ifcopenshell. See #1409
representation = tool.Structural.get_vertex_representation(product)
if not representation:
print(
"WARNING. Skipping invalid IfcStructuralPointConnection - "
f"element has no valid representation:\n{product}."
)
continue
representation: ifcopenshell.entity_instance = next(
rep for rep in product.Representation.Representations if rep.RepresentationType == "Vertex"
)
mesh = tool.Loader.create_structural_point_connection_mesh(representation)
if mesh is None:
continue
@@ -858,7 +843,7 @@ class IfcImporter:
return obj
def load_existing_meshes(self) -> None:
self.meshes.update({m.name: m for m in bpy.data.meshes if m.library is None})
self.meshes.update({m.name: m for m in bpy.data.meshes})
def merge_materials_by_colour(self):
cleaned_materials = {}
@@ -1033,9 +1018,9 @@ class IfcImporter:
def create_curve(
self,
element: ifcopenshell.entity_instance,
shape: Union[W.Triangulation, W.TriangulationElement],
shape: Union[ifcopenshell.geom.ShapeElementType, ifcopenshell.geom.ShapeType],
) -> bpy.types.Curve:
if isinstance(shape, W.TriangulationElement):
if hasattr(shape, "geometry"):
geometry = shape.geometry
else:
geometry = shape
@@ -1068,7 +1053,7 @@ class IfcImporter:
cartesian_point_offset: Union[npt.NDArray[np.float64], Literal[False]] = None,
) -> Union[bpy.types.Mesh, None]:
try:
if isinstance(shape, ifcopenshell.geom.ShapeElementType):
if hasattr(shape, "geometry"):
# shape is ShapeElementType
geometry = shape.geometry
else:
@@ -1077,7 +1062,7 @@ class IfcImporter:
# Mesh may already exists (e.g. during representation reimport)
# and we assign some suffix to it to prevent Blender from adding '.001' suffix to the new mesh.
mesh_name = tool.Loader.get_mesh_name_from_shape(geometry)
if old_mesh := bpy.data.meshes.get((mesh_name, None)):
if old_mesh := bpy.data.meshes.get(mesh_name):
old_mesh.name = mesh_name + ".old"
mesh = bpy.data.meshes.new(mesh_name)
@@ -1139,22 +1124,7 @@ class IfcImporter:
bpy.ops.view3d.view_selected()
bpy.ops.object.select_all(action="DESELECT")
def setup_arrays(
self,
*,
annotations_to_import: Optional[set[ifcopenshell.entity_instance]] = None,
openings_to_import: Optional[set[ifcopenshell.entity_instance]] = None,
) -> None:
# During base import some elements are skipped.
is_base_import = True
elements_to_import: set[ifcopenshell.entity_instance] = set()
if annotations_to_import is not None:
is_base_import = False
elements_to_import = annotations_to_import
elif openings_to_import is not None:
is_base_import = False
elements_to_import = openings_to_import
def setup_arrays(self):
for pset in self.file.by_type("IfcPropertySet"):
if pset.Name != "BBIM_Array":
continue
@@ -1162,24 +1132,16 @@ class IfcImporter:
continue
data = json.loads(data)
for rel in pset.DefinesOccurrence:
element: ifcopenshell.entity_instance
for element in rel.RelatedObjects:
if is_base_import:
if element.is_a("IfcAnnotation") or element.is_a("IfcOpeningElement"):
continue
else:
if element not in elements_to_import:
continue
for i in range(len(data)):
tool.Blender.Modifier.Array.set_children_lock_state(element, i, True)
tool.Blender.Modifier.Array.constrain_children_to_parent(element)
class IfcImportSettings:
input_file: Union[str, None] = None
logger: Union[logging.Logger, None] = None
def __init__(self):
self.logger: logging.Logger = None
self.input_file = None
self.diff_file = None
self.geometry_library = "opencascade"
self.should_use_cpu_multiprocessing = True
@@ -1200,19 +1162,17 @@ class IfcImportSettings:
self.element_limit_mode = "UNLIMITED"
self.element_offset = 0
self.element_limit = 30000
self.has_filter = False
self.has_filter = None
self.should_filter_spatial_elements = True
self.should_setup_viewport_camera = True
self.contexts: list[ifcopenshell.entity_instance] = []
self.context_settings: list[ifcopenshell.geom.main.settings] = []
self.gross_context_settings: list[ifcopenshell.geom.main.settings] = []
self.elements: Iterable[ifcopenshell.entity_instance] = set()
self.elements: set[ifcopenshell.entity_instance] = set()
self.load_indexed_maps = False
@staticmethod
def factory(
context=None, input_file: Optional[str] = None, logger: Optional[logging.Logger] = None
) -> IfcImportSettings:
def factory(context=None, input_file=None, logger=None):
scene_diff = tool.Blender.get_diff_props()
props = tool.Project.get_project_props()
settings = IfcImportSettings()
@@ -20,7 +20,6 @@ import blf
import bpy
import gpu
import ifcopenshell
import ifcopenshell.util.element
import bonsai.tool as tool
from bpy.types import SpaceView3D
from bpy_extras import view3d_utils
@@ -163,7 +162,8 @@ class AggregateDecorator:
batch.draw(shader)
def draw_aggregate(self, context):
props = tool.Aggregate.get_aggregate_props()
if context.scene.BIMAggregateProperties.in_aggregate_mode:
return
self.addon_prefs = tool.Blender.get_addon_preferences()
decorator_color_special = self.addon_prefs.decorator_color_special
decorator_color_selected = self.addon_prefs.decorator_color_selected
@@ -192,13 +192,7 @@ class AggregateDecorator:
aggregates.append(obj)
continue
aggregates_list = tool.Aggregate.get_aggregates_recursively(element)
if props.in_aggregate_mode and props.editing_aggregate:
index = aggregates_list.index(tool.Ifc.get_entity(props.editing_aggregate))
if index > 0:
aggregate = aggregates_list[index - 1]
else:
aggregate = aggregates_list[-1]
aggregate = ifcopenshell.util.element.get_aggregate(element)
if aggregate:
aggregates.append(tool.Ifc.get_object(aggregate))
@@ -216,7 +210,9 @@ class AggregateDecorator:
self.draw_batch("LINES", line_y, color, [(0, 1)])
line_z = (location - Vector((0.0, 0.0, size)), location + Vector((0.0, 0.0, size)))
self.draw_batch("LINES", line_z, color, [(0, 1)])
parts = tool.Aggregate.get_parts_recursively(tool.Ifc.get_entity(aggregate))
if context.scene.BIMAggregateProperties.in_aggregate_mode:
return
parts = ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(aggregate))
parts_objs = [tool.Ifc.get_object(p) for p in parts]
indices, edges = create_bounding_box(parts_objs)
@@ -265,7 +261,7 @@ class AggregateModeDecorator:
return
region = context.region
rv3d = region.data
props = tool.Aggregate.get_aggregate_props()
props = context.scene.BIMAggregateProperties
aggregate_obj = props.editing_aggregate
if not aggregate_obj:
@@ -296,7 +292,7 @@ class AggregateModeDecorator:
def draw_aggregate_empty(self, context):
if context.mode == "EDIT_MESH":
return
props = tool.Aggregate.get_aggregate_props()
props = context.scene.BIMAggregateProperties
aggregate_obj = props.editing_aggregate
if not aggregate_obj:
return
@@ -335,7 +331,7 @@ class AggregateModeDecorator:
self.draw_batch("LINES", line_y, color, [(0, 1)])
line_z = (location - Vector((0.0, 0.0, size)), location + Vector((0.0, 0.0, size)))
self.draw_batch("LINES", line_z, color, [(0, 1)])
parts = tool.Aggregate.get_parts_recursively(tool.Ifc.get_entity(aggregate_obj))
parts = ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(aggregate_obj))
color = self.addon_prefs.decorator_color_selected
parts_objs = [tool.Ifc.get_object(p) for p in parts]
@@ -19,13 +19,10 @@
import bpy
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.api.group
import ifcopenshell.api.pset
import ifcopenshell.util.element
import bonsai.tool as tool
import bonsai.core.aggregate as core
import bonsai.core.spatial
from typing import TYPE_CHECKING
class BIM_OT_aggregate_assign_object(bpy.types.Operator, tool.Ifc.Operator):
@@ -33,30 +30,23 @@ class BIM_OT_aggregate_assign_object(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Assign Object To Aggregation"
bl_description = (
"Assign object as an aggregate to the selected IFC elements.\n\n"
"If called from Object Aggregates UI, then either 'Relating Whole' or 'Related Part' must be provided.\n"
"If called from Bonsai UI, then either 'Relating Whole' or 'Related Part' must be provided.\n"
"If called directly, active object will be considered a relating whole"
)
bl_options = {"REGISTER", "UNDO"}
relating_object: bpy.props.IntProperty()
related_object: bpy.props.IntProperty()
if TYPE_CHECKING:
relating_object: int
related_object: int
def _execute(self, context):
relating_obj = None
if self.relating_object:
relating_obj = tool.Ifc.get_object(tool.Ifc.get().by_id(self.relating_object))
assert relating_obj
elif self.related_object:
aggregate = ifcopenshell.util.element.get_aggregate(tool.Ifc.get().by_id(self.related_object))
if aggregate:
relating_obj = tool.Ifc.get_object(aggregate)
assert relating_obj
else:
elif context.active_object:
relating_obj = context.active_object
if not relating_obj:
self.report({"ERROR"}, "No relating object is provided.")
return
@@ -76,7 +66,7 @@ class BIM_OT_aggregate_assign_object(bpy.types.Operator, tool.Ifc.Operator):
relating_obj=relating_obj,
related_obj=obj,
)
props = tool.Aggregate.get_aggregate_props()
props = context.scene.BIMAggregateProperties
if relating_obj == props.editing_aggregate and props.in_aggregate_mode:
new_editing_obj = props.editing_objects.add()
new_editing_obj.obj = obj
@@ -114,7 +104,7 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, tool.Ifc.Operator):
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Linked_Aggregate")
if pset:
pset = tool.Ifc.get().by_id(pset["id"])
ifcopenshell.api.pset.remove_pset(tool.Ifc.get(), product=element, pset=pset)
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset)
class BIM_OT_enable_editing_aggregate(bpy.types.Operator):
@@ -142,11 +132,12 @@ class BIM_OT_disable_editing_aggregate(bpy.types.Operator):
class BIM_OT_add_aggregate(bpy.types.Operator, tool.Ifc.Operator):
"""Add aggregate to selected IFC elements."""
"""Add aggregate to IFC element"""
bl_idname = "bim.add_aggregate"
bl_label = "Add Aggregate"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
ifc_class: bpy.props.StringProperty(name="IFC Class", default="IfcElementAssembly")
aggregate_name: bpy.props.StringProperty(name="Name", default="Default_Name")
@@ -166,7 +157,7 @@ class BIM_OT_add_aggregate(bpy.types.Operator, tool.Ifc.Operator):
return
aggregate = self.create_aggregate(context, ifc_class, self.aggregate_name)
for obj in tool.Blender.get_selected_objects():
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if not element:
continue
@@ -191,7 +182,7 @@ class BIM_OT_add_aggregate(bpy.types.Operator, tool.Ifc.Operator):
)
core.assign_object(tool.Ifc, tool.Aggregate, tool.Collector, relating_obj=aggregate, related_obj=obj)
def create_aggregate(self, context: bpy.types.Context, ifc_class: str, aggregate_name: str) -> bpy.types.Object:
def create_aggregate(self, context, ifc_class, aggregate_name):
aggregate = bpy.data.objects.new(aggregate_name, None)
aggregate.location = context.scene.cursor.location
bpy.ops.bim.assign_class(obj=aggregate.name, ifc_class=ifc_class)
@@ -225,25 +216,16 @@ class BIM_OT_select_aggregate(bpy.types.Operator):
bl_idname = "bim.select_aggregate"
bl_label = "Select Aggregate"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
select_parts: bpy.props.BoolProperty(default=False)
one_level_deep: bpy.props.BoolProperty(
name="One Level Deep", description="Select only immediate children, not recursively", default=False
)
@classmethod
def description(cls, context, properties):
if properties.select_parts:
return "Select Aggregate and Parts.\n\nCtrl+click to select only one level deep"
return "Select Aggregate and Parts"
else:
return "Select Aggregate"
def invoke(self, context, event):
if event.type == "LEFTMOUSE" and event.ctrl:
self.one_level_deep = True
return self.execute(context)
def execute(self, context):
all_parts = []
for obj in context.selected_objects:
@@ -259,27 +241,14 @@ class BIM_OT_select_aggregate(bpy.types.Operator):
obj.select_set(False)
if self.select_parts:
selected_parts = []
all_objs = []
for part in all_parts:
if part.IsDecomposedBy:
for rel in part.IsDecomposedBy:
for subpart in rel.RelatedObjects:
selected_parts.append(subpart)
for subpart in part.IsDecomposedBy[0].RelatedObjects:
all_parts.append(subpart)
all_objs.append(part)
# If not limited to one level, traverse deeper
if not self.one_level_deep:
def add_descendants(elem):
if elem.IsDecomposedBy:
for rel in elem.IsDecomposedBy:
for deeper in rel.RelatedObjects:
selected_parts.append(deeper)
add_descendants(deeper)
add_descendants(subpart)
for element in set(selected_parts + all_parts):
for element in all_objs:
obj = tool.Ifc.get_object(element)
if obj:
obj.select_set(True)
@@ -287,11 +256,9 @@ class BIM_OT_select_aggregate(bpy.types.Operator):
else:
for aggregate_element in all_parts:
aggregate_obj = tool.Ifc.get_object(aggregate_element)
if aggregate_obj:
aggregate_obj.select_set(True)
bpy.context.view_layer.objects.active = aggregate_obj
aggregate_obj.select_set(True)
bpy.context.view_layer.objects.active = aggregate_obj
self.one_level_deep = False # <-- forcibly reset
return {"FINISHED"}
@@ -326,7 +293,6 @@ class BIM_OT_break_link_to_other_aggregates(bpy.types.Operator, tool.Ifc.Operato
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
ifc_file = tool.Ifc.get()
element = tool.Ifc.get_entity(bpy.context.active_object)
aggregate = ifcopenshell.util.element.get_aggregate(element)
if not aggregate:
@@ -338,8 +304,8 @@ class BIM_OT_break_link_to_other_aggregates(bpy.types.Operator, tool.Ifc.Operato
for part in parts:
pset = ifcopenshell.util.element.get_pset(part, "BBIM_Linked_Aggregate")
pset = ifc_file.by_id(pset["id"])
ifcopenshell.api.pset.remove_pset(ifc_file, product=part, pset=pset)
pset = tool.Ifc.get().by_id(pset["id"])
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=part, pset=pset)
linked_aggregate_group = next(
r.RelatingGroup
@@ -347,7 +313,7 @@ class BIM_OT_break_link_to_other_aggregates(bpy.types.Operator, tool.Ifc.Operato
if r.is_a("IfcRelAssignsToGroup")
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
)
ifcopenshell.api.group.unassign_group(ifc_file, group=linked_aggregate_group, products=[aggregate])
tool.Ifc.run("group.unassign_group", group=linked_aggregate_group, products=[aggregate])
return {"FINISHED"}
@@ -412,7 +378,7 @@ class BIM_OT_disable_aggregate_mode(bpy.types.Operator):
def execute(self, context):
bpy.ops.object.select_all(action="DESELECT")
core.exit_aggregate_mode(tool.Aggregate)
bonsai.core.aggregate.disable_aggregate_mode(tool.Aggregate)
return {"FINISHED"}
@@ -422,7 +388,7 @@ class BIM_OT_toggle_aggregate_mode_local_view(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
props = tool.Aggregate.get_aggregate_props()
props = context.scene.BIMAggregateProperties
objs = [o.obj for o in props.editing_objects]
if props.in_aggregate_mode:
if context.space_data.local_view:
@@ -33,7 +33,6 @@ from bpy.props import (
CollectionProperty,
)
from bonsai.bim.module.aggregate.decorator import AggregateDecorator, AggregateModeDecorator
from typing import TYPE_CHECKING, Union
def can_aggregate(relating_obj: bpy.types.Object, related_obj: bpy.types.Object) -> bool:
@@ -99,27 +98,16 @@ class BIMObjectAggregateProperties(PropertyGroup):
poll=poll_related_object,
)
if TYPE_CHECKING:
is_editing: bool
relating_object: Union[bpy.types.Object, None]
related_object: Union[bpy.types.Object, None]
class Objects(bpy.types.PropertyGroup):
obj: PointerProperty(type=bpy.types.Object)
previous_display_type: bpy.props.StringProperty(default="TEXTURED")
previous_hide_select: bpy.props.BoolProperty(default=False)
if TYPE_CHECKING:
obj: Union[bpy.types.Object, None]
previous_display_type: str
previous_hide_select: bool
class BIMAggregateProperties(PropertyGroup):
in_aggregate_mode: BoolProperty(name="In Edit Mode", update=update_aggregate_mode_decorator)
editing_aggregate: PointerProperty(name="Editing Aggregate", type=bpy.types.Object)
previous_editing_aggregate: PointerProperty(name="Editing Aggregate", type=bpy.types.Object)
editing_objects: CollectionProperty(type=Objects)
not_editing_objects: CollectionProperty(type=Objects)
aggregate_decorator: BoolProperty(
@@ -127,16 +115,3 @@ class BIMAggregateProperties(PropertyGroup):
default=False,
update=update_aggregate_decorator,
)
previous_state: BoolProperty(
name="True if it was previously in aggregate mode",
default=False,
)
if TYPE_CHECKING:
in_aggregate_mode: bool
editing_aggregate: Union[bpy.types.Object, None]
previous_editing_aggregate: Union[bpy.types.Object, None]
editing_objects: bpy.types.bpy_prop_collection_idprop[Objects]
not_editing_objects: bpy.types.bpy_prop_collection_idprop[Objects]
aggregate_decorator: bool
previous_state: bool
+7 -7
View File
@@ -49,14 +49,11 @@ class BIM_PT_aggregate(Panel):
layout = self.layout
row = layout.row()
row.label(text="Aggregate Decorator")
props = tool.Aggregate.get_aggregate_props()
icon = "HIDE_OFF" if props.aggregate_decorator else "HIDE_ON"
row.prop(props, "aggregate_decorator", icon=icon, text="")
row.prop(context.scene.BIMAggregateProperties, "aggregate_decorator", icon="HIDE_OFF", text="")
if not AggregateData.is_loaded:
AggregateData.load()
assert (obj := context.active_object)
props = tool.Aggregate.get_object_aggregate_props(obj)
props = context.active_object.BIMObjectAggregateProperties
if props.is_editing:
row = layout.row()
@@ -79,8 +76,10 @@ class BIM_PT_aggregate(Panel):
if AggregateData.data["has_relating_object"]:
row.label(text=AggregateData.data["relating_object_label"], icon="TRIA_UP")
op = row.operator("bim.select_aggregate", icon="OBJECT_DATA", text="")
op.obj = context.active_object.name
op.select_parts = False
op = row.operator("bim.select_aggregate", icon="RESTRICT_SELECT_OFF", text="")
op.obj = context.active_object.name
op.select_parts = True
row.operator("bim.enable_editing_aggregate", icon="GREASEPENCIL", text="")
row.operator("bim.add_aggregate", icon="ADD", text="")
@@ -132,8 +131,9 @@ class BIM_PT_linked_aggregate(Panel):
if not AggregateData.is_loaded:
AggregateData.load()
assert (obj := context.active_object)
assert (element := tool.Ifc.get_entity(obj))
obj = context.active_object
element = tool.Ifc.get_entity(obj)
props = obj.BIMObjectAggregateProperties
row = layout.row(align=True)
if element.Decomposes:
@@ -21,6 +21,7 @@
import os
import ifcopenshell.api.alignment
import ifcopenshell.api.alignment.add_stationing_to_alignment
import bpy
import json
@@ -37,12 +38,13 @@ import ifcopenshell.util.selector
from datetime import datetime
from dateutil import parser, relativedelta
from bpy_extras.io_utils import ImportHelper
from typing import get_args, TYPE_CHECKING
from typing_extensions import assert_never
class ImportAlignmentCSV(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
bl_idname = "bim.import_alignment_csv"
bl_label = "Import Alignment CSV"
bl_description = " Import alignment from the provided .csv file."
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".csv"
filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"})
@@ -61,7 +63,9 @@ class ImportAlignmentCSV(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
def _execute(self, context):
self.file = tool.Ifc.get()
start = time.time()
alignment = ifcopenshell.api.alignment.create_from_csv(self.file, self.filepath)
alignment = ifcopenshell.api.alignment.create_alignment_from_csv(self.file, self.filepath)
ifcopenshell.api.alignment.create_geometric_representation(self.file, alignment)
ifcopenshell.api.alignment.add_stationing_to_alignment(self.file, alignment=alignment, start_station=0.0)
# IFC 4.1.5.1 alignments cannot be contained in spatial structures, but can be referenced into them
sites = self.file.by_type("IfcSite")
@@ -25,23 +25,13 @@ classes = (
operator.EditAttributes,
operator.GenerateGlobalId,
operator.CopyAttributeToSelection,
operator.ExplorerAddEntity,
operator.ExplorerEnableEditingEntity,
operator.ExplorerDisableEditingEntity,
operator.ExplorerEditEntity,
operator.ExplorerShowUIPopup,
prop.BIMAttributeProperties,
prop.ExplorerEntity,
prop.BIMExplorerProperties,
ui.BIM_PT_object_attributes,
ui.BIM_PT_explorer,
ui.BIM_UL_explorer,
)
def register():
bpy.types.Object.BIMAttributeProperties = bpy.props.PointerProperty(type=prop.BIMAttributeProperties)
bpy.types.Scene.BIMExplorerProperties = bpy.props.PointerProperty(type=prop.BIMExplorerProperties)
def unregister():
@@ -26,46 +26,30 @@ import bonsai.bim.helper
import bonsai.tool as tool
import bonsai.core.attribute as core
import bonsai.core.spatial
from typing import TYPE_CHECKING, Any, Union, Literal
if TYPE_CHECKING:
from bonsai.bim.prop import Attribute
import bpy.stub_internal.rna_enums as rna_enums
def get_objs_for_operation(
operator_properties: "AttributesOperator", context: bpy.types.Context
) -> list[bpy.types.Object]:
def get_objs_for_operation(operator_properties, context):
if operator_properties.obj:
return [bpy.data.objects[operator_properties.obj]]
if operator_properties.mass_operation:
return context.selected_objects[:]
obj = context.active_object
assert obj
return [obj]
return [context.active_object]
class AttributesOperator:
class EnableEditingAttributes(bpy.types.Operator):
bl_idname = "bim.enable_editing_attributes"
bl_label = "Enable Editing Attributes"
bl_description = "ALT + Left Click to enable editing attributes on all selected objects"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty(options={"SKIP_SAVE"})
mass_operation: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
if TYPE_CHECKING:
obj: str
mass_operation: bool
def invoke(self, context, event):
self.mass_operation = event.alt
return self.execute(context)
class EnableEditingAttributes(bpy.types.Operator, AttributesOperator):
bl_idname = "bim.enable_editing_attributes"
bl_label = "Enable Editing Attributes"
bl_description = "ALT + Left Click to enable editing attributes on all selected objects"
bl_options = {"REGISTER", "UNDO"}
def enable_editing_attribute_on_obj(self, obj: bpy.types.Object) -> None:
props = tool.Blender.get_object_attribute_props(obj)
def enable_editing_attribute_on_obj(self, obj):
props = obj.BIMAttributeProperties
props.attributes.clear()
element = tool.Ifc.get_entity(obj)
@@ -79,9 +63,7 @@ class EnableEditingAttributes(bpy.types.Operator, AttributesOperator):
None,
)
lookup_attrs = tool.Attribute.does_ifc_class_support_explorer_lookup(element.is_a())
def callback(name: str, prop: Union["Attribute", None], data: dict[str, Any]) -> None | Literal[True]:
def callback(name, prop, data):
if name in ("RefLatitude", "RefLongitude"):
new = props.attributes.add()
new.name = name
@@ -95,17 +77,6 @@ class EnableEditingAttributes(bpy.types.Operator, AttributesOperator):
if name in ("PredefinedType", "ObjectType") and has_inherited_predefined_type:
props.attributes.remove(len(props.attributes) - 1)
return True
if lookup_attrs and (name in lookup_attrs):
new = props.attributes.add()
new.name = name
new.ifc_class = data["type"]
new.data_type = "enum"
new.is_optional = True
new.enum_items_dynamic = lookup_attrs[name]
new.use_explorer_ui = True
value: Union[ifcopenshell.entity_instance, None] = data[name]
if value is not None:
new.enum_value = str(value.id())
bonsai.bim.helper.import_attributes2(element, props.attributes, callback=callback)
props.is_editing_attributes = True
@@ -116,16 +87,21 @@ class EnableEditingAttributes(bpy.types.Operator, AttributesOperator):
return {"FINISHED"}
class DisableEditingAttributes(bpy.types.Operator, AttributesOperator):
class DisableEditingAttributes(bpy.types.Operator):
bl_idname = "bim.disable_editing_attributes"
bl_label = "Disable Editing Attributes"
bl_description = "ALT + Left Click to disable editing attributes on all selected objects"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty(options={"SKIP_SAVE"})
mass_operation: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
def disable_editing_attributes_on_obj(self, obj: bpy.types.Object) -> None:
props = tool.Blender.get_object_attribute_props(obj)
props.attributes.clear()
props.property_unset("is_editing_attributes")
def invoke(self, context, event):
self.mass_operation = event.alt
return self.execute(context)
def disable_editing_attributes_on_obj(self, obj):
props = obj.BIMAttributeProperties
props.is_editing_attributes = False
def execute(self, context):
for obj in get_objs_for_operation(self, context):
@@ -142,10 +118,10 @@ class EditAttributes(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
self.file = tool.Ifc.get()
obj = tool.Blender.get_active_object(is_selected=False)
if not obj or not (element := tool.Ifc.get_entity(obj)):
if not (element := tool.Ifc.get_entity(obj)):
return
def callback(attributes: dict[str, Any], prop: "Attribute") -> None | Literal[True]:
def callback(attributes, prop):
if prop.name in ("RefLatitude", "RefLongitude"):
if not prop.is_null:
try:
@@ -154,11 +130,8 @@ class EditAttributes(bpy.types.Operator, tool.Ifc.Operator):
attributes[prop.name] = None
return True
props = tool.Blender.get_object_attribute_props(obj)
props = obj.BIMAttributeProperties
attributes = bonsai.bim.helper.export_attributes(props.attributes, callback=callback)
lookup_attrs = tool.Attribute.does_ifc_class_support_explorer_lookup(element.is_a())
if lookup_attrs:
bonsai.bim.helper.process_exported_entity_attribute(attributes, list(lookup_attrs))
ifcopenshell.api.attribute.edit_attributes(self.file, product=element, attributes=attributes)
tool.Root.set_object_name(obj, element)
@@ -192,12 +165,13 @@ class GenerateGlobalId(bpy.types.Operator, tool.Ifc.Operator):
element.GlobalId = ifcopenshell.guid.new()
obj = context.active_object
if not obj or not (props := tool.Blender.get_object_attribute_props(obj)).is_editing_attributes:
if not obj or not obj.BIMAttributeProperties.is_editing_attributes:
return {"FINISHED"}
props = obj.BIMAttributeProperties
element = tool.Ifc.get_entity(obj)
if not element or not element.is_a("IfcRoot"):
if not element.is_a("IfcRoot"):
return {"FINISHED"}
if self.use_selected and obj in context.selected_objects:
@@ -217,147 +191,8 @@ class CopyAttributeToSelection(bpy.types.Operator, tool.Ifc.Operator):
name: bpy.props.StringProperty()
def _execute(self, context):
obj = tool.Blender.get_active_object()
assert obj
props = tool.Blender.get_object_attribute_props(obj)
value = props.attributes[self.name].get_value()
value = tool.Blender.get_active_object().BIMAttributeProperties.attributes.get(self.name).get_value()
total = core.copy_attribute_to_selection(
tool.Ifc, tool.Blender, tool.Root, tool.Spatial, name=self.name, value=value
)
self.report({"INFO"}, f"Attribute was successfully copied to {total} elements.")
class ExplorerAddEntity(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.explorer_add_entity"
bl_label = "Add Entity"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context) -> None:
props = tool.Attribute.get_explorer_props()
ifc_file = tool.Ifc.get()
entity = ifc_file.create_entity(props.ifc_class)
tool.Attribute.refresh_uilist_entities()
tool.Attribute.enable_editing_entity(entity)
tool.Attribute.import_entity_attributes(entity)
class ExplorerEnableEditingEntity(bpy.types.Operator):
bl_idname = "bim.explorer_enable_editing_entity"
bl_label = "Enable Editing Entity"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
props = tool.Attribute.get_explorer_props()
ifc_file = tool.Ifc.get()
assert (active_entity := props.active_entity)
entity = ifc_file.by_id(active_entity.ifc_definition_id)
tool.Attribute.disable_editing_entity()
tool.Attribute.enable_editing_entity(entity)
tool.Attribute.import_entity_attributes(entity)
return {"FINISHED"}
class ExplorerDisableEditingEntity(bpy.types.Operator):
bl_idname = "bim.explorer_disable_editing_entity"
bl_label = "Disable Editing Entity"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context) -> set["rna_enums.OperatorReturnItems"]:
tool.Attribute.disable_editing_entity()
return {"FINISHED"}
class ExplorerEditEntity(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.explorer_edit_entity"
bl_label = "Edit Entity"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context) -> None:
ifc_file = tool.Ifc.get()
props = tool.Attribute.get_explorer_props()
entity = ifc_file.by_id(props.editing_entity_id)
attrs = tool.Attribute.export_entity_attributes()
for attr, value in attrs.items():
setattr(entity, attr, value)
tool.Attribute.refresh_uilist_entities()
tool.Attribute.disable_editing_entity()
class ExplorerShowUIPopup(bpy.types.Operator):
bl_idname = "bim.explorer_show_ui_popup"
bl_label = "Show Explorer UI"
bl_description = "Show Explorer UI to select element as attribute value or edit it."
bl_options = {"REGISTER", "UNDO"}
ifc_class: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
"""Element IFC class."""
attribute_name: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
"""IFC class attribute name."""
data_path: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
"""Full data path"""
preselect_ifc_id: bpy.props.IntProperty(options={"SKIP_SAVE"}) # pyright: ignore[reportRedeclaration]
"""IFC id to preselect in the popup."""
if TYPE_CHECKING:
ifc_class: str
attribute_name: str
data_path: str
preselect_ifc_id: int
def invoke(self, context, event) -> "set[rna_enums.OperatorReturnItems]":
assert context.window_manager
assert self.ifc_class and self.attribute_name and self.data_path
props = tool.Attribute.get_explorer_props()
props.is_loaded = True
props.ifc_class = self.get_attribute_type()
if self.preselect_ifc_id:
props.active_entity_index = next(
i for i, e in enumerate(props.entities) if e.ifc_definition_id == self.preselect_ifc_id
)
return context.window_manager.invoke_props_dialog(self, width=400)
def get_attribute_type(self) -> str:
schema = tool.Ifc.schema()
entity = schema.declaration_by_name(self.ifc_class).as_entity()
assert entity
i = entity.attribute_index(self.attribute_name)
attr = entity.all_attributes()[i]
named_type = attr.type_of_attribute().as_named_type()
assert named_type
declared = named_type.declared_type()
return declared.name()
def draw(self, context) -> None:
from bonsai.bim.module.attribute.ui import BIM_PT_explorer
BIM_PT_explorer.draw(self, context, is_popup=True)
def execute(self, context) -> "set[rna_enums.OperatorReturnItems]":
props = tool.Attribute.get_explorer_props()
active_entity = props.active_entity
if active_entity is None:
self.report({"WARNING"}, "No entity selected.")
return {"FINISHED"}
# Apply pending changes for convenience.
if props.editing_entity_id:
if props.editing_entity_id == active_entity.ifc_definition_id:
bpy.ops.bim.explorer_edit_entity()
else:
bpy.ops.bim.explorer_disable_editing_entity()
# Very important to do it after changes applied, otherwise enum might update
# and index will be pointing to a different element.
exec(f"{self.data_path} = '{active_entity.ifc_definition_id}'")
return {"FINISHED"}
def cancel(self, context: bpy.types.Context) -> None:
props = tool.Attribute.get_explorer_props()
if props.editing_entity_id:
bpy.ops.bim.explorer_disable_editing_entity()
@@ -17,7 +17,6 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
import bonsai.tool as tool
from bonsai.bim.prop import StrProperty, Attribute
from bpy.types import PropertyGroup
from bpy.props import (
@@ -30,72 +29,8 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from typing import TYPE_CHECKING, Union
class BIMAttributeProperties(PropertyGroup):
attributes: CollectionProperty(name="Attributes", type=Attribute)
is_editing_attributes: BoolProperty(name="Is Editing Attributes")
if TYPE_CHECKING:
attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
is_editing_attributes: bool
class ExplorerEntity(PropertyGroup):
ifc_definition_id: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
ifc_definition_id: int
class BIMExplorerProperties(PropertyGroup):
def update_is_loaded(self, context: object) -> None:
if self.is_loaded:
# Trigger refresh.
self.ifc_class = self.ifc_class
else:
self.property_unset("is_loaded")
self.property_unset("ifc_class")
self.entities.clear()
self.property_unset("active_entity_index")
self.property_unset("editing_entity_id")
self.entity_attributes.clear()
is_loaded: BoolProperty( # pyright: ignore[reportRedeclaration]
name="Toggle Explorer UI",
update=update_is_loaded,
)
def get_ifc_class(self, context: object) -> tool.Blender.BLENDER_ENUM_ITEMS:
# TODO: Add more entities.
classes = [
"IfcPostalAddress",
"IfcTelecomAddress",
]
return [(c, c, "") for c in classes]
def update_ifc_class(self, context: object) -> None:
tool.Attribute.refresh_uilist_entities()
ifc_class: EnumProperty( # pyright: ignore[reportRedeclaration]
name="IFC Class To Search",
items=get_ifc_class,
update=update_ifc_class,
)
entities: CollectionProperty(type=ExplorerEntity) # pyright: ignore[reportRedeclaration]
active_entity_index: IntProperty() # pyright: ignore[reportRedeclaration]
editing_entity_id: IntProperty() # pyright: ignore[reportRedeclaration]
entity_attributes: CollectionProperty(type=Attribute) # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
is_loaded: bool
ifc_class: str
entities: bpy.types.bpy_prop_collection_idprop[ExplorerEntity]
active_entity_index: int
editing_entity_id: int
entity_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
@property
def active_entity(self) -> Union[ExplorerEntity, None]:
return tool.Blender.get_active_uilist_element(self.entities, self.active_entity_index)
+3 -82
View File
@@ -16,31 +16,22 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import bonsai.bim.helper
import bpy.types
from bpy.types import Panel
from bonsai.bim.module.attribute.data import AttributesData
import bonsai.tool as tool
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from bonsai.bim.module.attribute.prop import BIMExplorerProperties, ExplorerEntity
def draw_ui(context: bpy.types.Context, layout: bpy.types.UILayout, attributes) -> None:
def draw_ui(context, layout, attributes):
obj = context.active_object
assert obj
props = tool.Blender.get_object_attribute_props(obj)
props = obj.BIMAttributeProperties
if props.is_editing_attributes:
row = layout.row(align=True)
row.operator("bim.edit_attributes", icon="CHECKMARK", text="Save Attributes")
row.operator("bim.disable_editing_attributes", icon="CANCEL", text="")
bonsai.bim.helper.draw_attributes(
props.attributes, layout, copy_operator="bim.copy_attribute_to_selection", enable_search=True
)
bonsai.bim.helper.draw_attributes(props.attributes, layout, copy_operator="bim.copy_attribute_to_selection")
else:
row = layout.row()
op = row.operator("bim.enable_editing_attributes", icon="GREASEPENCIL", text="Edit")
@@ -73,73 +64,3 @@ class BIM_PT_object_attributes(Panel):
if not AttributesData.is_loaded:
AttributesData.load()
draw_ui(context, self.layout, AttributesData.data["attributes"])
class BIM_PT_explorer(Panel):
bl_label = "Explorer"
bl_idname = "BIM_PT_explorer"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_tab_project_setup"
def draw(self, context, *, is_popup=False):
assert (layout := self.layout)
props = tool.Attribute.get_explorer_props()
if is_popup:
layout.label(text=props.ifc_class)
else:
if not props.is_loaded:
row = layout.row(align=True)
row.label(text="Explorer UI is not Loaded.")
row.prop(props, "is_loaded", text="", icon="IMPORT")
return
row = layout.row(align=True)
row.prop(props, "ifc_class", text="")
row.prop(props, "is_loaded", text="", icon="CANCEL")
active_entity = props.active_entity
row = layout.row(align=True)
row.label(text=f"{len(props.entities)} entities found")
row.operator("bim.explorer_add_entity", text="", icon="ADD")
if active_entity:
if active_entity.ifc_definition_id == props.editing_entity_id:
row.operator("bim.explorer_disable_editing_entity", icon="CANCEL", text="")
else:
row.operator("bim.explorer_enable_editing_entity", icon="GREASEPENCIL", text="")
# TODO: 'Remove' button?
layout.template_list("BIM_UL_explorer", "", props, "entities", props, "active_entity_index")
if props.editing_entity_id:
box = self.layout.box()
# In popup we accept edits automatically for the convenience.
# Othrewise it seems very unintuitive, when you need to click confirmation twice.
if not is_popup:
row = box.row(align=True)
row.operator("bim.explorer_edit_entity", icon="CHECKMARK")
row.operator("bim.explorer_disable_editing_entity", icon="CANCEL", text="")
bonsai.bim.helper.draw_attributes(props.entity_attributes, box, enable_search=True)
class BIM_UL_explorer(bpy.types.UIList):
def draw_item(
self,
context: bpy.types.Context,
layout: bpy.types.UILayout,
data: BIMExplorerProperties,
item: ExplorerEntity,
icon,
active_data,
active_propname,
) -> None:
row = layout.row(align=True)
if item.ifc_definition_id == data.editing_entity_id:
row.label(text=item.name, icon="GREASEPENCIL")
else:
row.label(text=item.name)
+1 -3
View File
@@ -397,7 +397,7 @@ class AddBcfBimSnippet(bpy.types.Operator):
def poll(cls, context):
props = tool.Bcf.get_bcf_props()
props_are_filled = all(
getattr(props, attr) for attr in ("bim_snippet_reference", "bim_snippet_schema", "bim_snippet_type")
(getattr(props, attr) for attr in ("bim_snippet_reference", "bim_snippet_schema", "bim_snippet_type"))
)
if not props_are_filled:
cls.poll_message_set("Some BIM snippet fields are empty.")
@@ -1283,7 +1283,6 @@ class ActivateBcfViewpoint(bpy.types.Operator):
cam_aspect: float,
context: bpy.types.Context,
) -> None:
assert isinstance(obj.data, bpy.types.Camera)
if viewpoint.visualization_info.orthogonal_camera:
camera = viewpoint.visualization_info.orthogonal_camera
obj.data.type = "ORTHO"
@@ -1401,7 +1400,6 @@ class ActivateBcfViewpoint(bpy.types.Operator):
self.hide_spaces(context)
def hide_spaces(self, context: bpy.types.Context) -> None:
assert context.area
old = context.area.type
context.area.type = "VIEW_3D"
bpy.ops.object.select_all(action="DESELECT")
+3
View File
@@ -31,6 +31,9 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from functools import partial
from typing import Literal
from typing_extensions import assert_never
from bcf.agnostic.extensions import get_extensions_attributes
from typing import TYPE_CHECKING, Union
@@ -404,13 +404,12 @@ class EditBoundaryAttributes(bpy.types.Operator, tool.Ifc.Operator):
assert obj
bprops = tool.Boundary.get_object_boundary_props(obj)
boundary = tool.Ifc.get_entity(obj)
assert boundary
attributes = dict()
for ifc_attribute, blender_property in EDITABLE_ATTRIBUTES.items():
obj = getattr(bprops, blender_property, None)
entity = tool.Ifc.get_entity(obj)
attributes[blender_property] = entity
ifcopenshell.api.boundary.edit_attributes(tool.Ifc.get(), entity=boundary, **attributes)
ifcopenshell.api.run("boundary.edit_attributes", tool.Ifc.get(), entity=boundary, **attributes)
bpy.ops.bim.disable_editing_boundary()
return {"FINISHED"}
@@ -606,12 +605,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
parent_boundaries: list[ifcopenshell.entity_instance] = []
objs = tool.Blender.get_selected_objects()
ifc_objects = {
element: obj
for obj in objs
if (element := tool.Ifc.get_entity(obj)) and isinstance(obj.data, bpy.types.Mesh) and obj.data.vertices
}
ifc_objects = {element: obj for obj in objs if (element := tool.Ifc.get_entity(obj))}
if len(ifc_objects) not in (1, 2):
self.report({"ERROR"}, "Please select one or two IFC elements.")
return {"CANCELLED"}
@@ -653,6 +647,46 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
return {"FINISHED"}
parent_boundaries.extend(res)
elif len(objs) == 1 and False:
# Optionally the user may select just the space, and the building element shall be auto-detected
# TODO : refactor to be able to generate all boundaries for selected space automatically or with an option
relating_space = None
related_building_element = None
relating_space_obj = None
related_building_element_obj = None
def msg(self, context):
self.layout.label(text="Please set an active container to detect space boundaries from.")
element = tool.Ifc.get_entity(objs[0])
if element.is_a("IfcSpace"):
relating_space = element
relating_space_obj = objs[0]
if not (container := tool.Root.get_default_container()):
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
return
for subelement in ifcopenshell.util.element.get_decomposition(container):
if not (
subelement.is_a("IfcWall") or subelement.is_a("IfcSlab") or subelement.is_a("IfcVirtualElement")
):
continue
obj = tool.Ifc.get_object(subelement)
if obj:
related_building_element = subelement
related_building_element_obj = obj
parent_boundary = self.create_element_boundary(
context,
relating_space,
relating_space_obj,
related_building_element,
related_building_element_obj,
)
if parent_boundary:
parent_boundaries.append(parent_boundary)
bpy.ops.bim.show_boundaries()
for parent_boundary in parent_boundaries:
obj = tool.Ifc.get_object(parent_boundary)
@@ -677,14 +711,6 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
+ tool.Ifc.get().by_type("IfcVirtualElement")
)
for building_element in building_elements:
if obj := tool.Ifc.get_object(building_element):
if tool.Ifc.is_moved(obj):
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
if tool.Ifc.is_moved(space_obj):
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=space_obj)
# Don't generate boundaries of building elements that we've already got bounaries for.
for boundary in space.BoundedBy:
if boundary.RelatedBuildingElement in building_elements:
@@ -701,10 +727,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
while True:
tree.add_element(iterator.get_native())
shape = iterator.get()
shapes[shape.id] = {
"verts": ifcopenshell.util.shape.get_vertices(shape.geometry),
"faces": ifcopenshell.util.shape.get_faces(shape.geometry),
}
shapes[shape.id] = shape
if not iterator.next():
break
@@ -725,11 +748,13 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
bm = bmesh.new()
shape = shapes[building_element.id()]
for vert in shape["verts"]:
verts = ifcopenshell.util.shape.get_vertices(shape.geometry)
for vert in verts:
bm.verts.new(Vector(vert))
bm.verts.ensure_lookup_table()
for face in shape["faces"]:
faces = ifcopenshell.util.shape.get_faces(shape.geometry)
for face in faces:
bm.faces.new([bm.verts[i] for i in face])
bm.verts.ensure_lookup_table()
bm.faces.ensure_lookup_table()
@@ -984,7 +1009,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
continue
connection_geometry = self.create_connection_geometry_from_polygon(opening_polygon, target_face_matrix)
boundary = ifcopenshell.api.root.create_entity(ifc_file, ifc_class=props.boundary_class)
boundary = tool.Ifc.run("root.create_entity", ifc_class=props.boundary_class)
boundary.RelatingSpace = space
boundary.RelatedBuildingElement = filling
boundary.ConnectionGeometry = connection_geometry
+6 -10
View File
@@ -20,20 +20,16 @@ import bpy
from . import ui, prop, operator
classes = (
operator.AddBSDDProperties,
operator.ImportBSDDClasses,
operator.LoadBSDDDictionaries,
operator.SearchBSDDClassifications,
operator.SearchBSDDProperties,
prop.BSDDDictionary,
operator.GetBSDDClassificationProperties,
operator.LoadBSDDDomains,
operator.SearchBSDDClass,
operator.SetActiveBSDDDictionary,
prop.BSDDDomain,
prop.BSDDClassification,
prop.BSDDProperty,
prop.BSDDPset,
prop.BIMBSDDProperties,
ui.BIM_UL_bsdd_domains,
ui.BIM_UL_bsdd_classifications,
ui.BIM_UL_bsdd_dictionaries,
ui.BIM_UL_bsdd_classes,
ui.BIM_UL_bsdd_properties,
ui.BIM_PT_bsdd,
)
-45
View File
@@ -1,45 +0,0 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
#
# This file is part of Bonsai.
#
# Bonsai is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Bonsai 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
import ifcopenshell
import ifcopenshell.util.date
import ifcopenshell.util.classification
import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore
def refresh():
BSDDData.is_loaded = False
class BSDDData:
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.is_loaded = True
cls.data["active_dictionary"] = cls.active_dictionary()
@classmethod
def active_dictionary(cls):
props = tool.Bsdd.get_bsdd_props()
results = [("ALL", "All Dictionaries", "All active dictionaries")]
results.extend([(d.uri, d.name, f"{d.status} - {d.version}") for d in props.dictionaries if d.is_active])
return results
+35 -63
View File
@@ -19,89 +19,61 @@
import bpy
import bsdd
import bonsai.tool as tool
import ifcopenshell.api.pset
import ifcopenshell.util.element
from bonsai.core import bsdd as core
from typing import Any
class LoadBSDDDictionaries(bpy.types.Operator):
bl_idname = "bim.load_bsdd_dictionaries"
class LoadBSDDDomains(bpy.types.Operator):
bl_idname = "bim.load_bsdd_domains"
bl_label = "Load bSDD Dictionaries"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
core.load_bsdd(tool.Bsdd)
core.load_bsdd(bsdd.Client(), tool.Bsdd)
return {"FINISHED"}
class SearchBSDDClassifications(bpy.types.Operator):
class SetActiveBSDDDictionary(bpy.types.Operator):
bl_idname = "bim.set_active_bsdd_domain"
bl_label = "Set bSDD Dictionary as active"
bl_options = {"REGISTER", "UNDO"}
name: bpy.props.StringProperty()
uri: bpy.props.StringProperty()
def execute(self, context):
core.set_active_bsdd_dictionary(self.name, self.uri, tool.Bsdd)
return {"FINISHED"}
class SearchBSDDClass(bpy.types.Operator):
bl_idname = "bim.search_bsdd_classifications"
bl_label = "Search bSDD Class"
bl_description = "Search for bSDD classes by the provided keyword"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
# Requirement by buildingSMART bSDD api.
if len(context.scene.BIMBSDDProperties.keyword) < 3:
cls.poll_message_set("Search query has to be at least 3 characters long.")
return False
return True
def execute(self, context):
total = core.search_bsdd_class(tool.Bsdd, tool.Bsdd.get_bsdd_props().keyword)
self.report({"INFO"}, f"{total} bSDD classes found.")
keyword = context.scene.BIMBSDDProperties.keyword
classes_found = core.search_class(keyword, bsdd.Client(), tool.Bsdd)
self.report({"INFO"}, f"{classes_found} bSDD classes found for '{keyword}'.")
return {"FINISHED"}
class ImportBSDDClasses(bpy.types.Operator):
bl_idname = "bim.import_bsdd_classes"
bl_label = "Import bSDD Classes"
bl_description = "Load bSDD classes that apply to the current element"
class GetBSDDClassificationProperties(bpy.types.Operator):
bl_idname = "bim.get_bsdd_classification_properties"
bl_label = "Search bSDD Class Properties"
bl_description = "Search for bSDD class properties for the currently selected class"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
obj_type: bpy.props.StringProperty()
def execute(self, context):
props = tool.Bsdd.get_bsdd_props()
core.import_bsdd_classes(tool.Bsdd, self.obj, self.obj_type)
self.report({"INFO"}, f"{len(props.classes)} bSDD classes found.")
pset_data = core.get_class_properties(bsdd.Client(), tool.Bsdd)
psets_found = len(pset_data)
props_found = sum(len(pset) for pset in pset_data.values())
self.report({"INFO"}, f"{psets_found} psets found ({props_found} props) for the active classification.")
return {"FINISHED"}
class SearchBSDDProperties(bpy.types.Operator):
bl_idname = "bim.search_bsdd_properties"
bl_label = "Search bSDD Properties"
bl_description = "Search for bSDD properties that apply to the current element"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
obj_type: bpy.props.StringProperty()
def execute(self, context):
props = tool.Bsdd.get_bsdd_props()
core.search_bsdd_properties(tool.Bsdd, tool.Bsdd.get_bsdd_props().keyword, self.obj, self.obj_type)
self.report({"INFO"}, f"{len(props.properties)} bSDD properties found.")
return {"FINISHED"}
class AddBSDDProperties(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_bsdd_properties"
bl_label = "Add bSDD Properties"
bl_description = "Add selected bSDD properties to the active object"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
obj_type: bpy.props.StringProperty()
def _execute(self, context):
self.file = tool.Ifc.get()
bprops = tool.Bsdd.get_bsdd_props()
psets: dict[str, dict[str, Any]] = {}
for selected_property in bprops.selected_properties:
psets.setdefault(selected_property.metadata, {})[selected_property.name] = selected_property.get_value()
props = tool.Pset.get_pset_props(self.obj, self.obj_type)
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(self.obj, self.obj_type, context)
element = tool.Ifc.get().by_id(ifc_definition_id)
properties = {}
current_psets = ifcopenshell.util.element.get_psets(element, verbose=True)
for pset_name, properties in psets.items():
if pset := current_psets.get(pset_name, None):
pset = self.file.by_id(pset["id"])
else:
pset = ifcopenshell.api.pset.add_pset(self.file, product=element, name=pset_name)
ifcopenshell.api.pset.edit_pset(self.file, pset=pset, properties=properties)
+13 -132
View File
@@ -17,10 +17,7 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
import bonsai.tool as tool
from bpy.types import PropertyGroup
from bonsai.bim.module.bsdd.data import BSDDData
from bonsai.bim.module.classification.data import ClassificationsData
from bonsai.bim.prop import Attribute, StrProperty
from bpy.props import (
PointerProperty,
@@ -32,121 +29,37 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from typing import Union, TYPE_CHECKING, Literal
def get_active_dictionary(self: "BIMBSDDProperties", context: object) -> tool.Blender.BLENDER_ENUM_ITEMS:
if not BSDDData.is_loaded:
BSDDData.load()
return BSDDData.data["active_dictionary"]
def update_is_active(self: "BSDDDictionary", context: bpy.types.Context) -> None:
BSDDData.data["active_dictionary"] = BSDDData.active_dictionary()
if ClassificationsData.is_loaded:
props = tool.Classification.get_classification_props()
# Preserve original enum value.
classification_source = props.classification_source
ClassificationsData.data["classification_source"] = ClassificationsData.classification_source()
# Try to restore enum value.
if "classification_source" not in props:
# It's already on the default value, nothing to restore.
return
try:
props.classification_source = classification_source
except TypeError:
# Item is no longer active and not present in enum, fallback to the default.
del props["classification_source"]
def update_is_selected(self: "BSDDProperty", context: bpy.types.Context) -> None:
tool.Bsdd.import_selected_properties()
def update_active_class_index(self: "BIMBSDDProperties", context: bpy.types.Context) -> None:
tool.Bsdd.import_class_properties()
BSDDData.data["active_dictionary"] = BSDDData.active_dictionary()
class BSDDDictionary(PropertyGroup):
class BSDDDomain(PropertyGroup):
name: StringProperty(name="Name")
uri: StringProperty(name="URI")
default_language_code: StringProperty(name="Language")
organization_name_owner: StringProperty(name="Organization")
status: StringProperty(name="Status")
version: StringProperty(name="Version")
is_active: BoolProperty(
name="Is Active",
description="Enable to search with this dictionary",
default=False,
update=update_is_active,
)
if TYPE_CHECKING:
uri: str
default_language_code: str
organization_name_owner: str
status: str
version: str
is_active: bool
class BSDDClassification(PropertyGroup):
name: StringProperty(name="Name")
reference_code: StringProperty(name="Reference Code")
uri: StringProperty(name="URI")
dictionary_name: StringProperty(name="Dictionary Name")
dictionary_namespace_uri: StringProperty(name="Dictionary Namespace URI")
if TYPE_CHECKING:
reference_code: str
uri: str
dictionary_name: str
dictionary_namespace_uri: str
class BSDDProperty(PropertyGroup):
code: StringProperty(name="Code")
uri: StringProperty(name="URI")
pset: StringProperty(name="Pset")
is_selected: BoolProperty(
name="Is Selected", description="Select to add or edit this property", default=False, update=update_is_selected
)
if TYPE_CHECKING:
code: str
uri: str
pset: str
is_selected: bool
uri: StringProperty(name="Namespace URI")
domain_name: StringProperty(name="Domain Name")
domain_namespace_uri: StringProperty(name="Domain Namespace URI")
class BSDDPset(PropertyGroup):
name: StringProperty(name="Name")
properties: CollectionProperty(name="Properties", type=Attribute)
if TYPE_CHECKING:
properties: bpy.types.bpy_prop_collection_idprop[Attribute]
class BIMBSDDProperties(PropertyGroup):
# TODO: `active_dictionary` is not used anywhere?
active_dictionary: EnumProperty(items=get_active_dictionary, name="Active Dictionary")
active_domain: StringProperty(name="Active Domain")
active_uri: StringProperty(name="Active URI")
dictionaries: CollectionProperty(name="Dictionaries", type=BSDDDictionary)
active_dictionary_index: IntProperty(name="Active Dictionary Index")
domains: CollectionProperty(name="Domains", type=BSDDDomain)
active_domain_index: IntProperty(name="Active Domain Index")
classifications: CollectionProperty(name="Classifications", type=BSDDClassification)
active_classification_index: IntProperty(name="Active Classification Index")
property_filter_mode: EnumProperty(
name="Property Filter Mode",
items=[
("CLASS", "By Class", "Browse properties by class or group"),
("KEYWORD", "By Keyword", "Search properties directly using a keyword"),
],
default="CLASS",
)
classes: CollectionProperty(name="Classes", type=BSDDClassification)
active_class_index: IntProperty(name="Active Class Index", update=update_active_class_index)
properties: CollectionProperty(name="Properties", type=BSDDProperty)
active_property_index: IntProperty(name="Active Property Index")
selected_properties: CollectionProperty(name="Selected Properties", type=Attribute)
keyword: StringProperty(name="Keyword", description="Query for bsdd classes search, case and accent insensitive")
should_filter_ifc_class: BoolProperty(
name="Filter Active IFC Class",
@@ -156,41 +69,9 @@ class BIMBSDDProperties(PropertyGroup):
use_only_ifc_properties: BoolProperty(
name="Only IFC Properties",
description="Whether to display and assign only properties from IFC dictionary",
default=False,
default=True,
)
load_preview_dictionaries: BoolProperty(
name="Load Preview Dictionaries", description="Load dictionaries marked as Preview status", default=False
)
load_inactive_dictionaries: BoolProperty(
name="Load Inactive Dictionaries", description="Load dictionaries marked as Inactive status", default=False
)
load_test_dictionaries: BoolProperty(
name="Load Test Dictionaries", description="Load dictionaries that are for testing only", default=False
load_preview_domains: BoolProperty(
name="Load Preview Domains", description="Whether it should load preview and inactive domains", default=False
)
classification_psets: CollectionProperty(name="Classification Psets", type=BSDDPset)
if TYPE_CHECKING:
active_dictionary: str
active_dictionary: str
active_uri: str
dictionaries: bpy.types.bpy_prop_collection_idprop[BSDDDictionary]
active_dictionary_index: int
classifications: bpy.types.bpy_prop_collection_idprop[BSDDClassification]
active_classification_index: int
property_filter_mode: Literal["CLASS", "KEYWORD"]
classes: bpy.types.bpy_prop_collection_idprop[BSDDClassification]
active_class_index: int
properties: bpy.types.bpy_prop_collection_idprop[BSDDProperty]
active_property_index: int
selected_properties: bpy.types.bpy_prop_collection_idprop[Attribute]
keyword: str
should_filter_ifc_class: bool
use_only_ifc_properties: bool
load_preview_dictionaries: bool
load_inactive_dictionaries: bool
load_test_dictionaries: bool
classification_psets: bpy.types.bpy_prop_collection_idprop[BSDDPset]
@property
def active_class(self) -> Union[BSDDClassification, None]:
return tool.Blender.get_active_uilist_element(self.classes, self.active_class_index)
+37 -101
View File
@@ -16,15 +16,8 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import bpy
import bonsai.tool as tool
from bonsai.bim.module.bsdd.data import BSDDData
from bpy.types import Panel, UIList
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from bonsai.bim.module.bsdd.prop import BIMBSDDProperties, BSDDDictionary, BSDDClassification, BSDDProperty
class BIM_PT_bsdd(Panel):
@@ -37,128 +30,71 @@ class BIM_PT_bsdd(Panel):
bl_parent_id = "BIM_PT_tab_project_setup"
def draw(self, context):
if not BSDDData.is_loaded:
BSDDData.load()
props = tool.Bsdd.get_bsdd_props()
assert self.layout
props = context.scene.BIMBSDDProperties
layout = self.layout
if len(props.dictionaries):
row = self.layout.row()
row.operator("bim.load_bsdd_dictionaries", icon="FILE_REFRESH")
row = self.layout.row(align=True)
row.prop(props, "load_preview_domains")
if len(props.domains):
row.operator("bim.load_bsdd_domains", text="", icon="FILE_REFRESH")
if len(props.dictionaries):
if props.active_domain:
row = self.layout.row()
row.label(text="Active: " + props.active_domain, icon="URL")
else:
row = self.layout.row()
row.label(text="No Active bSDD Domain", icon="ERROR")
if len(props.domains):
self.layout.template_list(
"BIM_UL_bsdd_dictionaries",
"BIM_UL_bsdd_domains",
"",
props,
"dictionaries",
"domains",
props,
"active_dictionary_index",
"active_domain_index",
)
if 0 <= props.active_dictionary_index < len(props.dictionaries):
selected_dictionary = props.dictionaries[props.active_dictionary_index]
if 0 <= props.active_domain_index < len(props.domains):
selected_domain = props.domains[props.active_domain_index]
else:
selected_dictionary = None
selected_domain = None
if selected_dictionary:
layout.label(text="Selected Dictionary:")
if selected_domain:
layout.label(text="Selected domain:")
box = layout.box()
row = box.row(align=True)
row.label(text="Language")
row.label(text=selected_dictionary.default_language_code)
row.label(text=selected_domain.default_language_code)
row = box.row(align=True)
row.label(text="Version")
row.label(text=selected_dictionary.version)
box.operator("bim.open_uri", text="Open bSDD In Browser", icon="URL").uri = selected_dictionary.uri
row.label(text=selected_domain.version)
box.operator("bim.open_uri", text="Open bSDD In Browser", icon="URL").uri = selected_domain.uri
else:
row = self.layout.row()
row.operator("bim.load_bsdd_dictionaries")
row.operator("bim.load_bsdd_domains")
class BIM_UL_bsdd_dictionaries(UIList):
def draw_item(
self,
context,
layout: bpy.types.UILayout,
data: BIMBSDDProperties,
item: BSDDDictionary,
icon,
active_data,
active_propname,
) -> None:
class BIM_UL_bsdd_domains(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
props = context.scene.BIMBSDDProperties
row = layout.row(align=True)
if item.status != "Active":
row.label(
text=f"{item.name} ({item.organization_name_owner}) v{item.version} - {item.status}", icon="ERROR"
)
row.label(text=f"{item.name} ({item.organization_name_owner}) - {item.status}", icon="ERROR")
else:
row.label(text=f"{item.name} ({item.organization_name_owner}) v{item.version}")
row.prop(
item, "is_active", icon="CHECKBOX_HLT" if item.is_active else "CHECKBOX_DEHLT", text="", emboss=False
)
row.label(text=f"{item.name} ({item.organization_name_owner})")
if item.uri == props.active_uri:
row.label(text="", icon="URL")
else:
op = row.operator("bim.set_active_bsdd_domain", text="", icon="RESTRICT_SELECT_OFF")
op.name = item.name
op.uri = item.uri
class BIM_UL_bsdd_classifications(UIList):
def draw_item(
self,
context,
layout: bpy.types.UILayout,
data: BIMBSDDProperties,
item: BSDDClassification,
icon,
active_data,
active_propname,
) -> None:
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
row = layout.row(align=True)
row.label(text=item.reference_code)
row.label(text=item.name)
row.operator("bim.open_uri", text="", icon="URL").uri = item.uri
class BIM_UL_bsdd_classes(UIList):
def draw_item(
self,
context,
layout: bpy.types.UILayout,
data: BIMBSDDProperties,
item: BSDDClassification,
icon,
active_data,
active_propname,
) -> None:
if item:
row = layout.row(align=True)
row.label(text=item.name)
row.operator("bim.open_uri", text="", icon="URL").uri = item.uri
class BIM_UL_bsdd_properties(UIList):
def draw_item(
self,
context,
layout: bpy.types.UILayout,
data: BIMBSDDProperties,
item: BSDDProperty,
icon,
active_data,
active_propname,
) -> None:
if item:
row = layout.row(align=True)
name = item.name
if name != item.code:
name += f" ({item.code})"
row.label(text=name)
if item.pset:
row.label(text=item.pset)
row.operator("bim.open_uri", text="", icon="URL").uri = item.uri
row.prop(
item,
"is_selected",
icon="CHECKBOX_HLT" if item.is_selected else "CHECKBOX_DEHLT",
text="",
emboss=False,
)
+4 -4
View File
@@ -177,7 +177,7 @@ class CadFillet(bpy.types.Operator):
selected_edges.append(cut_edges[1])
# Calculate the distance to slide each edge to make space for the fillet arc
shared_vert = next(iter(set(selected_edges[0].verts) & set(selected_edges[1].verts)))
shared_vert = list(set(selected_edges[0].verts) & set(selected_edges[1].verts))[0]
v1 = selected_edges[0].other_vert(shared_vert)
v2 = selected_edges[1].other_vert(shared_vert)
dir1 = (v1.co - shared_vert.co).normalized()
@@ -205,7 +205,7 @@ class CadFillet(bpy.types.Operator):
# If faces are involved, then the chamfer edge protects existing faces from the newly created fillet.
# If no faces are involved, we delete the chamfer edge.
if is_wire:
chamfer_edge = next(e for e in bm.edges if shared_vert in e.verts and v2 in e.verts)
chamfer_edge = [e for e in bm.edges if shared_vert in e.verts and v2 in e.verts][0]
bm.edges.remove(chamfer_edge)
bm.verts.index_update()
@@ -675,7 +675,7 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
bm = bmesh.from_edit_mesh(self.mesh)
edges = [e for e in bm.edges if e.select and not e.hide]
mid = next(iter(set(edges[0].verts) & set(edges[1].verts)))
mid = list(set(edges[0].verts) & set(edges[1].verts))[0]
v1 = edges[0].other_vert(mid)
v2 = edges[1].other_vert(mid)
assert v1 and v2
@@ -738,7 +738,7 @@ class AddIfcArcIndexFillet(bpy.types.Operator):
bmesh.ops.remove_doubles(bm, verts=all_verts, dist=1e-4)
# Calculate the distance to slide each edge to make space for the fillet arc
shared_vert = next(iter(set(selected_edges[0].verts) & set(selected_edges[1].verts)))
shared_vert = list(set(selected_edges[0].verts) & set(selected_edges[1].verts))[0]
v1 = selected_edges[0].other_vert(shared_vert)
v2 = selected_edges[1].other_vert(shared_vert)
assert v1 and v2
+26 -96
View File
@@ -19,19 +19,16 @@
import os
import bpy
import json
import tempfile
import bmesh
import logging
import numpy as np
import ifcopenshell
import bonsai.tool as tool
from pathlib import Path
from bpy_extras.io_utils import ExportHelper, ImportHelper
from math import radians
from mathutils import Matrix, Vector
from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.clash.decorator import ClashDecorator
from typing import TYPE_CHECKING
class ExportClashSets(bpy.types.Operator, ExportHelper):
@@ -130,8 +127,7 @@ class AddClashSource(bpy.types.Operator):
def execute(self, context):
props = tool.Clash.get_clash_props()
clash_set = props.active_clash_set
assert clash_set
clash_set.get_clash_sources_group(self.group).add()
source = getattr(clash_set, self.group).add()
return {"FINISHED"}
@@ -146,8 +142,7 @@ class RemoveClashSource(bpy.types.Operator):
def execute(self, context):
props = tool.Clash.get_clash_props()
clash_set = props.active_clash_set
assert clash_set
clash_set.get_clash_sources_group(self.group).remove(self.index)
getattr(clash_set, self.group).remove(self.index)
return {"FINISHED"}
@@ -157,16 +152,14 @@ class SelectClashSource(bpy.types.Operator, ImportHelper):
bl_options = {"REGISTER", "UNDO"}
bl_description = "Select an IFC file to add as a clash source"
filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"})
index: bpy.props.IntProperty(options={"HIDDEN"})
group: bpy.props.StringProperty(options={"HIDDEN"})
index: bpy.props.IntProperty()
group: bpy.props.StringProperty()
filename_ext = ".ifc"
def execute(self, context):
props = tool.Clash.get_clash_props()
clash_set = props.active_clash_set
assert clash_set
clash_source = clash_set.get_clash_sources_group(self.group)[self.index]
clash_source.name = self.filepath
getattr(clash_set, self.group)[self.index].name = self.filepath
return {"FINISHED"}
@@ -197,39 +190,16 @@ class SelectSmartGroupedClashesPath(bpy.types.Operator, ImportHelper):
class ExecuteIfcClash(bpy.types.Operator, ExportHelper):
bl_idname = "bim.execute_ifc_clash"
bl_label = "Execute IFC Clash"
bl_description = (
"Execute clash detection and save the information to a .bcf or .json file.\n\n"
"ALT+click to run a quick clash without selecting a file to save."
)
filter_glob: bpy.props.StringProperty( # pyright: ignore[reportRedeclaration]
default="*.bcf;*.json", options={"HIDDEN"}
)
format: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
name="Format", items=[(i, i, "") for i in ("bcf", "json")]
)
filepath: bpy.props.StringProperty( # pyright: ignore[reportRedeclaration]
subtype="FILE_PATH", options={"SKIP_SAVE"}
)
quick_clash: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
options={"SKIP_SAVE"},
)
if TYPE_CHECKING:
filter_glob: str
format: str
filepath: str
quick_clash: bool
bl_description = "Execute clash detection and save the information to a .bcf or .json file"
filter_glob: bpy.props.StringProperty(default="*.bcf;*.json", options={"HIDDEN"})
format: bpy.props.EnumProperty(name="Format", items=[(i, i, "") for i in ("bcf", "json")])
filepath: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE"})
@property
def filename_ext(self) -> str:
return f".{self.format.lower()}"
def invoke(self, context, event):
if event.alt:
self.quick_clash = True
return self.execute(context)
if self.filepath:
return self.execute(context)
return ExportHelper.invoke(self, context, event)
@@ -239,32 +209,14 @@ class ExecuteIfcClash(bpy.types.Operator, ExportHelper):
self.props = tool.Clash.get_clash_props()
for clash_set in self.props.clash_sets:
for clash_sources in clash_set.get_clash_sources().values():
for clash_source in clash_sources:
if not Path(clash_source.name).is_file():
self.report(
{"ERROR"},
f"One of the provided clash source filepaths do not exist: '{clash_source.name}'.",
)
return {"CANCELLED"}
temp_file = None
if self.quick_clash:
temp_file = tempfile.NamedTemporaryFile(suffix=".json", delete=False, mode="w")
temp_file.close()
extension = ".json"
self.filepath = temp_file.name
else:
extension = Path(self.filepath).suffix.lower()
if extension != ".bcf":
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
# TODO Temporarily until BCF support comes back
if extension != ".json":
self.filepath = bpy.path.ensure_ext(self.filepath, ".bcf")
assert extension in (".bcf", ".json")
self.props.export_path = self.filepath
_, extension = os.path.splitext(self.filepath)
if extension != ".bcf":
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
# TODO Temporarily until BCF support comes back
if extension != ".json":
self.filepath = bpy.path.ensure_ext(self.filepath, ".bcf")
self.props.export_path = self.filepath
settings = ifcclash.ClashSettings()
settings.output = self.filepath
settings.logger = logging.getLogger("Clash")
@@ -273,14 +225,11 @@ class ExecuteIfcClash(bpy.types.Operator, ExportHelper):
if self.props.should_create_clash_snapshots:
def get_viewpoint_snapshot(viewpoint) -> tuple[str, bytes]:
assert context.scene
def get_viewpoint_snapshot(viewpoint):
camera = bpy.data.objects.get("IFC Clash Camera")
if not camera:
camera = bpy.data.objects.new("IFC Clash Camera", bpy.data.cameras.new("IFC Clash Camera"))
context.scene.collection.objects.link(camera)
assert isinstance(camera.data, bpy.types.Camera)
bcf_camera = viewpoint.visualization_info.perspective_camera
p = bcf_camera.camera_view_point
@@ -289,7 +238,6 @@ class ExecuteIfcClash(bpy.types.Operator, ExportHelper):
y = bcf_camera.camera_up_vector
y = Vector([y.x, y.y, y.z])
x = y.cross(z)
assert isinstance(x, Vector)
mat = Matrix(
[
@@ -303,9 +251,9 @@ class ExecuteIfcClash(bpy.types.Operator, ExportHelper):
camera.matrix_world = mat
context.scene.camera = camera
camera.data.angle = radians(60)
assert (space := tool.Blender.get_view3d_space()) and space.region_3d
space.region_3d.view_perspective = "CAMERA"
space.shading.show_xray = True
area = next(area for area in context.screen.areas if area.type == "VIEW_3D")
area.spaces[0].region_3d.view_perspective = "CAMERA"
area.spaces[0].shading.show_xray = True
context.scene.render.resolution_x = 480
context.scene.render.resolution_y = 270
context.scene.render.image_settings.file_format = "PNG"
@@ -320,26 +268,10 @@ class ExecuteIfcClash(bpy.types.Operator, ExportHelper):
clasher.clash()
clasher.export()
# Load clash results to UI.
if extension == ".bcf":
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as tmp:
try:
tmp.close()
settings.output = tmp.name
clasher.export()
tool.Clash.load_clash_sets(tmp.name)
finally:
Path(tmp.name).unlink()
else:
if extension == ".json":
tool.Clash.load_clash_sets(self.filepath)
tool.Clash.import_active_clashes()
if self.quick_clash:
assert temp_file is not None
Path(temp_file.name).unlink()
self.report({"INFO"}, "IFC Clash completed and results are loaded.")
else:
self.report({"INFO"}, f"IFC Clash results are saved to '{Path(self.filepath).name}'.")
tool.Clash.import_active_clashes()
self.report({"INFO"}, "Finished IFC clash.")
return {"FINISHED"}
@@ -416,16 +348,14 @@ class SelectClash(bpy.types.Operator):
def execute(self, context):
self.props = tool.Clash.get_clash_props()
assert (active_clash := self.props.active_clash)
assert (active_clash_set := self.props.active_clash_set)
clash_set = tool.Clash.get_clash_set(active_clash_set.name)
assert clash_set
clash_set = tool.Clash.get_clash_set(self.props.active_clash_set.name)
active_clash = self.props.active_clash
clash = tool.Clash.get_clash(clash_set, active_clash.a_global_id, active_clash.b_global_id)
if not clash:
return {"FINISHED"}
products: list[ifcopenshell.entity_instance] = []
products = []
for global_id in (clash["a_global_id"], clash["b_global_id"]):
try:
+6 -42
View File
@@ -30,18 +30,14 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from ifcopenshell.geom.main import ClashType, CLASH_TYPE_ITEMS
from mathutils import Vector
from typing import TYPE_CHECKING, Literal, Union
class ClashSource(PropertyGroup):
name: StringProperty( # pyright: ignore[reportRedeclaration]
name="File",
description="Absolute filepath to existing .ifc file to use as a clash source.",
)
filter_groups: CollectionProperty(type=BIMFilterGroup, name="Filter Groups") # pyright: ignore[reportRedeclaration]
mode: EnumProperty( # pyright: ignore[reportRedeclaration]
name: StringProperty(name="File")
filter_groups: CollectionProperty(type=BIMFilterGroup, name="Filter Groups")
mode: EnumProperty(
items=[
("a", "All Elements", "All elements will be used for clashing"),
("i", "Include", "Only the selected elements are included for clashing"),
@@ -51,7 +47,6 @@ class ClashSource(PropertyGroup):
)
if TYPE_CHECKING:
name: str
filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]
mode: Literal["a", "i", "e"]
@@ -61,31 +56,16 @@ class Clash(PropertyGroup):
b_global_id: StringProperty(name="B")
a_name: StringProperty(name="A Name")
b_name: StringProperty(name="B Name")
clash_type: EnumProperty( # pyright: ignore[reportRedeclaration]
name="Clash Type",
items=tuple((i, i, "") for i in CLASH_TYPE_ITEMS),
)
status: BoolProperty(
name="Status",
description="Clash status, not stored anywhere - currently just displayed in UI for convenience.",
default=False,
)
status: BoolProperty(name="Status", default=False)
if TYPE_CHECKING:
a_global_id: str
b_global_id: str
a_name: str
b_name: str
clash_type: ClashType
status: bool
def clashes_loaded_update(self: "ClashSet", context: bpy.types.Context) -> None:
if self.clashes_loaded:
return
tool.Clash.clear_active_clash_set_results()
class ClashSet(PropertyGroup):
mode: EnumProperty(
items=[
@@ -101,18 +81,13 @@ class ClashSet(PropertyGroup):
],
name="Mode",
)
tolerance: FloatProperty(name="Tolerance", default=0.002, subtype="DISTANCE")
clearance: FloatProperty(name="Clearance", default=0.01, subtype="DISTANCE")
tolerance: FloatProperty(name="Tolerance", default=0.002)
clearance: FloatProperty(name="Clearance", default=0.01)
allow_touching: BoolProperty(name="Allow Touching", default=False)
check_all: BoolProperty(name="Check All", default=False)
a: CollectionProperty(name="Group A", type=ClashSource)
b: CollectionProperty(name="Group B", type=ClashSource)
clashes: CollectionProperty(name="Clashes", type=Clash)
clashes_loaded: BoolProperty(
name="Clash Results Are Loaded",
description="Click to unload clash results for the clash set.",
update=clashes_loaded_update,
)
if TYPE_CHECKING:
mode: Literal["intersection", "collision", "clearance"]
@@ -123,17 +98,6 @@ class ClashSet(PropertyGroup):
a: bpy.types.bpy_prop_collection_idprop[ClashSource]
b: bpy.types.bpy_prop_collection_idprop[ClashSource]
clashes: bpy.types.bpy_prop_collection_idprop[Clash]
clashes_loaded: bool
def get_clash_sources_group(
self, group: tool.Clash.ClashSourceGroup
) -> "bpy.types.bpy_prop_collection_idprop[ClashSource]":
return getattr(self, group)
def get_clash_sources(
self,
) -> "dict[tool.Clash.ClashSourceGroup, bpy.types.bpy_prop_collection_idprop[ClashSource]]":
return {g: self.get_clash_sources_group(g) for g in tool.Clash.CLASH_SOURCE_GROUP_LITERALS}
class SmartClashGroup(PropertyGroup):
+44 -117
View File
@@ -16,16 +16,11 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import bpy
import bonsai.tool as tool
import bonsai.bim.helper
from bpy.types import Panel
from bonsai.bim.module.clash.data import ClashData
from typing import TYPE_CHECKING, assert_never
if TYPE_CHECKING:
from bonsai.bim.module.clash.prop import BIMClashProperties, ClashSet, SmartClashGroup, Clash
class BIM_PT_ifcclash(Panel):
@@ -41,7 +36,6 @@ class BIM_PT_ifcclash(Panel):
if not ClashData.is_loaded:
ClashData.load()
assert self.layout
layout = self.layout
props = tool.Clash.get_clash_props()
@@ -80,62 +74,46 @@ class BIM_PT_ifcclash(Panel):
row.prop(clash_set, "clearance")
row = layout.row()
row.prop(clash_set, "check_all")
else:
assert_never(clash_set.mode)
def draw_clash_set_group(group: tool.Clash.ClashSourceGroup) -> None:
row = layout.row(align=True)
row.label(text="Group A:", icon="OUTLINER_OB_POINTCLOUD")
row.operator("bim.add_clash_source", icon="ADD", text="").group = "a"
for index, source in enumerate(clash_set.a):
row = layout.row(align=True)
row.label(text=f"Group {group.upper()}:", icon="OUTLINER_OB_POINTCLOUD")
row.operator("bim.add_clash_source", icon="ADD", text="").group = group
row.prop(source, "name", text="")
row.prop(source, "mode", text="")
op = row.operator("bim.select_clash_source", icon="FILE_FOLDER", text="")
op.index = index
op.group = "a"
op = row.operator("bim.remove_clash_source", icon="X", text="")
op.index = index
op.group = "a"
sources = clash_set.get_clash_sources_group(group)
if not sources:
return
layout_ = layout.box()
for index, source in enumerate(sources):
row = layout_.row(align=True)
row.column().label(text="", icon="POINTCLOUD_POINT")
if source.mode != "a":
bonsai.bim.helper.draw_filter(
layout, source.filter_groups, ClashData, f"clash_{props.active_clash_set_index}_a_{index}"
)
# Draw user attention to empty filepaths.
col = row.column()
col.alert = not bool(source.name)
col.prop(source, "name", text="", placeholder="source.ifc")
row = layout.row(align=True)
row.label(text="Group B:", icon="OUTLINER_OB_POINTCLOUD")
row.operator("bim.add_clash_source", icon="ADD", text="").group = "b"
op = row.operator("bim.select_clash_source", icon="FILE_FOLDER", text="")
op.index = index
op.group = group
# Make sure file selection sticks to "name" field.
row.label(text="", icon="BLANK1")
for index, source in enumerate(clash_set.b):
row = layout.row(align=True)
row.prop(source, "name", text="")
row.prop(source, "mode", text="")
op = row.operator("bim.select_clash_source", icon="FILE_FOLDER", text="")
op.index = index
op.group = "b"
op = row.operator("bim.remove_clash_source", icon="X", text="")
op.index = index
op.group = "b"
row.prop(source, "mode", text="")
op = row.operator("bim.remove_clash_source", icon="X", text="")
op.index = index
op.group = group
if source.mode != "a":
def draw_filter(layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
bonsai.bim.helper.draw_filter(
layout,
source.filter_groups,
ClashData,
f"clash_{props.active_clash_set_index}_{group}_{index}",
)
bonsai.bim.helper.draw_expandable_panel(
layout_,
context,
"Filter",
draw_filter,
default_closed=True,
panel_id=f"filter_{group}_{index}",
)
layout.separator()
draw_clash_set_group("a")
layout.separator()
draw_clash_set_group("b")
layout.separator()
if source.mode != "a":
bonsai.bim.helper.draw_filter(
layout, source.filter_groups, ClashData, f"clash_{props.active_clash_set_index}_b_{index}"
)
row = layout.row()
row.prop(props, "should_create_clash_snapshots")
@@ -147,26 +125,11 @@ class BIM_PT_ifcclash(Panel):
op.filepath = props.export_path
row = layout.row()
if clash_set.clashes_loaded:
row.column().label(text=f"{len(clash_set.clashes)} Clashes Found", icon="PIVOT_CURSOR")
row.label(text=f"{len(clash_set.clashes)} Clashes Found", icon="PIVOT_CURSOR")
col = row.column()
col.alignment = "RIGHT"
col.prop(clash_set, "clashes_loaded", text="", icon="TRASH", invert_checkbox=True)
split = layout.split(factor=0.07, align=True)
split.label(text="#")
row = split.row(align=True)
row.label(text="Group A Element")
row.label(text="Group B Element")
row.label(text="Type")
layout.template_list("BIM_UL_clashes", "", props.active_clash_set, "clashes", props, "active_clash_index")
row = layout.row()
row.operator("bim.select_clash")
else:
row.label(text="Clashes Are Not Loaded", icon="PIVOT_CURSOR")
layout.template_list("BIM_UL_clashes", "", props.active_clash_set, "clashes", props, "active_clash_index")
row = layout.row()
row.operator("bim.select_clash")
class BIM_PT_clash_manager(Panel):
@@ -192,7 +155,6 @@ class BIM_PT_smart_clash_manager(Panel):
bl_parent_id = "BIM_PT_clash_manager"
def draw(self, context):
assert self.layout
layout = self.layout
props = tool.Clash.get_clash_props()
@@ -226,16 +188,8 @@ class BIM_PT_smart_clash_manager(Panel):
class BIM_UL_clash_sets(bpy.types.UIList):
def draw_item(
self,
context,
layout: bpy.types.UILayout,
data: BIMClashProperties,
item: ClashSet,
icon,
active_data,
active_propname,
) -> None:
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
ob = data
if item:
layout.prop(item, "name", text="", emboss=False)
else:
@@ -243,16 +197,8 @@ class BIM_UL_clash_sets(bpy.types.UIList):
class BIM_UL_smart_groups(bpy.types.UIList):
def draw_item(
self,
context,
layout: bpy.types.UILayout,
data: BIMClashProperties,
item: SmartClashGroup,
icon,
active_data,
active_propname,
) -> None:
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
ob = data
if item:
layout.label(text=str(item.number), translate=False, icon="NONE", icon_value=0)
else:
@@ -260,30 +206,11 @@ class BIM_UL_smart_groups(bpy.types.UIList):
class BIM_UL_clashes(bpy.types.UIList):
def draw_item(
self,
context,
layout: bpy.types.UILayout,
data: BIMClashProperties,
item: Clash,
icon,
active_data,
active_propname,
index,
fit_flag,
) -> None:
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
split = layout.split(factor=0.05, align=True)
split.label(text=str(index + 1))
row = split.row(align=False)
row = layout.row(align=True)
row.label(text=str(item.a_name), translate=False, icon="NONE", icon_value=0)
row.label(text=str(item.b_name), translate=False, icon="NONE", icon_value=0)
col = row.column()
col.enabled = False
col.prop(item, "clash_type", text="")
row.prop(item, "status", text="")
else:
layout.label(text="", translate=False)
@@ -21,7 +21,6 @@ import ifcopenshell
import ifcopenshell.util.date
import ifcopenshell.util.classification
import bonsai.tool as tool
from typing import Any
from bonsai.bim.ifc import IfcStore
@@ -42,7 +41,6 @@ class ClassificationsData:
cls.data["has_classification_file"] = cls.has_classification_file()
cls.data["classifications"] = cls.classifications()
cls.data["available_classifications"] = cls.available_classifications()
cls.data["classification_source"] = cls.classification_source()
@classmethod
def has_classification_file(cls):
@@ -64,26 +62,13 @@ class ClassificationsData:
return []
return [(str(e.id()), e.Name, "") for e in IfcStore.classification_file.by_type("IfcClassification")]
@classmethod
def classification_source(cls):
items = [
("FILE", "IFC File", ""),
("MANUAL", "Manual Entry", ""),
]
bprops = tool.Bsdd.get_bsdd_props()
dictionaries = [(d.uri, f"bSDD: {d.name}", "") for d in bprops.dictionaries if d.is_active]
if dictionaries:
items.append(("BSDD", "All Active bSDDs", ""))
items.extend(dictionaries)
return items
class ReferencesData:
@classmethod
def active_classification_library(cls):
if not IfcStore.classification_file or not IfcStore.classification_file.by_type("IfcClassification"):
return False
props = tool.Classification.get_classification_props()
props = bpy.context.scene.BIMClassificationProperties
name = IfcStore.classification_file.by_id(int(props.available_classifications)).Name
if name in [e.Name for e in tool.Ifc.get().by_type("IfcClassification")]:
return name
@@ -158,13 +143,10 @@ class CostClassificationsData(ReferencesData):
cls.data["object_type"] = "Cost"
@classmethod
def references(cls) -> list[dict[str, Any]]:
results: list[dict[str, Any]] = []
def references(cls):
results = []
props = tool.Cost.get_cost_props()
active_cost_item = props.active_cost_item
if not active_cost_item:
return results
element = tool.Ifc.get().by_id(active_cost_item.ifc_definition_id)
element = tool.Ifc.get().by_id(props.cost_items[props.active_cost_item_index].ifc_definition_id)
if element:
for reference in ifcopenshell.util.classification.get_references(element):
data = reference.get_info()
@@ -20,7 +20,6 @@ import bpy
import json
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.api.pset
import ifcopenshell.api.classification
import ifcopenshell.util.classification
import ifcopenshell.util.element
@@ -47,8 +46,9 @@ class AddClassification(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = tool.Classification.get_classification_props()
ifcopenshell.api.classification.add_classification(
props = context.scene.BIMClassificationProperties
ifcopenshell.api.run(
"classification.add_classification",
tool.Ifc.get(),
classification=IfcStore.classification_file.by_id(int(props.available_classifications)),
)
@@ -60,11 +60,13 @@ class AddManualClassification(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = tool.Classification.get_classification_props()
props = context.scene.BIMClassificationProperties
attributes = bonsai.bim.helper.export_attributes(props.classification_attributes)
classification = ifcopenshell.api.classification.add_classification(tool.Ifc.get(), classification="Unnamed")
ifcopenshell.api.classification.edit_classification(
tool.Ifc.get(), classification=classification, attributes=attributes
classification = ifcopenshell.api.run(
"classification.add_classification", tool.Ifc.get(), classification="Unnamed"
)
ifcopenshell.api.run(
"classification.edit_classification", tool.Ifc.get(), classification=classification, attributes=attributes
)
props.is_adding = False
@@ -84,7 +86,7 @@ class AddManualClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
objects = [context.active_object.name]
else:
objects = [self.obj]
props = tool.Classification.get_classification_reference_props()
props = context.scene.BIMClassificationReferenceProperties
attributes = bonsai.bim.helper.export_attributes(props.reference_attributes)
products = [
tool.Ifc.get().by_id(ifc_definition_id)
@@ -93,14 +95,17 @@ class AddManualClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
]
if products:
classification = tool.Ifc.get().by_id(int(props.classifications))
reference = ifcopenshell.api.classification.add_reference(
reference = ifcopenshell.api.run(
"classification.add_reference",
tool.Ifc.get(),
products=products,
classification=classification,
identification="X",
name="Unnamed",
)
ifcopenshell.api.classification.edit_reference(tool.Ifc.get(), reference=reference, attributes=attributes)
ifcopenshell.api.run(
"classification.edit_reference", tool.Ifc.get(), reference=reference, attributes=attributes
)
props.is_adding = False
@@ -110,30 +115,29 @@ class AddClassificationFromBSDD(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
cprops = tool.Classification.get_classification_props()
bprops = tool.Bsdd.get_bsdd_props()
if cprops.classification_source == "BSDD":
dictionaries = [d.uri for d in bprops.dictionaries if d.is_active]
else:
dictionaries = [cprops.classification_source]
for uri in dictionaries:
if not (dictionary := tool.Bsdd.get_dictionary(uri)):
continue
has_classification = False
for element in tool.Ifc.get().by_type("IfcClassification"):
if element.Name == dictionary["name"] or (
tool.Classification.get_location(element) == dictionary["uri"]
):
self.report({"INFO"}, f"Classification '{dictionary['name']}' is already added to the project.")
has_classification = True
if has_classification:
continue
classification = ifcopenshell.api.classification.add_classification(
tool.Ifc.get(), classification=dictionary["name"]
props = context.scene.BIMBSDDProperties
domain = next((d for d in props.domains if d.name == props.active_domain and d.uri == props.active_uri), None)
# Maybe user loaded preview domains, set it as active
# and then reloaded them without preview domains.
if not domain:
self.report(
{"INFO"},
f"Couldn't find domain '{props.active_domain}' ({props.active_uri}). Try to reload bSDD dictionaries.",
)
classification.Source = dictionary["organizationNameOwner"]
classification.Edition = dictionary["version"]
tool.Classification.set_location(classification, dictionary["uri"])
return
for element in tool.Ifc.get().by_type("IfcClassification"):
if element.Name == props.active_domain or (tool.Classification.get_location(element) == domain.uri):
self.report({"INFO"}, f"Classification '{props.active_domain}' is already added to the project.")
return
classification = ifcopenshell.api.run(
"classification.add_classification", tool.Ifc.get(), classification=props.active_domain
)
classification.Source = domain.organization_name_owner
classification.Edition = domain.version
tool.Classification.set_location(classification, domain.uri)
class EnableAddingManualClassification(bpy.types.Operator):
@@ -142,7 +146,7 @@ class EnableAddingManualClassification(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
props = tool.Classification.get_classification_props()
props = context.scene.BIMClassificationProperties
props.is_adding = True
props.active_classification_id = 0
props.classification_attributes.clear()
@@ -156,7 +160,7 @@ class DisableAddingManualClassification(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
props = tool.Classification.get_classification_props()
props = context.scene.BIMClassificationProperties
props.is_adding = False
return {"FINISHED"}
@@ -167,7 +171,7 @@ class EnableAddingManualClassificationReference(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
props = tool.Classification.get_classification_reference_props()
props = context.scene.BIMClassificationReferenceProperties
props.is_adding = True
props.reference_attributes.clear()
bonsai.bim.helper.import_attributes2("IfcClassificationReference", props.reference_attributes)
@@ -180,7 +184,7 @@ class DisableAddingManualClassificationReference(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
props = tool.Classification.get_classification_reference_props()
props = context.scene.BIMClassificationReferenceProperties
props.is_adding = False
return {"FINISHED"}
@@ -203,7 +207,7 @@ class EnableEditingClassification(bpy.types.Operator):
new.string_value = "" if new.is_null else json.dumps(data[name])
return True
props = tool.Classification.get_classification_props()
props = context.scene.BIMClassificationProperties
props.classification_attributes.clear()
bonsai.bim.helper.import_attributes2(
tool.Ifc.get().by_id(self.classification), props.classification_attributes, callback
@@ -218,8 +222,7 @@ class DisableEditingClassification(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
props = tool.Classification.get_classification_props()
props.active_classification_id = 0
context.scene.BIMClassificationProperties.active_classification_id = 0
return {"FINISHED"}
@@ -235,7 +238,8 @@ class RemoveClassification(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
self.file = tool.Ifc.get()
ifcopenshell.api.classification.remove_classification(
ifcopenshell.api.run(
"classification.remove_classification",
tool.Ifc.get(),
classification=self.file.by_id(self.classification),
)
@@ -247,7 +251,7 @@ class EditClassification(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = tool.Classification.get_classification_props()
props = context.scene.BIMClassificationProperties
def callback(attributes, prop):
if prop.name == "ReferenceTokens":
@@ -272,7 +276,7 @@ class EnableEditingClassificationReference(bpy.types.Operator):
obj: bpy.props.StringProperty()
def execute(self, context):
props = tool.Classification.get_classification_reference_props()
props = context.scene.BIMClassificationReferenceProperties
props.reference_attributes.clear()
bonsai.bim.helper.import_attributes2(tool.Ifc.get().by_id(self.reference), props.reference_attributes)
props.active_reference_id = self.reference
@@ -286,7 +290,7 @@ class DisableEditingClassificationReference(bpy.types.Operator):
obj: bpy.props.StringProperty()
def execute(self, context):
tool.Classification.get_classification_reference_props().active_reference_id = 0
context.scene.BIMClassificationReferenceProperties.active_reference_id = 0
return {"FINISHED"}
@@ -324,7 +328,8 @@ class RemoveClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
if elements_by_references:
for reference, products in elements_by_references.items():
ifcopenshell.api.classification.remove_reference(
ifcopenshell.api.run(
"classification.remove_reference",
tool.Ifc.get(),
reference=reference,
products=products,
@@ -338,7 +343,7 @@ class EditClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
obj: bpy.props.StringProperty()
def _execute(self, context):
props = tool.Classification.get_classification_reference_props()
props = context.scene.BIMClassificationReferenceProperties
attributes = bonsai.bim.helper.export_attributes(props.reference_attributes)
ifc_file = tool.Ifc.get()
ifcopenshell.api.classification.edit_reference(
@@ -365,7 +370,7 @@ class AddClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
objects = [context.active_object.name]
else:
objects = [self.obj]
props = tool.Classification.get_classification_props()
props = context.scene.BIMClassificationProperties
classification = None
classification_name = IfcStore.classification_file.by_id(int(props.available_classifications)).Name
for element in tool.Ifc.get().by_type("IfcClassification"):
@@ -379,7 +384,8 @@ class AddClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
if (ifc_definition_id := tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context))
]
if products:
ifcopenshell.api.classification.add_reference(
ifcopenshell.api.run(
"classification.add_reference",
tool.Ifc.get(),
reference=IfcStore.classification_file.by_id(self.reference),
products=products,
@@ -406,31 +412,33 @@ class AddClassificationReferenceFromBSDD(bpy.types.Operator, tool.Ifc.Operator):
objects = [context.active_object.name]
else:
objects = [self.obj]
bprops = tool.Bsdd.get_bsdd_props()
props = context.scene.BIMClassificationProperties
bprops = context.scene.BIMBSDDProperties
use_only_ifc_properties: bool = bprops.use_only_ifc_properties
bsdd_classification = bprops.classifications[bprops.active_classification_index]
classification = None
for element in tool.Ifc.get().by_type("IfcClassification"):
if element.Name == bsdd_classification.dictionary_name or (
tool.Classification.get_location(element) == bsdd_classification.dictionary_namespace_uri
if element.Name == bsdd_classification.domain_name or (
tool.Classification.get_location(element) == bsdd_classification.domain_namespace_uri
):
classification = element
break
if not classification:
classification = ifcopenshell.api.classification.add_classification(
tool.Ifc.get(), classification=bsdd_classification.dictionary_name
classification = ifcopenshell.api.run(
"classification.add_classification", tool.Ifc.get(), classification=bsdd_classification.domain_name
)
tool.Classification.set_location(classification, bsdd_classification.dictionary_namespace_uri)
tool.Classification.set_location(classification, bsdd_classification.domain_namespace_uri)
for obj in objects:
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context)
if not ifc_definition_id:
continue
element = tool.Ifc.get().by_id(ifc_definition_id)
reference = ifcopenshell.api.classification.add_reference(
reference = ifcopenshell.api.run(
"classification.add_reference",
tool.Ifc.get(),
products=[element],
classification=classification,
@@ -468,16 +476,18 @@ class AddClassificationReferenceFromBSDD(bpy.types.Operator, tool.Ifc.Operator):
if pset:
pset = tool.Ifc.get().by_id(pset["id"])
elif is_pset:
pset = ifcopenshell.api.pset.add_pset(
tool.Ifc.get(), product=element, name=classification_pset.name
pset = ifcopenshell.api.run(
"pset.add_pset", tool.Ifc.get(), product=element, name=classification_pset.name
)
else:
pset = ifcopenshell.api.pset.add_qto(tool.Ifc.get(), product=element, name=classification_pset.name)
pset = ifcopenshell.api.run(
"pset.add_qto", tool.Ifc.get(), product=element, name=classification_pset.name
)
if is_pset:
ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties=properties)
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties=properties)
else:
ifcopenshell.api.pset.edit_qto(tool.Ifc.get(), qto=pset, properties=properties)
ifcopenshell.api.run("pset.edit_qto", tool.Ifc.get(), qto=pset, properties=properties)
class ChangeClassificationLevel(bpy.types.Operator):
@@ -487,7 +497,7 @@ class ChangeClassificationLevel(bpy.types.Operator):
parent_id: bpy.props.IntProperty()
def execute(self, context):
props = tool.Classification.get_classification_props()
props = context.scene.BIMClassificationProperties
props.available_library_references.clear()
for reference in IfcStore.classification_file.by_id(self.parent_id).HasReferences:
new = props.available_library_references.add()
@@ -509,6 +519,6 @@ class DisableEditingClassificationReferences(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
props = tool.Classification.get_classification_props()
props = context.scene.BIMClassificationProperties
props.available_library_references.clear()
return {"FINISHED"}
@@ -17,7 +17,6 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
import bonsai.tool as tool
from bonsai.bim.prop import Attribute
from bonsai.bim.module.classification.data import ClassificationsData, ClassificationReferencesData
from bpy.types import PropertyGroup
@@ -31,12 +30,9 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from typing import TYPE_CHECKING
def get_available_classifications(
self: "BIMClassificationProperties", context: bpy.types.Context
) -> tool.Blender.BLENDER_ENUM_ITEMS:
def get_available_classifications(self, context):
if not ClassificationsData.is_loaded:
ClassificationsData.load()
return ClassificationsData.data["available_classifications"]
@@ -48,30 +44,25 @@ def get_classifications(self, context):
return ClassificationReferencesData.data["classifications"]
def get_classification_source(
self: "BIMClassificationProperties", context: bpy.types.Context
) -> tool.Blender.BLENDER_ENUM_ITEMS:
if not ClassificationsData.is_loaded:
ClassificationsData.load()
return ClassificationsData.data["classification_source"]
class ClassificationReference(PropertyGroup):
name: StringProperty(name="Name")
identification: StringProperty(name="Identification")
ifc_definition_id: IntProperty(name="IFC Definition ID")
has_references: BoolProperty(name="Has References")
referenced_source: IntProperty(name="IFC Definition ID")
if TYPE_CHECKING:
identification: str
ifc_definition_id: int
has_references: bool
referenced_source: int
class BIMClassificationProperties(PropertyGroup):
is_adding: BoolProperty(name="Is Adding", default=False)
classification_source: EnumProperty(items=get_classification_source, name="Classification Source")
classification_source: EnumProperty(
items=[
("FILE", "IFC File", ""),
("BSDD", "buildingSMART Data Dictionary", ""),
("MANUAL", "Manual Entry", ""),
],
name="Classification Source",
default="FILE",
)
available_classifications: EnumProperty(items=get_available_classifications, name="Available Classifications")
classification_attributes: CollectionProperty(name="Classification Attributes", type=Attribute)
active_classification_id: IntProperty(name="Active Classification Id")
@@ -79,25 +70,9 @@ class BIMClassificationProperties(PropertyGroup):
active_library_referenced_source: IntProperty(name="Active Library Referenced Source")
active_library_reference_index: IntProperty(name="Active Library Reference Index")
if TYPE_CHECKING:
is_adding: bool
classification_source: str
available_classifications: str
classification_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
active_classification_id: int
available_library_references: bpy.types.bpy_prop_collection_idprop[ClassificationReference]
active_library_referenced_source: int
active_library_reference_index: int
class BIMClassificationReferenceProperties(PropertyGroup):
is_adding: BoolProperty(name="Is Adding", default=False)
classifications: EnumProperty(items=get_classifications, name="Classifications")
reference_attributes: CollectionProperty(name="Reference Attributes", type=Attribute)
active_reference_id: IntProperty(name="Active Reference Id")
if TYPE_CHECKING:
is_adding: bool
classifications: str
reference_attributes: bpy.types.bpy_prop_collection_idprop
active_reference_id: int
@@ -16,7 +16,6 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import bpy
import bonsai.bim.helper
import bonsai.tool as tool
@@ -28,10 +27,6 @@ from bonsai.bim.module.classification.data import (
MaterialClassificationsData,
CostClassificationsData,
)
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from bonsai.bim.module.classification.prop import BIMClassificationProperties, ClassificationReference
class BIM_PT_classifications(Panel):
@@ -51,8 +46,7 @@ class BIM_PT_classifications(Panel):
if not ClassificationsData.is_loaded:
ClassificationsData.load()
self.props = tool.Classification.get_classification_props()
assert self.layout
self.props = context.scene.BIMClassificationProperties
row = self.layout.row(align=True)
row.label(text="Source", icon="OUTLINER")
@@ -60,10 +54,10 @@ class BIM_PT_classifications(Panel):
if self.props.classification_source == "FILE":
self.draw_add_file_ui(context)
elif self.props.classification_source == "BSDD":
self.draw_add_bsdd_ui(context)
elif self.props.classification_source == "MANUAL":
self.draw_add_manual_ui(context)
else:
self.draw_add_bsdd_ui(context)
for classification in ClassificationsData.data["classifications"]:
if self.props.active_classification_id == classification["id"]:
@@ -82,6 +76,16 @@ class BIM_PT_classifications(Panel):
row.operator("bim.enable_adding_manual_classification", text="Add Classification", icon="ADD")
def draw_add_bsdd_ui(self, context):
self.bprops = context.scene.BIMBSDDProperties
if not self.bprops.active_domain:
row = self.layout.row()
row.label(text="No Active bSDD Domain", icon="ERROR")
return
row = self.layout.row()
row.label(text="Active: " + self.bprops.active_domain, icon="URL")
row = self.layout.row()
row.operator("bim.add_classification_from_bsdd", icon="ADD")
@@ -116,9 +120,9 @@ class ReferenceUI:
def draw_ui(self, context):
obj = context.active_object
self.sprops = tool.Classification.get_classification_props()
self.bprops = tool.Bsdd.get_bsdd_props()
self.props = tool.Classification.get_classification_reference_props()
self.sprops = context.scene.BIMClassificationProperties
self.bprops = context.scene.BIMBSDDProperties
self.props = context.scene.BIMClassificationReferenceProperties
self.file = tool.Ifc.get()
self.draw_add_ui(context)
@@ -140,10 +144,10 @@ class ReferenceUI:
if self.sprops.classification_source == "FILE":
self.draw_add_file_ui(context)
elif self.sprops.classification_source == "BSDD":
self.draw_add_bsdd_ui(context)
elif self.sprops.classification_source == "MANUAL":
self.draw_add_manual_ui(context)
else:
self.draw_add_bsdd_ui(context)
def draw_add_manual_ui(self, context):
row = self.layout.row()
@@ -159,11 +163,22 @@ class ReferenceUI:
row.operator("bim.enable_adding_manual_classification_reference", text="Add Reference", icon="ADD")
def draw_add_bsdd_ui(self, context):
if not self.bprops.active_domain:
row = self.layout.row()
row.label(text="No Active bSDD Domain", icon="ERROR")
return
row = self.layout.row()
row.label(text="Active: " + self.bprops.active_domain, icon="URL")
row = self.layout.row(align=True)
row.prop(self.bprops, "keyword", text="")
row.prop(self.bprops, "should_filter_ifc_class", text="", icon="FILTER")
row.operator("bim.search_bsdd_classifications", text="", icon="VIEWZOOM")
row = self.layout.row()
row.prop(self.bprops, "should_filter_ifc_class")
row.prop(self.bprops, "use_only_ifc_properties")
if len(self.bprops.classifications):
self.layout.template_list(
"BIM_UL_bsdd_classifications",
@@ -184,6 +199,21 @@ class ReferenceUI:
)
op.obj = self.obj
op.obj_type = self.obj_type
row.operator("bim.get_bsdd_classification_properties", text="", icon="COPY_ID")
if len(self.bprops.classification_psets):
use_only_ifc_properties = self.bprops.use_only_ifc_properties
for pset in self.bprops.classification_psets:
properties = pset.properties
if use_only_ifc_properties:
properties = [p for p in properties if p.metadata == "IFC"]
if not properties:
continue
box = self.layout.box()
row = box.row()
row.label(text=pset.name, icon="COPY_ID")
bonsai.bim.helper.draw_attributes(properties, box)
def draw_add_file_ui(self, context):
if not self.data.data["active_classification_library"]:
@@ -321,16 +351,7 @@ class BIM_PT_cost_classifications(Panel, ReferenceUI):
class BIM_UL_classifications(UIList):
def draw_item(
self,
context,
layout: bpy.types.UILayout,
data: BIMClassificationProperties,
item: ClassificationReference,
icon,
active_data,
active_propname,
):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
if item.has_references:
op = layout.operator("bim.change_classification_level", text="", icon="DISCLOSURE_TRI_RIGHT")
@@ -17,27 +17,12 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
import json
import ifcopenshell.api
import ifcopenshell.api.constraint
import ifcopenshell.util.attribute
import bonsai.bim.helper
import bonsai.tool as tool
from typing import TYPE_CHECKING
def get_active_object(context: bpy.types.Context, obj_name: str) -> bpy.types.Object:
if obj_name:
obj = bpy.data.objects[obj_name]
else:
assert (obj := context.active_object)
return obj
def get_selected_objects(context: bpy.types.Context, obj_name: str) -> list[bpy.types.Object]:
if obj_name:
objs = [bpy.data.objects[obj_name]]
else:
objs = context.selected_objects
return objs
class LoadObjectives(bpy.types.Operator):
@@ -46,7 +31,7 @@ class LoadObjectives(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
props = tool.Blender.get_constraint_props()
props = context.scene.BIMConstraintProperties
props.constraints.clear()
for constraint in tool.Ifc.get().by_type("IfcObjective"):
new = props.constraints.add()
@@ -63,8 +48,7 @@ class DisableConstraintEditingUI(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
props = tool.Blender.get_constraint_props()
props.is_editing = ""
context.scene.BIMConstraintProperties.is_editing = ""
bpy.ops.bim.disable_editing_constraint()
return {"FINISHED"}
@@ -75,11 +59,8 @@ class EnableEditingConstraint(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
constraint: bpy.props.IntProperty()
if TYPE_CHECKING:
constraint: int
def execute(self, context):
props = tool.Blender.get_constraint_props()
props = context.scene.BIMConstraintProperties
props.constraint_attributes.clear()
bonsai.bim.helper.import_attributes2(tool.Ifc.get().by_id(self.constraint), props.constraint_attributes)
props.active_constraint_id = self.constraint
@@ -92,8 +73,7 @@ class DisableEditingConstraint(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
props = tool.Blender.get_constraint_props()
props.active_constraint_id = 0
context.scene.BIMConstraintProperties.active_constraint_id = 0
return {"FINISHED"}
@@ -103,7 +83,7 @@ class AddObjective(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
result = ifcopenshell.api.constraint.add_objective(tool.Ifc.get())
result = ifcopenshell.api.run("constraint.add_objective", tool.Ifc.get())
bpy.ops.bim.load_objectives()
bpy.ops.bim.enable_editing_constraint(constraint=result.id())
return {"FINISHED"}
@@ -115,7 +95,7 @@ class EditObjective(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = tool.Blender.get_constraint_props()
props = context.scene.BIMConstraintProperties
attributes = bonsai.bim.helper.export_attributes(props.constraint_attributes)
ifc_file = tool.Ifc.get()
ifcopenshell.api.constraint.edit_objective(
@@ -133,13 +113,12 @@ class RemoveConstraint(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
constraint: bpy.props.IntProperty()
if TYPE_CHECKING:
constraint: int
def _execute(self, context):
props = tool.Blender.get_constraint_props()
props = context.scene.BIMConstraintProperties
self.file = tool.Ifc.get()
ifcopenshell.api.constraint.remove_constraint(self.file, constraint=self.file.by_id(self.constraint))
ifcopenshell.api.run(
"constraint.remove_constraint", self.file, **{"constraint": self.file.by_id(self.constraint)}
)
if props.is_editing == "IfcObjective":
bpy.ops.bim.load_objectives()
return {"FINISHED"}
@@ -151,12 +130,9 @@ class EnableAssigningConstraint(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
if TYPE_CHECKING:
obj: str
def execute(self, context):
obj = get_active_object(context, self.obj)
props = tool.Blender.get_object_constraint_props(obj)
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
props = obj.BIMObjectConstraintProperties
if props.available_constraint_types == "IfcObjective":
bpy.ops.bim.load_objectives()
props.is_adding = props.available_constraint_types
@@ -169,12 +145,9 @@ class DisableAssigningConstraint(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
if TYPE_CHECKING:
obj: str
def execute(self, context):
obj = get_active_object(context, self.obj)
props = tool.Blender.get_object_constraint_props(obj)
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
props = obj.BIMObjectConstraintProperties
props.is_adding = ""
return {"FINISHED"}
@@ -186,13 +159,8 @@ class AssignConstraint(bpy.types.Operator, tool.Ifc.Operator):
obj: bpy.props.StringProperty()
constraint: bpy.props.IntProperty()
if TYPE_CHECKING:
obj: str
constraint: int
def _execute(self, context):
self.file = tool.Ifc.get()
objs = get_selected_objects(context, self.obj)
objs = [bpy.data.objects[self.obj]] if self.obj else context.selected_objects
products = [
self.file.by_id(obj_id)
@@ -200,10 +168,13 @@ class AssignConstraint(bpy.types.Operator, tool.Ifc.Operator):
if (obj_id := tool.Blender.get_object_bim_props(obj).ifc_definition_id)
]
if products:
ifcopenshell.api.constraint.assign_constraint(
ifcopenshell.api.run(
"constraint.assign_constraint",
self.file,
products=products,
constraint=self.file.by_id(self.constraint),
**{
"products": products,
"constraint": self.file.by_id(self.constraint),
},
)
return {"FINISHED"}
@@ -215,22 +186,21 @@ class UnassignConstraint(bpy.types.Operator, tool.Ifc.Operator):
obj: bpy.props.StringProperty()
constraint: bpy.props.IntProperty()
if TYPE_CHECKING:
obj: str
constraint: int
def _execute(self, context):
self.file = tool.Ifc.get()
objs = get_selected_objects(context, self.obj)
objs = [bpy.data.objects[self.obj]] if self.obj else context.selected_objects
products = [
self.file.by_id(obj_id)
for obj in objs
if (obj_id := tool.Blender.get_object_bim_props(obj).ifc_definition_id)
]
if products:
ifcopenshell.api.constraint.unassign_constraint(
ifcopenshell.api.run(
"constraint.unassign_constraint",
self.file,
products=products,
constraint=self.file.by_id(self.constraint),
**{
"products": products,
"constraint": self.file.by_id(self.constraint),
},
)
return {"FINISHED"}
@@ -32,7 +32,6 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from typing import TYPE_CHECKING, Literal
def get_available_constraint_types(self, context):
@@ -45,10 +44,6 @@ class Constraint(PropertyGroup):
name: StringProperty(name="Name")
ifc_definition_id: IntProperty(name="IFC Definition ID")
if TYPE_CHECKING:
name: str
ifc_definition_id: int
class BIMConstraintProperties(PropertyGroup):
constraint_attributes: CollectionProperty(name="Constraint Attributes", type=Attribute)
@@ -57,18 +52,7 @@ class BIMConstraintProperties(PropertyGroup):
active_constraint_index: IntProperty(name="Active Constraint Index")
is_editing: StringProperty(name="Is Editing")
if TYPE_CHECKING:
constraint_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
active_constraint_id: int
constraints: bpy.types.bpy_prop_collection_idprop[Constraint]
active_constraint_index: int
is_editing: str
class BIMObjectConstraintProperties(PropertyGroup):
is_adding: StringProperty(name="Is Adding")
available_constraint_types: EnumProperty(items=get_available_constraint_types, name="Available Constraint Types")
if TYPE_CHECKING:
is_adding: str
available_constraint_types: Literal["IfcObjective"]
+8 -31
View File
@@ -16,15 +16,10 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import bonsai.tool as tool
from bpy.types import Panel, UIList
from bonsai.bim.helper import draw_attributes
from bonsai.bim.module.constraint.data import ConstraintsData, ObjectConstraintsData
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from bonsai.bim.module.constraint.prop import BIMConstraintProperties, BIMObjectConstraintProperties, Constraint
class BIM_PT_constraints(Panel):
@@ -44,7 +39,7 @@ class BIM_PT_constraints(Panel):
if not ConstraintsData.is_loaded:
ConstraintsData.load()
self.props = tool.Blender.get_constraint_props()
self.props = context.scene.BIMConstraintProperties
if not self.props.is_editing or self.props.is_editing == "IfcObjective":
row = self.layout.row(align=True)
@@ -96,8 +91,8 @@ class BIM_PT_object_constraints(Panel):
obj = context.active_object
assert obj
self.oprops = tool.Blender.get_object_bim_props(obj)
self.sprops = tool.Blender.get_constraint_props()
self.props = tool.Blender.get_object_constraint_props(obj)
self.sprops = context.scene.BIMConstraintProperties
self.props = obj.BIMObjectConstraintProperties
self.file = tool.Ifc.get()
self.draw_add_ui()
@@ -136,24 +131,15 @@ class BIM_PT_object_constraints(Panel):
class BIM_UL_constraints(UIList):
def draw_item(
self,
context,
layout,
data: BIMConstraintProperties,
item: Constraint,
icon,
active_data,
active_propname,
):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
row = layout.row(align=True)
row.label(text=item.name)
if data.active_constraint_id == item.ifc_definition_id:
if data.is_editing == "IfcObjective":
if context.scene.BIMConstraintProperties.active_constraint_id == item.ifc_definition_id:
if context.scene.BIMConstraintProperties.is_editing == "IfcObjective":
row.operator("bim.edit_objective", text="", icon="CHECKMARK")
row.operator("bim.disable_editing_constraint", text="", icon="CANCEL")
elif data.active_constraint_id:
elif context.scene.BIMConstraintProperties.active_constraint_id:
row.operator("bim.remove_constraint", text="", icon="X").constraint = item.ifc_definition_id
else:
op = row.operator("bim.enable_editing_constraint", text="", icon="GREASEPENCIL")
@@ -162,16 +148,7 @@ class BIM_UL_constraints(UIList):
class BIM_UL_object_constraints(UIList):
def draw_item(
self,
context,
layout,
data: BIMObjectConstraintProperties,
item: Constraint,
icon,
active_data,
active_propname,
):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
row = layout.row(align=True)
row.label(text=item.name)
@@ -52,19 +52,12 @@ class ContextData:
def get_subcontexts(cls, context: ifcopenshell.entity_instance) -> list[dict[str, Any]]:
results = []
for subcontext in context.HasSubContexts:
target_scale_denominator = None
if subcontext.TargetScale is not None:
scale_value = float(subcontext.TargetScale)
if scale_value > 0:
target_scale_denominator = 1.0 / scale_value
results.append(
{
"id": subcontext.id(),
"context_type": subcontext.ContextType,
"context_identifier": subcontext.ContextIdentifier,
"target_view": subcontext.TargetView,
"target_scale_denominator": target_scale_denominator,
}
)
return results
@@ -30,7 +30,6 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from typing import TYPE_CHECKING
class BIMContextProperties(PropertyGroup):
@@ -67,10 +66,3 @@ class BIMContextProperties(PropertyGroup):
)
active_context_id: IntProperty(name="Active Context Id")
context_attributes: CollectionProperty(name="Context Attributes", type=Attribute)
if TYPE_CHECKING:
contexts: str
subcontexts: str
target_views: str
active_context_id: int
context_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
+2 -24
View File
@@ -33,14 +33,13 @@ class BIM_PT_context(bpy.types.Panel):
@classmethod
def poll(cls, context):
return bool(tool.Ifc.get())
return tool.Ifc.get()
def draw(self, context):
if not ContextData.is_loaded:
ContextData.load()
assert self.layout
props = tool.Context.get_context_props()
props = context.scene.BIMContextProperties
row = self.layout.row(align=True)
row.prop(props, "contexts", text="")
@@ -84,26 +83,5 @@ class BIM_PT_context(bpy.types.Panel):
row.label(text=subcontext["context_type"])
row.label(text=subcontext["context_identifier"])
row.label(text=subcontext["target_view"])
scale_col = row.column()
scale_col.ui_units_x = 6
if subcontext["target_scale_denominator"] is not None:
scale_value = subcontext["target_scale_denominator"]
if scale_value == int(scale_value):
scale_str = str(int(scale_value))
if len(scale_str) > 4:
scale_str = f"{float(scale_value):.3g}"
else:
scale_str = f"{scale_value:.4g}"
if len(scale_str) > 4:
scale_str = f"{float(scale_value):.3g}"
scale_col.label(text=f"1:{scale_str}")
else:
scale_col.label(text="")
row.operator("bim.enable_editing_context", icon="GREASEPENCIL", text="").context = subcontext["id"]
row.operator("bim.remove_context", icon="X", text="").context = subcontext["id"]
@@ -59,14 +59,10 @@ classes = (
operator.ExpandCostItemRate,
operator.ExpandCostItems,
operator.ExportCostSchedules,
operator.ExportCostSchedulesToPDF,
operator.HighlightProductCostItem,
operator.ImportCostScheduleCsv,
operator.RefreshCostScheduleCsv,
operator.RemoveCostScheduleCsvLink,
operator.LoadCostItemElementQuantities,
operator.LoadCostItemQuantities,
operator.ShowAssignedCostRate,
operator.LoadCostItemResourceQuantities,
operator.LoadCostItemTaskQuantities,
operator.LoadCostItemTypes,
+31 -79
View File
@@ -24,7 +24,7 @@ import ifcopenshell.util.element
import ifcopenshell.util.unit
import bonsai.tool as tool
from ifcopenshell.util.doc import get_entity_doc, get_predefined_type_doc
from typing import Any, Union
from typing import Any
def refresh():
@@ -33,19 +33,13 @@ def refresh():
CostItemQuantitiesData.is_loaded = False
CostItem = dict[str, Any]
CostQuantity = dict[str, Any]
CostSchedule = dict[str, Any]
Currency = dict[str, Any]
class CostSchedulesData:
data = {}
is_loaded = False
_cost_values: dict[int, dict[str, Any]]
@classmethod
def load(cls) -> None:
def load(cls):
cls.data = {
"predefined_types": cls.get_cost_schedule_types(),
"total_cost_schedules": cls.total_cost_schedules(),
@@ -56,42 +50,22 @@ class CostSchedulesData:
"cost_values": cls.cost_values(),
"quantity_types": cls.quantity_types(),
"currency": cls.currency(),
"csv_filepaths": cls.get_csv_filepaths(),
}
cls.is_loaded = True
@classmethod
def get_csv_filepaths(cls) -> dict[int, str]:
"""Get CSV filepaths for cost schedules from document references."""
filepaths = {}
cost_docs_document = tool.Cost.get_or_create_cost_documents()
if cost_docs_document:
references = tool.Document.get_document_references(cost_docs_document)
for reference in references:
if reference.Description and "Cost Schedule ID:" in reference.Description:
schedule_id_str = reference.Description.split("Cost Schedule ID:")[1].strip()
try:
schedule_id = int(schedule_id_str)
filepaths[schedule_id] = reference.Location
except ValueError:
pass
return filepaths
@classmethod
def currency(cls) -> Union[Currency, None]:
def currency(cls):
unit = tool.Unit.get_project_currency_unit()
if unit:
return {"id": unit.id(), "name": unit.Currency}
@classmethod
def total_cost_schedules(cls) -> int:
def total_cost_schedules(cls):
return len(tool.Ifc.get().by_type("IfcCostSchedule"))
@classmethod
def schedules(cls) -> list[CostSchedule]:
results: list[CostSchedule] = []
def schedules(cls):
results = []
props = tool.Cost.get_cost_props()
if props.active_cost_schedule_id:
schedule = tool.Ifc.get().by_id(props.active_cost_schedule_id)
@@ -114,34 +88,33 @@ class CostSchedulesData:
return results
@classmethod
def is_editing_rates(cls) -> bool:
def is_editing_rates(cls):
props = tool.Cost.get_cost_props()
ifc_id = props.active_cost_schedule_id
if not ifc_id:
return False
return
return tool.Ifc.get().by_id(ifc_id).PredefinedType == "SCHEDULEOFRATES"
@classmethod
def cost_items(cls) -> dict[int, CostItem]:
def cost_items(cls):
cls._cost_values = {}
results: dict[int, CostItem] = {}
results = {}
for cost_item in tool.Ifc.get().by_type("IfcCostItem"):
data: CostItem = {}
data = {}
cls._load_cost_item_quantities(cost_item, data)
cls._load_cost_values(cost_item, data)
cls._load_nesting_index(cost_item, data)
cls._load_assigned_cost_rate(cost_item, data)
results[cost_item.id()] = data
return results
@classmethod
def _load_nesting_index(cls, cost_item: ifcopenshell.entity_instance, data: CostItem) -> None:
def _load_nesting_index(cls, cost_item, data):
data["NestingIndex"] = None
for rel in cost_item.Nests or []:
data["NestingIndex"] = rel.RelatedObjects.index(cost_item)
@classmethod
def _load_cost_values(cls, root_element: ifcopenshell.entity_instance, data: CostItem) -> None:
def _load_cost_values(cls, root_element, data):
# data["CostValues"] = []
data["CategoryValues"] = {}
data["UnitBasisValueComponent"] = None
@@ -150,7 +123,6 @@ class CostSchedulesData:
data["TotalCost"] = 0.0
has_unit_basis = False
is_sum = False
values: list[ifcopenshell.entity_instance]
if root_element.is_a("IfcCostItem"):
values = root_element.CostValues
elif root_element.is_a("IfcConstructionResource"):
@@ -175,15 +147,10 @@ class CostSchedulesData:
else:
data["TotalCost"] = data["TotalAppliedValue"] * cost_quantity
if is_sum:
pass # If it is None it doesn't allow me to assign a cost rate composed by sum
# data["TotalAppliedValue"] = None
data["TotalAppliedValue"] = None
@classmethod
def _load_assigned_cost_rate(cls, cost_item: ifcopenshell.entity_instance, data: CostItem) -> None:
data["AssignedCostRate"] = tool.Cost.get_assigned_rate_cost_item(cost_item)
@classmethod
def _load_cost_item_quantities(cls, cost_item: ifcopenshell.entity_instance, data: CostItem) -> None:
def _load_cost_item_quantities(cls, cost_item, data):
# parametric_quantities = []
# for rel in cost_item.Controls:
# for related_object in rel.RelatedObjects or []:
@@ -191,10 +158,8 @@ class CostSchedulesData:
# parametric_quantities.extend(quantities)
data["TotalCostQuantity"] = ifcopenshell.util.cost.get_total_quantity(cost_item)
data["UnitSymbol"] = "-"
data["QuantityType"] = None
quantities: list[ifcopenshell.entity_instance] = cost_item.CostQuantities
if quantities:
quantity = quantities[0]
if cost_item.CostQuantities:
quantity = cost_item.CostQuantities[0]
data["QuantityType"] = quantity.is_a()
unit = ifcopenshell.util.unit.get_property_unit(quantity, tool.Ifc.get())
if unit:
@@ -248,35 +213,25 @@ class CostSchedulesData:
) -> list[int]:
if not element.is_a("IfcObject"):
return []
cost_quantities: list[ifcopenshell.entity_instance] = cost_item.CostQuantities
cost_quantities = cost_item.CostQuantities
if not cost_quantities:
return []
results: list[int] = []
relationship: ifcopenshell.entity_instance
results = []
for relationship in element.IsDefinedBy:
if not relationship.is_a("IfcRelDefinesByProperties"):
continue
qto: ifcopenshell.entity_instance = relationship.RelatingPropertyDefinition
qto = relationship.RelatingPropertyDefinition
if not qto.is_a("IfcElementQuantity"):
continue
prop: ifcopenshell.entity_instance
for prop in qto.Quantities:
if prop in cost_quantities:
results.append(prop.id())
return results
@classmethod
def _load_cost_value(
cls,
root_element: ifcopenshell.entity_instance,
root_element_data: CostItem,
cost_value: ifcopenshell.entity_instance,
) -> None:
def _load_cost_value(cls, root_element, root_element_data, cost_value):
value_data = cost_value.get_info()
del value_data["AppliedValue"]
if tool.Cost.get_assigned_rate_cost_item(root_element):
root_element = tool.Cost.get_assigned_rate_cost_item(root_element)
if value_data["UnitBasis"]:
data = cost_value.UnitBasis.get_info()
data["ValueComponent"] = data["ValueComponent"].wrappedValue
@@ -301,8 +256,8 @@ class CostSchedulesData:
cls._load_cost_value(root_element, root_element_data, component)
@classmethod
def cost_quantities(cls) -> list[CostQuantity]:
results: list[CostQuantity] = []
def cost_quantities(cls):
results = []
props = tool.Cost.get_cost_props()
ifc_id = props.active_cost_item_id
if not ifc_id:
@@ -312,7 +267,7 @@ class CostSchedulesData:
return results
@classmethod
def cost_values(cls) -> list[dict[str, str]]:
def cost_values(cls):
props = tool.Cost.get_cost_props()
ifc_id = props.active_cost_item_id
if not ifc_id:
@@ -320,12 +275,14 @@ class CostSchedulesData:
return ifcopenshell.util.cost.get_cost_values(tool.Ifc.get().by_id(ifc_id))
@classmethod
def quantity_types(cls) -> list[tuple[str, str, str]]:
assert (entity := tool.Ifc.schema().declaration_by_name("IfcPhysicalSimpleQuantity").as_entity())
return [(t.name(), t.name(), "") for t in entity.subtypes()]
def quantity_types(cls):
return [
(t.name(), t.name(), "")
for t in tool.Ifc.schema().declaration_by_name("IfcPhysicalSimpleQuantity").subtypes()
]
@classmethod
def get_cost_schedule_types(cls) -> list[tuple[str, str, str]]:
def get_cost_schedule_types(cls):
types = ifcopenshell.util.cost.get_cost_schedule_types(tool.Ifc.get())
return [(t["name"], t["name"], t["description"]) for t in types]
@@ -338,22 +295,17 @@ class CostItemRatesData:
def load(cls):
cls.data = {
"schedule_of_rates": cls.schedule_of_rates(),
"cost_schedules": cls.cost_schedules(),
}
cls.is_loaded = True
@classmethod
def schedule_of_rates(cls) -> list[tuple[str, str, str]]:
def schedule_of_rates(cls):
return [
(str(s.id()), s.Name or "Unnamed", "")
for s in tool.Ifc.get().by_type("IfcCostSchedule")
if s.PredefinedType == "SCHEDULEOFRATES"
]
@classmethod
def cost_schedules(cls) -> list[tuple[str, str, str]]:
return [(str(s.id()), s.Name or "Unnamed", "") for s in tool.Ifc.get().by_type("IfcCostSchedule")]
class CostItemQuantitiesData:
data = {}

Some files were not shown because too many files have changed in this diff Show More