Compare commits

..

1 Commits

Author SHA1 Message Date
Andrej730 e8f18b5b6d ci-bonsai-daily - include submodules for a test 2025-05-21 13:00:46 +05:00
823 changed files with 8648 additions and 21025 deletions
+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 -14
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
@@ -124,7 +124,4 @@ jobs:
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 --debug
+12 -24
View File
@@ -4,6 +4,9 @@ on:
push:
pull_request:
env:
PYTHON_VERSION: "3.12"
jobs:
lint-formatting:
runs-on: ubuntu-latest
@@ -14,39 +17,27 @@ jobs:
- name: Action - install python
uses: actions/setup-python@v5.3.0
with:
python-version: "3.9"
- name: Action - install python
uses: actions/setup-python@v5.3.0
with:
python-version: "3.11"
python-version: "${{ env.PYTHON_VERSION }}"
- name: Install dependencies
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
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
python3 -W error -m compileall -q src/ifcopenshell-python || ERROR=1
python3 -W error -m compileall -q src/bonsai || ERROR=1
exit $ERROR
continue-on-error: true
- name: Black formatter
id: black
id: linting
run: |
uvx black --diff --check .
continue-on-error: true
- name: Ruff check
id: ruff
run: |
uvx ruff check
python3 -m black --diff --check .
continue-on-error: true
- name: Final check
@@ -55,10 +46,7 @@ jobs:
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
if [ "${{ steps.linting.outcome }}" != "success" ]; then
echo "::error::Black formatting check failed, see 'linting' step for the details." && ERROR=1
fi
exit $ERROR
+4 -2
View File
@@ -55,6 +55,8 @@ jobs:
}
steps:
- uses: actions/checkout@v2
with:
submodules: recursive
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
@@ -96,7 +98,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 +106,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.5/blender-4.5.0-linux-x64.tar.xz
wget -q -O blender.tar.xz https://download.blender.org/release/Blender4.4/blender-4.4.0-linux-x64.tar.xz
tar -xf blender.tar.xz
# Setup Blender.
@@ -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 \
@@ -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
-1
View File
@@ -46,7 +46,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 \
+1 -2
View File
@@ -86,7 +86,6 @@ 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
@@ -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.4
0.8.3
+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}")
-1
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%" ^
-1
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 \
+2 -5
View File
@@ -1,6 +1,3 @@
python:
- 3.12
occt:
- 7.8.1
@@ -31,7 +28,7 @@ hdf5:
libboost_devel:
- '1.86'
libxml2:
- 2.13
- '2'
mpfr:
- '4'
gmp:
@@ -55,4 +52,4 @@ MACOSX_DEPLOYMENT_TARGET: # [osx]
- 10.13 # [osx and x86_64]
CONDA_BUILD_SYSROOT: # [osx]
- "/Users/runner/work/MacOSX10.13.sdk" # [osx and x86_64]
- "/Users/runner/work/MacOSX10.13.sdk" # [osx and x86_64]
+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 .
+26 -48
View File
@@ -1,62 +1,40 @@
[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
]
# Just to add a quick insert of `# pyright: ignore[xxx]` comments in Pylance.
enableTypeIgnoreComments = false
+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 -6
View File
@@ -87,7 +87,7 @@ BLENDER_PLATFORM:=windows-x64
endif
# Current build commit hash.
OLD:=ec13294
OLD:=c49ca69
.PHONY: bump
bump:
ifndef NEW
@@ -138,7 +138,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 +193,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 && \
+1 -2
View File
@@ -40,8 +40,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"
+5 -7
View File
@@ -22,8 +22,7 @@ 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,6 +96,7 @@ 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,
@@ -105,8 +105,6 @@ classes = [
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,6 +116,7 @@ classes = [
operator.OpenUpstream,
operator.OpenUri,
operator.ReloadIfcFile,
operator.RemoveIfcFile,
operator.RevertClippingPlaneCut,
operator.SelectDir,
operator.SelectIfcFile,
@@ -127,7 +126,6 @@ classes = [
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 +134,7 @@ classes = [
prop.BIMProperties,
prop.IfcParameter,
prop.PsetQto,
prop.GlobalId,
prop.BIMObjectProperties,
prop.BIMCollectionProperties,
prop.BIMMeshProperties,
@@ -146,7 +145,6 @@ classes = [
ui.BIM_UL_clipping_plane,
ui.BIM_UL_generic,
ui.BIM_UL_topics,
ui.DocPreferences,
ui.BIM_ADDON_preferences,
# Tabs panel
ui.BIM_PT_tabs,
@@ -154,10 +152,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,
@@ -499,10 +499,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",
@@ -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"
+5 -1
View File
@@ -76,7 +76,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(
+11 -64
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
@@ -394,11 +350,6 @@ def load_post(scene):
aggregate_props = tool.Aggregate.get_aggregate_props()
nest_props = tool.Nest.get_nest_props()
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,13 +360,9 @@ 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):
# 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
scene.tool_settings.snap_elements_base = {"EDGE", "EDGE_PERPENDICULAR", "VERTEX", "EDGE_MIDPOINT", "FACE"}
tool.Blender.sync_old_preferences()
+25 -113
View File
@@ -27,9 +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 types import EllipsisType
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
@@ -40,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
@@ -56,61 +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: Union[bool, EllipsisType] = ...,
) -> 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
Possible values:
- ``...`` (default value) -
add search if possible. If it's not possible, there will be no warnings or errors.
- ``True`` - always add search, if it's not possible it will result in errors.
- ``False`` - never add search.
"""
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: Union[bool, EllipsisType] = ...,
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:
@@ -131,34 +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 attribute.data_type in ("string", "integer", "float") and (
enable_search is True or (enable_search is ... and attribute.ifc_class)
):
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:
@@ -167,22 +114,28 @@ def draw_attribute(
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)
@@ -196,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.
@@ -218,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"):
@@ -254,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)
@@ -327,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]
@@ -419,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)
@@ -479,12 +396,7 @@ def draw_filter(
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
+14 -15
View File
@@ -34,8 +34,7 @@ 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, Literal
IFC_CONNECTED_TYPE = Union[bpy.types.Material, bpy.types.Object]
@@ -70,7 +69,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 = ""
@@ -105,7 +104,7 @@ class IfcStore:
IfcStore.session_files = {}
@staticmethod
def get_file() -> ifcopenshell.file | None:
def get_file():
if IfcStore.file is None:
props = tool.Blender.get_bim_props()
IfcStore.set_path(props.ifc_file)
@@ -117,20 +116,20 @@ class IfcStore:
return IfcStore.file
@staticmethod
def set_path(value: str) -> None:
def set_path(value):
IfcStore.path = value
# Interpret relative paths as relative to .blend file.
if IfcStore.path and not os.path.isabs(IfcStore.path):
IfcStore.path = os.path.abspath(os.path.join(bpy.path.abspath("//"), IfcStore.path))
@staticmethod
def get_cache() -> ifcopenshell.geom.serializers.hdf5 | None:
def get_cache():
if IfcStore.cache is None and IfcStore.path:
prefs = tool.Blender.get_addon_preferences()
props = tool.Blender.get_bim_props()
ifc_key = IfcStore.path + IfcStore.file.wrapped_data.header.file_name.time_stamp
ifc_hash = hashlib.md5(ifc_key.encode("utf-8")).hexdigest()
os.makedirs(prefs.cache_dir, exist_ok=True)
IfcStore.cache_path = os.path.join(prefs.cache_dir, f"{ifc_hash}.h5")
os.makedirs(props.cache_dir, exist_ok=True)
IfcStore.cache_path = os.path.join(props.cache_dir, f"{ifc_hash}.h5")
cache_path = Path(IfcStore.cache_path)
cache_settings = ifcopenshell.geom.settings()
serializer_settings = ifcopenshell.geom.serializer_settings()
@@ -163,15 +162,15 @@ class IfcStore:
return IfcStore.cache
@staticmethod
def update_cache() -> None:
def update_cache():
if not IfcStore.cache:
return
assert IfcStore.cache_path
assert IfcStore.file
ifc_key = IfcStore.path + IfcStore.file.wrapped_data.header.file_name.time_stamp
ifc_hash = hashlib.md5(ifc_key.encode("utf-8")).hexdigest()
prefs = tool.Blender.get_addon_preferences()
new_cache_path = os.path.join(prefs.cache_dir, f"{ifc_hash}.h5")
props = tool.Blender.get_bim_props()
new_cache_path = os.path.join(props.cache_dir, f"{ifc_hash}.h5")
IfcStore.cache = None
try:
shutil.move(IfcStore.cache_path, new_cache_path)
@@ -183,11 +182,11 @@ class IfcStore:
IfcStore.get_cache()
@staticmethod
def load_file(path: str) -> None:
def load_file(path) -> None:
if not os.path.isfile(path):
return
extension = path.split(".")[-1]
prefs = tool.Blender.get_addon_preferences()
props = tool.Project.get_project_props()
if extension.lower() == "ifczip":
with tempfile.TemporaryDirectory() as unzipped_path:
with zipfile.ZipFile(path, "r") as zip_ref:
@@ -197,7 +196,7 @@ class IfcStore:
return
elif extension.lower() == "ifcxml":
IfcStore.file = ifcopenshell.file(ifcopenshell.ifcopenshell_wrapper.parse_ifcxml(path))
elif prefs.should_stream:
elif props.should_stream:
IfcStore.file = ifcopenshell.open(path, should_stream=True)
else:
IfcStore.file = ifcopenshell.open(path)
+10 -12
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, Iterable
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
@@ -231,8 +229,7 @@ class IfcImporter:
self.progress = 0
self.material_creator = MaterialCreator(ifc_import_settings, self)
prefs = tool.Blender.get_addon_preferences()
classes_to_wireframe_str = prefs.doc.classes_to_wireframe
classes_to_wireframe_str = tool.Drawing.get_document_props().classes_to_wireframe
self.classes_to_wireframe_list = [word.strip() for word in classes_to_wireframe_str.split(",")]
def profile_code(self, message: str) -> None:
@@ -303,6 +300,7 @@ 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()
@@ -663,7 +661,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
@@ -859,7 +857,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 = {}
@@ -1034,9 +1032,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
@@ -1069,7 +1067,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:
@@ -1078,7 +1076,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)
@@ -19,8 +19,6 @@
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
@@ -114,7 +112,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):
@@ -225,25 +223,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 +248,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 +263,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 +300,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 +311,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 +320,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 +385,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.exit_aggregate_mode(tool.Aggregate)
return {"FINISHED"}
@@ -21,6 +21,7 @@
import os
import ifcopenshell.api.alignment
import ifcopenshell.api.alignment.add_stationing_to_alignment
import bpy
import json
@@ -37,6 +38,8 @@ 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):
@@ -61,7 +64,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,11 +26,7 @@ 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
from typing import TYPE_CHECKING
def get_objs_for_operation(
@@ -79,9 +75,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,19 +89,8 @@ 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_attributes(element, props.attributes, callback=callback)
bonsai.bim.helper.import_attributes2(element, props.attributes, callback=callback)
props.is_editing_attributes = True
def execute(self, context):
@@ -124,8 +107,7 @@ class DisableEditingAttributes(bpy.types.Operator, AttributesOperator):
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")
props.is_editing_attributes = False
def execute(self, context):
for obj in get_objs_for_operation(self, context):
@@ -145,7 +127,7 @@ class EditAttributes(bpy.types.Operator, tool.Ifc.Operator):
if not obj or 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:
@@ -156,9 +138,6 @@ class EditAttributes(bpy.types.Operator, tool.Ifc.Operator):
props = tool.Blender.get_object_attribute_props(obj)
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)
@@ -225,139 +204,3 @@ class CopyAttributeToSelection(bpy.types.Operator, tool.Ifc.Operator):
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()
+1 -61
View File
@@ -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,7 +29,7 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from typing import TYPE_CHECKING, Union
from typing import TYPE_CHECKING
class BIMAttributeProperties(PropertyGroup):
@@ -40,62 +39,3 @@ class BIMAttributeProperties(PropertyGroup):
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)
+1 -78
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 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:
@@ -38,9 +33,7 @@ def draw_ui(context: bpy.types.Context, layout: bpy.types.UILayout, attributes)
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 +66,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)
+2 -7
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.")
@@ -1208,11 +1208,9 @@ class ActivateBcfViewpoint(bpy.types.Operator):
self.file = tool.Ifc.get()
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
assert context.scene
props = tool.Bcf.get_bcf_props()
blender_topic = props.active_topic
assert blender_topic
topic = bcfxml.topics[blender_topic.name]
if self.viewpoint_guid:
viewpoint_guid = self.viewpoint_guid
@@ -1236,7 +1234,6 @@ class ActivateBcfViewpoint(bpy.types.Operator):
cam_height = context.scene.render.resolution_y
cam_aspect = cam_width / cam_height
assert isinstance(obj.data, bpy.types.Camera)
obj.data.background_images.clear()
if viewpoint.snapshot:
obj.data.show_background_images = True
@@ -1276,7 +1273,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
if tool.Bcf.get_viewpoint_bitmaps(viewpoint):
self.create_bitmaps(bcfxml, viewpoint, topic)
self.setup_camera(viewpoint, obj, cam_aspect, context, cam_height, cam_width)
self.setup_camera(viewpoint, obj, cam_aspect, context)
return {"FINISHED"}
def setup_camera(
@@ -1285,8 +1282,6 @@ class ActivateBcfViewpoint(bpy.types.Operator):
obj: bpy.types.Object,
cam_aspect: float,
context: bpy.types.Context,
cam_height: float,
cam_width: float,
) -> None:
assert isinstance(obj.data, bpy.types.Camera)
if viewpoint.visualization_info.orthogonal_camera:
+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
@@ -20,20 +20,15 @@ import bpy
from . import ui, prop, operator
classes = (
operator.AddBSDDProperties,
operator.ImportBSDDClasses,
operator.GetBSDDClassificationProperties,
operator.LoadBSDDDictionaries,
operator.SearchBSDDClassifications,
operator.SearchBSDDProperties,
operator.SearchBSDDClass,
prop.BSDDDictionary,
prop.BSDDClassification,
prop.BSDDProperty,
prop.BSDDPset,
prop.BIMBSDDProperties,
ui.BIM_UL_bsdd_classifications,
ui.BIM_UL_bsdd_dictionaries,
ui.BIM_UL_bsdd_classes,
ui.BIM_UL_bsdd_properties,
ui.BIM_UL_bsdd_classifications,
ui.BIM_PT_bsdd,
)
+12 -60
View File
@@ -19,10 +19,7 @@
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):
@@ -35,73 +32,28 @@ class LoadBSDDDictionaries(bpy.types.Operator):
return {"FINISHED"}
class SearchBSDDClassifications(bpy.types.Operator):
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"}
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, 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(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)
+16 -102
View File
@@ -17,10 +17,8 @@
# 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,10 +30,9 @@ 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:
def get_active_dictionary(self, context):
if not BSDDData.is_loaded:
BSDDData.load()
return BSDDData.data["active_dictionary"]
@@ -43,110 +40,41 @@ def get_active_dictionary(self: "BIMBSDDProperties", context: object) -> tool.Bl
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):
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,
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")
uri: StringProperty(name="Namespace 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
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: StringProperty(name="Active Dictionary")
active_dictionary: EnumProperty(items=get_active_dictionary, name="Active Dictionary")
active_uri: StringProperty(name="Active URI")
dictionaries: CollectionProperty(name="Dictionaries", type=BSDDDictionary)
active_dictionary_index: IntProperty(name="Active Dictionary 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",
@@ -158,27 +86,13 @@ class BIMBSDDProperties(PropertyGroup):
description="Whether to display and assign only properties from IFC dictionary",
default=False,
)
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
)
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
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)
+5 -75
View File
@@ -16,15 +16,9 @@
# 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):
@@ -39,8 +33,7 @@ class BIM_PT_bsdd(Panel):
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()
@@ -61,7 +54,7 @@ class BIM_PT_bsdd(Panel):
selected_dictionary = None
if selected_dictionary:
layout.label(text="Selected Dictionary:")
layout.label(text="Selected dictionary:")
box = layout.box()
row = box.row(align=True)
row.label(text="Language")
@@ -77,17 +70,9 @@ class BIM_PT_bsdd(Panel):
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:
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(
@@ -101,64 +86,9 @@ class BIM_UL_bsdd_dictionaries(UIList):
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
@@ -33,7 +32,7 @@ def refresh():
class ClassificationsData:
data: dict[str, Any] = {}
data = {}
is_loaded = False
@classmethod
@@ -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
@@ -94,7 +79,7 @@ class ReferencesData:
class ClassificationReferencesData(ReferencesData):
data: dict[str, Any] = {}
data = {}
is_loaded = False
@classmethod
@@ -118,7 +103,7 @@ class ClassificationReferencesData(ReferencesData):
class MaterialClassificationsData(ReferencesData):
data: dict[str, Any] = {}
data = {}
is_loaded = False
@classmethod
@@ -134,7 +119,8 @@ class MaterialClassificationsData(ReferencesData):
results = []
props = tool.Material.get_material_props()
if material := props.active_material:
if props.materials and props.active_material_index < len(props.materials):
material = props.materials[props.active_material_index]
if material.ifc_definition_id:
element = tool.Ifc.get().by_id(material.ifc_definition_id)
for reference in ifcopenshell.util.classification.get_references(element):
@@ -145,7 +131,7 @@ class MaterialClassificationsData(ReferencesData):
class CostClassificationsData(ReferencesData):
data: dict[str, Any] = {}
data = {}
is_loaded = False
@classmethod
@@ -157,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,12 +115,11 @@ 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]
props = context.scene.BIMBSDDProperties
if props.active_dictionary == "ALL":
dictionaries = [d.uri for d in props.dictionaries if d.is_active]
else:
dictionaries = [cprops.classification_source]
dictionaries = [props.active_dictionary]
for uri in dictionaries:
if not (dictionary := tool.Bsdd.get_dictionary(uri)):
continue
@@ -128,8 +132,8 @@ class AddClassificationFromBSDD(bpy.types.Operator, tool.Ifc.Operator):
has_classification = True
if has_classification:
continue
classification = ifcopenshell.api.classification.add_classification(
tool.Ifc.get(), classification=dictionary["name"]
classification = ifcopenshell.api.run(
"classification.add_classification", tool.Ifc.get(), classification=dictionary["name"]
)
classification.Source = dictionary["organizationNameOwner"]
classification.Edition = dictionary["version"]
@@ -142,11 +146,11 @@ 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()
bonsai.bim.helper.import_attributes("IfcClassification", props.classification_attributes)
bonsai.bim.helper.import_attributes2("IfcClassification", props.classification_attributes)
return {"FINISHED"}
@@ -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,10 +171,10 @@ 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_attributes("IfcClassificationReference", props.reference_attributes)
bonsai.bim.helper.import_attributes2("IfcClassificationReference", props.reference_attributes)
return {"FINISHED"}
@@ -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,9 +207,9 @@ 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_attributes(
bonsai.bim.helper.import_attributes2(
tool.Ifc.get().by_id(self.classification), props.classification_attributes, callback
)
props.active_classification_id = self.classification
@@ -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,9 +276,9 @@ 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_attributes(tool.Ifc.get().by_id(self.reference), props.reference_attributes)
bonsai.bim.helper.import_attributes2(tool.Ifc.get().by_id(self.reference), props.reference_attributes)
props.active_reference_id = self.reference
return {"FINISHED"}
@@ -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,7 +412,8 @@ 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]
@@ -420,8 +427,8 @@ class AddClassificationReferenceFromBSDD(bpy.types.Operator, tool.Ifc.Operator):
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.dictionary_name
)
tool.Classification.set_location(classification, bsdd_classification.dictionary_namespace_uri)
@@ -430,7 +437,8 @@ class AddClassificationReferenceFromBSDD(bpy.types.Operator, tool.Ifc.Operator):
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, Any, Union
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,11 @@ 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
row = self.layout.row()
row.prop(self.bprops, "active_dictionary", text="")
row = self.layout.row()
row.operator("bim.add_classification_from_bsdd", icon="ADD")
@@ -113,18 +112,12 @@ class BIM_PT_classifications(Panel):
class ReferenceUI:
layout: bpy.types.UILayout
data: type[
Union[
ClassificationReferencesData,
MaterialClassificationsData,
CostClassificationsData,
]
]
def draw_ui(self, context) -> None:
self.sprops = tool.Classification.get_classification_props()
self.bprops = tool.Bsdd.get_bsdd_props()
self.props = tool.Classification.get_classification_reference_props()
def draw_ui(self, context):
obj = context.active_object
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)
@@ -139,19 +132,19 @@ class ReferenceUI:
else:
self.draw_reference_ui(reference)
def draw_add_ui(self, context) -> None:
def draw_add_ui(self, context):
row = self.layout.row(align=True)
row.label(text="Source", icon="OUTLINER")
row.prop(self.sprops, "classification_source", text="")
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) -> None:
def draw_add_manual_ui(self, context):
row = self.layout.row()
row.prop(self.props, "classifications", text="")
if self.props.is_adding:
@@ -164,12 +157,18 @@ class ReferenceUI:
row = self.layout.row()
row.operator("bim.enable_adding_manual_classification_reference", text="Add Reference", icon="ADD")
def draw_add_bsdd_ui(self, context) -> None:
def draw_add_bsdd_ui(self, context):
row = self.layout.row()
row.prop(self.bprops, "active_dictionary", text="")
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",
@@ -190,8 +189,23 @@ class ReferenceUI:
)
op.obj = self.obj
op.obj_type = self.obj_type
row.operator("bim.get_bsdd_classification_properties", text="", icon="COPY_ID")
def draw_add_file_ui(self, context) -> None:
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"]:
row = self.layout.row(align=True)
row.label(text="No Active Classification Library", icon="ERROR")
@@ -225,13 +239,13 @@ class ReferenceUI:
"active_library_reference_index",
)
def draw_editable_ui(self) -> None:
def draw_editable_ui(self):
row = self.layout.row(align=True)
row.operator("bim.edit_classification_reference", text="Save changes", icon="CHECKMARK")
row.operator("bim.disable_editing_classification_reference", text="", icon="CANCEL")
bonsai.bim.helper.draw_attributes(self.props.reference_attributes, self.layout)
def draw_reference_ui(self, reference: dict[str, Any]) -> None:
def draw_reference_ui(self, reference):
row = self.layout.row(align=True)
if self.file.schema == "IFC2X3":
name = reference["ItemReference"] or "No Identification"
@@ -327,16 +341,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,13 +59,10 @@ 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_attributes(tool.Ifc.get().by_id(self.constraint), props.constraint_attributes)
bonsai.bim.helper.import_attributes2(tool.Ifc.get().by_id(self.constraint), props.constraint_attributes)
props.active_constraint_id = self.constraint
return {"FINISHED"}
@@ -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
@@ -84,26 +84,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,11 @@ 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,
@@ -86,6 +83,7 @@ classes = (
prop.CostItem,
prop.CostItemQuantity,
prop.CostItemType,
prop.CostItemsMapping,
prop.ScheduleColumn,
prop.BIMCostProperties,
ui.BIM_PT_cost_schedules,
+5 -37
View File
@@ -56,29 +56,9 @@ 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]:
unit = tool.Unit.get_project_currency_unit()
@@ -130,7 +110,6 @@ class CostSchedulesData:
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
@@ -175,12 +154,7 @@ 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
@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)
data["TotalAppliedValue"] = None
@classmethod
def _load_cost_item_quantities(cls, cost_item: ifcopenshell.entity_instance, data: CostItem) -> None:
@@ -191,7 +165,6 @@ 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]
@@ -275,8 +248,6 @@ class CostSchedulesData:
) -> None:
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
@@ -321,8 +292,10 @@ class CostSchedulesData:
@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()]
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]]:
@@ -338,7 +311,6 @@ class CostItemRatesData:
def load(cls):
cls.data = {
"schedule_of_rates": cls.schedule_of_rates(),
"cost_schedules": cls.cost_schedules(),
}
cls.is_loaded = True
@@ -350,10 +322,6 @@ class CostItemRatesData:
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 = {}
+17 -201
View File
@@ -19,14 +19,12 @@
# pyright: reportUnnecessaryTypeIgnoreComment=error
import bpy
import textwrap
import ifcopenshell.api.nest
import ifcopenshell.api
import bonsai.tool as tool
from bpy_extras.io_utils import ImportHelper, ExportHelper
import bonsai.tool as tool
import bonsai.core.cost as core
from pathlib import Path
from typing import get_args, TYPE_CHECKING, Literal
@@ -47,7 +45,6 @@ class AddCostSchedule(bpy.types.Operator, tool.Ifc.Operator):
def draw(self, context):
layout = self.layout
assert layout
props = tool.Cost.get_cost_props()
layout.prop(self, "name", text="Name")
layout.prop(props, "cost_schedule_predefined_types", text="Type")
@@ -151,11 +148,8 @@ class ExpandCostItem(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "Expand this cost item"
cost_item: bpy.props.IntProperty()
if TYPE_CHECKING:
cost_item: int
def _execute(self, context):
core.expand_cost_item(tool.Cost, cost_item_id=self.cost_item)
core.expand_cost_item(tool.Cost, cost_item=tool.Ifc.get().by_id(self.cost_item))
class ExpandCostItems(bpy.types.Operator, tool.Ifc.Operator):
@@ -163,6 +157,7 @@ class ExpandCostItems(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Expand Cost Items"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Expand all cost items"
cost_items: bpy.props.StringProperty()
def _execute(self, context):
core.expand_cost_items(tool.Cost)
@@ -175,11 +170,8 @@ class ContractCostItem(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "Contract a cost item"
cost_item: bpy.props.IntProperty()
if TYPE_CHECKING:
cost_item: int
def _execute(self, context):
core.contract_cost_item(tool.Cost, cost_item_id=self.cost_item)
core.contract_cost_item(tool.Cost, cost_item=tool.Ifc.get().by_id(self.cost_item))
class ContractCostItems(bpy.types.Operator, tool.Ifc.Operator):
@@ -187,6 +179,7 @@ class ContractCostItems(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Contract Cost Item"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Collapse cost item tree"
cost_item: bpy.props.IntProperty()
def _execute(self, context):
core.contract_cost_items(tool.Cost)
@@ -198,9 +191,6 @@ class RemoveCostItem(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
cost_item: bpy.props.IntProperty()
if TYPE_CHECKING:
cost_item: int
def _execute(self, context):
core.remove_cost_item(tool.Ifc, tool.Cost, cost_item_id=self.cost_item)
@@ -547,16 +537,11 @@ class SelectCostScheduleProducts(bpy.types.Operator):
class ImportCostScheduleCsv(bpy.types.Operator, ImportHelper, tool.Ifc.Operator):
bl_idname = "bim.import_cost_schedule_csv"
bl_label = "Import Cost Schedule CSV"
bl_description = "Import cost schedule from the provided .csv file."
bl_description = "Import cost schdule from the provided .csv file."
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".csv"
filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"})
is_schedule_of_rates: bpy.props.BoolProperty(name="Is Schedule Of Rates", default=False)
use_relative_path: bpy.props.BoolProperty(
name="Use Relative Path",
description="Store the CSV filepath relative to the currently opened IFC file",
default=False,
)
@classmethod
def poll(cls, context):
@@ -567,82 +552,22 @@ class ImportCostScheduleCsv(bpy.types.Operator, ImportHelper, tool.Ifc.Operator)
return True
def _execute(self, context):
store_path = self.filepath
if self.use_relative_path:
store_path = tool.Ifc.get_uri(self.filepath, use_relative_path=True)
resolved_path = Path(tool.Ifc.resolve_uri(self.filepath))
if not resolved_path.exists():
self.report({"ERROR"}, f"File does not exist: '{store_path}' (resolved to '{resolved_path}')")
return {"CANCELLED"}
cost_schedule = core.import_cost_schedule_csv(tool.Cost, str(resolved_path), self.is_schedule_of_rates)
if cost_schedule:
core.add_csv_filepath(tool.Cost, store_path, self.is_schedule_of_rates, cost_schedule)
return {"FINISHED"}
return {"CANCELLED"}
def draw(self, context):
row = self.layout.row()
row.prop(self, "is_schedule_of_rates")
row = self.layout.row()
row.prop(self, "use_relative_path")
cost_schedule = core.import_cost_schedule_csv(tool.Cost, self.filepath, self.is_schedule_of_rates)
core.add_csv_filepath(tool.Cost, self.filepath, self.is_schedule_of_rates, cost_schedule)
return {"FINISHED"}
class RefreshCostScheduleCsv(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.refresh_cost_schedule_csv"
bl_label = "Refresh Cost Schedule CSV"
bl_description = "Refresh cost schedule data from the associated CSV file"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
props = tool.Cost.get_cost_props()
if not props.active_cost_schedule_id:
cls.poll_message_set("No active cost schedule")
return False
file_path = tool.Cost.get_cost_schedule_csv_filepath(props.active_cost_schedule_id)
if not file_path:
cls.poll_message_set("No CSV file associated with this cost schedule")
return False
return True
def _execute(self, context):
core.refresh_cost_schedule_csv(tool.Cost)
core.refresh_cost_schedule_csv(tool.Ifc, tool.Cost)
return {"FINISHED"}
class RemoveCostScheduleCsvLink(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_cost_schedule_csv_link"
bl_label = "Remove CSV Link"
bl_description = "Remove the link to the CSV file and delete the related document reference"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
props = tool.Cost.get_cost_props()
if not props.active_cost_schedule_id:
cls.poll_message_set("No active cost schedule")
return False
file_path = tool.Cost.get_cost_schedule_csv_filepath(props.active_cost_schedule_id)
if not file_path:
cls.poll_message_set("No CSV file associated with this cost schedule")
return False
return True
def _execute(self, context):
core.remove_cost_schedule_csv_link(tool.Cost)
return {"FINISHED"}
def invoke(self, context, event):
return context.window_manager.invoke_confirm(self, event)
class AddCostColumn(bpy.types.Operator):
bl_idname = "bim.add_cost_column"
bl_label = "Add Cost Column"
@@ -683,34 +608,6 @@ class LoadCostItemQuantities(bpy.types.Operator):
return {"FINISHED"}
class ShowAssignedCostRate(bpy.types.Operator):
bl_idname = "bim.show_assigned_cost_rate"
bl_label = "Info about the assigned cost item rate"
bl_options = {"REGISTER"}
assigned_rate_identification: bpy.props.StringProperty()
assigned_rate_name: bpy.props.StringProperty()
assigned_rate_description: bpy.props.StringProperty()
assigned_rate_total_value: bpy.props.FloatProperty()
def invoke(self, context, event):
wm = context.window_manager
return wm.invoke_props_dialog(self, width=450)
def execute(self, context):
# core.load_cost_item_quantities(tool.Cost) IS IT NECESSARY?
return {"FINISHED"}
def draw(self, context):
layout = self.layout
wrapper = textwrap.TextWrapper(width=80)
layout.label(text=f"ID: {self.assigned_rate_identification}")
layout.label(text=f"Name: {self.assigned_rate_name}")
layout.label(text="Description:")
for line in wrapper.wrap(str(self.assigned_rate_description)):
layout.label(text=line)
layout.label(text=f"Value: {self.assigned_rate_total_value}")
class LoadCostItemElementQuantities(bpy.types.Operator):
bl_idname = "bim.load_cost_item_element_quantities"
bl_label = "Load Cost Item Element Quantities"
@@ -761,10 +658,7 @@ class AssignCostValue(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
core.assign_cost_value(
tool.Ifc,
tool.Cost,
cost_item=tool.Ifc.get().by_id(self.cost_item),
cost_rate=tool.Ifc.get().by_id(self.cost_rate),
tool.Ifc, cost_item=tool.Ifc.get().by_id(self.cost_item), cost_rate=tool.Ifc.get().by_id(self.cost_rate)
)
@@ -774,9 +668,6 @@ class ExpandCostItemRate(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
cost_item: bpy.props.IntProperty()
if TYPE_CHECKING:
cost_item: int
def _execute(self, context):
core.expand_cost_item_rate(tool.Cost, self.cost_item)
return {"FINISHED"}
@@ -788,9 +679,6 @@ class ContractCostItemRate(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
cost_item: bpy.props.IntProperty()
if TYPE_CHECKING:
cost_item: int
def _execute(self, context):
core.contract_cost_item_rate(tool.Cost, self.cost_item)
return {"FINISHED"}
@@ -852,81 +740,6 @@ class ExportCostSchedules(bpy.types.Operator, ExportHelper):
self.layout.label(text="Select a directory.")
class ExportCostSchedulesToPDF(bpy.types.Operator, ExportHelper):
bl_idname = "bim.export_cost_schedules_to_pdf"
bl_label = "Export Cost Schedule to PDF"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Print chosen cost schedule to pdf."
filename_ext = ".pdf"
filter_glob: bpy.props.StringProperty(default="*.pdf", options={"HIDDEN"}, maxlen=255)
cost_schedules_items = []
def get_cost_schedules_enum_items(self, context):
return ExportCostSchedulesToPDF.cost_schedules_items
cost_schedules_enum: bpy.props.EnumProperty(
name="",
description="Choose IfcCostSchedule to print",
items=get_cost_schedules_enum_items,
)
should_print_summary: bpy.props.BoolProperty(
name="Should print summary",
description="Print summary at the end of the document",
default=True,
)
def draw(self, context):
layout = self.layout
box = layout.box()
box.label(text="Select Ifc Cost Schedule:")
box.prop(self, "cost_schedules_enum", text="")
layout.separator()
box = layout.box()
box.label(text="Export properties:")
box.prop(self, "should_print_summary")
@classmethod
def poll(cls, context):
try:
import typst
return True
except:
cls.poll_message_set(
"Typst not available.\nIt can be installed from Quality and\nControl -> Debug and using 'typst' with Pip Install.\n(Run Blender as Administrator)"
)
return False
def invoke(self, context, event):
ExportCostSchedulesToPDF.cost_schedules_items.clear()
file = tool.Ifc.get()
schedules = file.by_type("IfcCostSchedule")
for schedule in schedules:
ExportCostSchedulesToPDF.cost_schedules_items.append(
(
str(schedule.id()),
"{} ({})".format(
schedule.Name if schedule.Name is not None else "Unnamed",
schedule.PredefinedType if schedule.PredefinedType is not None else "UNTYPED",
),
"",
)
)
return ExportHelper.invoke(self, context, event)
def execute(self, context):
file = tool.Ifc.get()
self.props = tool.Cost.get_cost_props()
cost_schedule = file.by_id(int(self.cost_schedules_enum))
options = {"should_print_summary": self.should_print_summary}
core.export_cost_schedules_to_pdf(
tool.Cost, filepath=self.filepath, cost_schedule=cost_schedule, options=options
)
return {"FINISHED"}
class ClearCostItemAssignments(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.clear_cost_item_assignments"
bl_label = "Clear Cost Item Product Assignments"
@@ -988,10 +801,13 @@ class ReorderCostItem(bpy.types.Operator, tool.Ifc.Operator):
cost_item: bpy.props.IntProperty()
def _execute(self, context):
ifcopenshell.api.nest.reorder_nesting(
ifcopenshell.api.run(
"nest.reorder_nesting",
tool.Ifc.get(),
item=tool.Ifc.get().by_id(self.cost_item),
new_index=self.new_index,
**{
"item": tool.Ifc.get().by_id(self.cost_item),
"new_index": self.new_index,
},
)
tool.Cost.load_cost_schedule_tree()
+30 -31
View File
@@ -18,7 +18,6 @@
import bpy
import ifcopenshell.api
import ifcopenshell.api.cost
import bonsai.tool as tool
from bonsai.bim.module.classification.data import CostClassificationsData
from bonsai.bim.module.cost.data import CostSchedulesData, CostItemRatesData, CostItemQuantitiesData
@@ -34,51 +33,44 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from typing import TYPE_CHECKING, Literal, Union
from typing import TYPE_CHECKING, Literal
def get_schedule_of_rates(self: "BIMCostProperties", context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
def get_schedule_of_rates(self, context):
if not CostItemRatesData.is_loaded:
CostItemRatesData.load()
# return CostItemRatesData.data["schedule_of_rates"]
return CostItemRatesData.data["cost_schedules"]
return CostItemRatesData.data["schedule_of_rates"]
def update_schedule_of_rates(self: "BIMCostProperties", context: bpy.types.Context) -> None:
def update_schedule_of_rates(self, context):
tool.Cost.load_schedule_of_rates_tree(schedule_of_rates=tool.Ifc.get().by_id(int(self.schedule_of_rates)))
def get_quantity_types(self: "BIMCostProperties", context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
def get_quantity_types(self, context):
if not CostSchedulesData.is_loaded:
CostSchedulesData.load()
return CostSchedulesData.data["quantity_types"]
def get_product_quantity_names(
self: "BIMCostProperties", context: bpy.types.Context
) -> tool.Blender.BLENDER_ENUM_ITEMS:
def get_product_quantity_names(self, context):
if not CostItemQuantitiesData.is_loaded:
CostItemQuantitiesData.load()
return CostItemQuantitiesData.data["product_quantity_names"]
def get_process_quantity_names(
self: "BIMCostProperties", context: bpy.types.Context
) -> tool.Blender.BLENDER_ENUM_ITEMS:
def get_process_quantity_names(self, context):
if not CostItemQuantitiesData.is_loaded:
CostItemQuantitiesData.load()
return CostItemQuantitiesData.data["process_quantity_names"]
def get_resource_quantity_names(
self: "BIMCostProperties", context: bpy.types.Context
) -> tool.Blender.BLENDER_ENUM_ITEMS:
def get_resource_quantity_names(self, context):
if not CostItemQuantitiesData.is_loaded:
CostItemQuantitiesData.load()
return CostItemQuantitiesData.data["resource_quantity_names"]
def update_active_cost_item_index(self: "BIMCostProperties", context: bpy.types.Context) -> None:
def update_active_cost_item_index(self, context):
schedule = tool.Ifc.get().by_id(self.active_cost_schedule_id)
if schedule.PredefinedType == "SCHEDULEOFRATES":
tool.Cost.load_cost_item_types()
@@ -91,11 +83,11 @@ def update_cost_item_identification(self: "CostItem", context: bpy.types.Context
props = tool.Cost.get_cost_props()
if not props.is_cost_update_enabled or self.identification == "XXX":
return
ifc_file = tool.Ifc.get()
ifcopenshell.api.cost.edit_cost_item(
ifc_file,
cost_item=ifc_file.by_id(self.ifc_definition_id),
attributes={"Identification": self.identification},
self.file = tool.Ifc.get()
ifcopenshell.api.run(
"cost.edit_cost_item",
self.file,
**{"cost_item": self.file.by_id(self.ifc_definition_id), "attributes": {"Identification": self.identification}},
)
if props.active_cost_item_id == self.ifc_definition_id:
attribute = props.cost_item_attributes["Identification"]
@@ -106,11 +98,11 @@ def update_cost_item_name(self: "CostItem", context: bpy.types.Context) -> None:
props = tool.Cost.get_cost_props()
if not props.is_cost_update_enabled or self.name == "Unnamed":
return
ifc_file = tool.Ifc.get()
ifcopenshell.api.cost.edit_cost_item(
ifc_file,
cost_item=ifc_file.by_id(self.ifc_definition_id),
attributes={"Name": self.name},
self.file = tool.Ifc.get()
ifcopenshell.api.run(
"cost.edit_cost_item",
self.file,
**{"cost_item": self.file.by_id(self.ifc_definition_id), "attributes": {"Name": self.name}},
)
if props.active_cost_item_id == self.ifc_definition_id:
attribute = props.cost_item_attributes["Name"]
@@ -175,6 +167,15 @@ class CostItemQuantity(PropertyGroup):
total_cost_quantity: float
class CostItemsMapping(PropertyGroup):
cost_item_id: IntProperty(name="cost_item_id")
csv_filepath: StringProperty(name="filepath")
if TYPE_CHECKING:
cost_item_id: int
csv_filepath: str
class CostItemType(PropertyGroup):
name: StringProperty(name="Name")
ifc_definition_id: IntProperty(name="IFC Definition ID")
@@ -280,6 +281,7 @@ class BIMCostProperties(PropertyGroup):
custom_currency: StringProperty(
name="Custom Currency", default="USD", description="Custom Currency in ISO 4217 format"
)
cost_schedule_files: CollectionProperty(name="Cost Schedule Files", type=CostItemsMapping)
if TYPE_CHECKING:
cost_schedule_predefined_types: str
@@ -334,7 +336,4 @@ class BIMCostProperties(PropertyGroup):
show_cost_item_operators: bool
currency: str
custom_currency: str
@property
def active_cost_item(self) -> Union[CostItem, None]:
return tool.Blender.get_active_uilist_element(self.cost_items, self.active_cost_item_index)
cost_schedule_files: bpy.types.bpy_prop_collection_idprop[CostItemsMapping]
+13 -30
View File
@@ -53,7 +53,6 @@ class BIM_PT_cost_schedules(Panel):
if CostSchedulesData.data["total_cost_schedules"]:
row.alignment = "RIGHT"
row.operator("bim.export_cost_schedules", icon="EXPORT", text="Export All Schedules")
row.operator("bim.export_cost_schedules_to_pdf", icon="OUTPUT", text="")
row = self.layout.row(align=True)
row.label(text=f"{CostSchedulesData.data['total_cost_schedules']} Cost Schedules Found", icon="TEXT")
else:
@@ -77,14 +76,17 @@ class BIM_PT_cost_schedules(Panel):
col = row0.column()
col.label(text="Linked CSV:")
row_1 = col.row(align=True)
csv_filepaths = CostSchedulesData.data["csv_filepaths"]
file_path = csv_filepaths.get(self.props.active_cost_schedule_id)
if file_path:
row_1.label(text=file_path)
if self.props.active_cost_schedule_id in [item.cost_item_id for item in self.props.cost_schedule_files]:
file = next(
(
item.csv_filepath
for item in self.props.cost_schedule_files
if item.cost_item_id == self.props.active_cost_schedule_id
),
None,
)
row_1.label(text=file)
row_1.operator("bim.refresh_cost_schedule_csv", icon="FILE_REFRESH", text="")
row_1.operator("bim.remove_cost_schedule_csv_link", icon="X", text="")
else:
row_1.label(text="No CSV file found")
row_1.operator("bim.import_cost_schedule_csv", icon="IMPORT", text="")
@@ -224,7 +226,7 @@ class BIM_PT_cost_schedules(Panel):
quantities = CostSchedulesData.data["cost_quantities"]
row = self.layout.row(align=True)
# In IFC, all quantities of IfcCostTime should have 1 type.
if quantities and cost_item.get("QuantityType"):
if quantities:
quantity_class = cost_item["QuantityType"]
row.label(text=quantity_class)
else:
@@ -606,8 +608,8 @@ class BIM_PT_cost_item_rates(Panel):
return False
if not CostSchedulesData.is_loaded:
return False
# if CostSchedulesData.data["is_editing_rates"]:
# return False
if CostSchedulesData.data["is_editing_rates"]:
return False
if total_cost_items > 0 and props.active_cost_item_index < total_cost_items:
return True
return False
@@ -653,8 +655,6 @@ class BIM_UL_cost_items_trait:
for column in props.columns:
split2.label(text=column.name)
split2.label(text="Total Cost")
split2.alignment = "LEFT"
split2.label(text="Rate")
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
@@ -685,8 +685,6 @@ class BIM_UL_cost_items_trait:
if self.props.change_cost_item_parent:
self.draw_parent_operator(row, item.ifc_definition_id)
self.draw_assigned_rate_column(split2, cost_item)
# TODO: reimplement "bim.copy_cost_item_values" somewhere with better UX
def draw_parent_operator(self, row, cost_item_id):
@@ -726,21 +724,6 @@ class BIM_UL_cost_items_trait:
else:
layout.label(text="-")
def draw_assigned_rate_column(self, layout, cost_item):
row = layout.row()
row.alignment = "LEFT"
if cost_item["AssignedCostRate"] is not None:
op = row.operator("bim.show_assigned_cost_rate", text="", emboss=False, icon="ZOOM_IN")
identification = cost_item["AssignedCostRate"].Identification
op.assigned_rate_identification = identification if identification is not None else "XXX"
op.assigned_rate_name = cost_item["AssignedCostRate"].Name or ""
op.assigned_rate_description = cost_item["AssignedCostRate"].Description or ""
op.assigned_rate_total_value = CostSchedulesData.data["cost_items"][cost_item["AssignedCostRate"].id()][
"TotalAppliedValue"
]
else:
row.label(text="-")
def draw_value_column(self, layout, cost_item):
if cost_item["TotalAppliedValue"]:
text = "{0:,.2f}".format(cost_item["TotalAppliedValue"]).replace(",", " ")
@@ -312,7 +312,6 @@ class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
import ifccsv
props = tool.Blender.get_csv_props()
ifc_file: ifcopenshell.file
if props.should_load_from_memory:
ifc_file = tool.Ifc.get()
else:
+56 -52
View File
@@ -26,10 +26,10 @@ import subprocess
import platform
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.api.pset
import ifcopenshell.geom
import ifcopenshell.util.element
import ifcopenshell.util.placement
import ifcopenshell.util.representation
import ifcopenshell.util.unit
import bonsai.tool as tool
import bonsai.core.debug as core
@@ -42,10 +42,7 @@ from bpy_extras.io_utils import ImportHelper, ExportHelper
from pathlib import Path
from bonsai import get_debug_info, format_debug_info
from bonsai.bim.ifc import IfcStore
from typing import get_args, Union, Any, TYPE_CHECKING, Literal, get_args, assert_never
if TYPE_CHECKING:
from bonsai.bim.prop import Attribute
from typing import get_args, Union
class CopyDebugInformation(bpy.types.Operator):
@@ -73,7 +70,6 @@ class CopyDebugInformation(bpy.types.Operator):
print(text_with_backticks)
print("-" * 80)
assert context.window_manager
context.window_manager.clipboard = text_with_backticks
return {"FINISHED"}
@@ -239,7 +235,7 @@ class ProfileImportIFC(bpy.types.Operator):
import cProfile
import pstats
profile_file = Path(self.profile_filename)
profile_file = Path(profile_filename)
cProfile.run("import bpy; bpy.ops.bim.load_project_elements()", str(profile_file))
p = pstats.Stats(str(profile_file))
p.sort_stats("cumulative").print_stats(50)
@@ -351,9 +347,8 @@ class SelectHighPolygonMeshes(bpy.types.Operator):
threshold: bpy.props.IntProperty()
def execute(self, context):
assert context.view_layer
for obj in context.view_layer.objects:
if isinstance(obj.data, bpy.types.Mesh) and len(obj.data.polygons) > self.threshold:
if obj.type == "MESH" and len(obj.data.polygons) > self.threshold:
obj.select_set(True)
return {"FINISHED"}
@@ -366,7 +361,6 @@ class SelectHighestPolygonMeshes(bpy.types.Operator):
percentile: bpy.props.IntProperty()
def execute(self, context):
assert context.view_layer
objects = [obj for obj in context.view_layer.objects if obj.type == "MESH"]
if objects:
percentile = len(max(objects, key=lambda o: len(o.data.polygons)).data.polygons) * self.percentile / 100
@@ -429,7 +423,7 @@ class InspectFromStepId(bpy.types.Operator):
new.int_value = inverse.id()
return {"FINISHED"}
def add_attribute(self, prop: "bpy.types.bpy_prop_collection_idprop[Attribute]", key: str, value: Any) -> None:
def add_attribute(self, prop, key, value):
if isinstance(value, tuple) and len(value) < 10:
for i, item in enumerate(value):
self.add_attribute(prop, key + f"[{i}]", item)
@@ -465,10 +459,8 @@ class InspectFromObject(bpy.types.Operator):
def poll(cls, context):
if not context.active_object:
cls.poll_message_set("No Active Object")
return False
elif not cls.get_active_object_ifc_definition(context):
cls.poll_message_set("Active Object doesn't have an IFC definition")
return False
else:
return True
@@ -500,7 +492,6 @@ class PrintObjectPlacement(bpy.types.Operator):
if self.create_empty_object:
bpy.ops.object.empty_add(type="ARROWS")
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
assert context.active_object
context.active_object.matrix_world = placement.transpose()
context.active_object.matrix_world.translation *= si_conversion
context.active_object.empty_display_size = self.arrow_size
@@ -744,14 +735,16 @@ class PurgeUnusedObjects(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Purge Unused Objects"
bl_options = {"REGISTER", "UNDO"}
object_type: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
object_type: bpy.props.EnumProperty(
name="Object Type",
items=((s, s.capitalize(), "") for s in get_args(tool.Debug.PurgeMergeObjectType)),
items=(
("TYPE", "Type", ""),
("PROFILE", "Profile", ""),
("STYLE", "Style", ""),
("MATERIAL", "Material", ""),
),
)
if TYPE_CHECKING:
object_type: tool.Debug.PurgeMergeObjectType
def _execute(self, context):
object_type = self.object_type
if object_type == "TYPE":
@@ -762,21 +755,27 @@ class PurgeUnusedObjects(bpy.types.Operator, tool.Ifc.Operator):
purged = tool.Style.purge_unused_styles()
elif object_type == "MATERIAL":
purged = tool.Material.purge_unused_materials()
elif object_type in ("APPLICATION", "ORGANIZATION"):
purged = core.purge_unused_elements(tool.Ifc, tool.Debug, "IfcApplication")
elif object_type == "PERSON":
purged = core.purge_unused_elements(tool.Ifc, tool.Debug, "IfcPerson")
elif object_type == "PERSON_AND_ORGANIZATION":
purged = core.purge_unused_elements(tool.Ifc, tool.Debug, "IfcPersonAndOrganization")
else:
assert_never(object_type)
self.report({"ERROR"}, f"Invalid object type {object_type}.")
return {"CANCELLED"}
self.report({"INFO"}, f"{purged} unused {object_type.replace('_', ' ').lower()}s were purged.")
self.report({"INFO"}, f"{purged} unused {object_type.lower()}s were purged.")
if purged == 0:
return
tool.Debug.refresh_ui_after_purge_merge(object_type)
if object_type == "PROFILE":
props = tool.Profile.get_profile_props()
if props.is_editing:
bpy.ops.bim.load_profiles()
elif object_type == "STYLE":
props = tool.Style.get_style_props()
if props.is_editing:
bpy.ops.bim.load_styles()
elif object_type == "MATERIAL":
props = tool.Material.get_material_props()
if props.is_editing:
bpy.ops.bim.load_materials()
class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator):
@@ -785,28 +784,28 @@ class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "For materials currently only IfcMaterials are supported"
bl_options = {"REGISTER", "UNDO"}
object_type: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
object_type: bpy.props.EnumProperty(
name="Object Type",
items=((s, s.capitalize(), "") for s in get_args(tool.Debug.PurgeMergeObjectType)),
items=(
("TYPE", "Type", ""),
("PROFILE", "Profile", ""),
("STYLE", "Style", ""),
("MATERIAL", "Material", ""),
),
)
if TYPE_CHECKING:
object_type: tool.Debug.PurgeMergeObjectType
def _execute(self, context):
object_type: str = self.object_type
if object_type in ("PROFILE", "TYPE"):
self.report({"ERROR"}, f"Unsupported object type {object_type}.")
if object_type in ("STYLE", "MATERIAL"):
merged_data = tool.Debug.merge_identical_objects(object_type)
else:
self.report({"ERROR"}, f"Invalid object type {object_type}.")
return {"CANCELLED"}
merged_data = tool.Debug.merge_identical_objects(object_type)
plural_object_type = f"{object_type.lower().replace('_', ' ')}s"
plural_object_type = f"{object_type.lower()}s"
if merged_data:
for element_type, element_names in merged_data.items():
print(f"- {element_type}:")
for name in element_names:
name = name or "Unnamed"
print(f" - '{name}'")
names = ", ".join([n or "Unnamed" for n in element_names])
print(f"- {element_type}: {names}")
merged = sum(len(v) for v in merged_data.values())
msg = " See system console for details." if merged else ""
@@ -815,7 +814,18 @@ class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator):
if merged == 0:
return
tool.Debug.refresh_ui_after_purge_merge(object_type)
if object_type == "PROFILE":
props = tool.Profile.get_profile_props()
if props.is_editing:
bpy.ops.bim.load_profiles()
elif object_type == "STYLE":
props = tool.Style.get_style_props()
if props.is_editing:
bpy.ops.bim.load_styles()
elif object_type == "MATERIAL":
props = tool.Material.get_material_props()
if props.is_editing:
bpy.ops.bim.load_materials()
class PipInstall(bpy.types.Operator):
@@ -824,10 +834,6 @@ class PipInstall(bpy.types.Operator):
bl_description = "Installs a package from PyPI"
name: bpy.props.StringProperty()
@classmethod
def description(cls, context, properties):
return f"Installs a package from PyPI: '{properties.name}'."
def execute(self, context):
blender_path = Path(bpy.app.binary_path).parent
target = next(
@@ -874,7 +880,6 @@ class DebugActiveDrawing(bpy.types.Operator):
)
def execute(self, context: bpy.types.Context):
ifc_file = tool.Ifc.get()
props = tool.Drawing.get_document_props()
drawing_item = props.drawings[props.active_drawing_index]
drawing = tool.Ifc.get().by_id(drawing_item.ifc_definition_id)
@@ -900,12 +905,12 @@ class DebugActiveDrawing(bpy.types.Operator):
original_exclude = ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing", "Exclude")
pset = tool.Pset.get_element_pset(drawing, "EPset_Drawing")
def drawing_fails_to_load(chunk_to_include: set[ifcopenshell.entity_instance]) -> bool:
def drawing_fails_to_load(chunk_to_include: set) -> bool:
current_elements = all_elements - chunk_to_include
excluded_guids = ", ".join([e.GlobalId for e in current_elements if hasattr(e, "GlobalId")])
new_exclude = "" if not original_exclude else f"{original_exclude}, "
new_exclude += excluded_guids
ifcopenshell.api.pset.edit_pset(ifc_file, pset=pset, properties={"Exclude": new_exclude})
tool.Ifc.run("pset.edit_pset", pset=pset, properties={"Exclude": new_exclude})
try:
bpy.ops.bim.create_drawing(sync=False)
@@ -914,7 +919,7 @@ class DebugActiveDrawing(bpy.types.Operator):
# print(e)
result = True
ifcopenshell.api.pset.edit_pset(ifc_file, pset=pset, properties={"Exclude": original_exclude})
tool.Ifc.run("pset.edit_pset", pset=pset, properties={"Exclude": original_exclude})
return result
def test_elements(elements: list[ifcopenshell.entity_instance], attempts: int = ATTEMPS) -> None:
@@ -1012,7 +1017,6 @@ class RestartBlender(bpy.types.Operator):
def execute(self, context):
# Save preferences manually since we're restarting Blender using .execv
# and it doens't have a chance to save them on exit.
assert context.preferences
if context.preferences.use_preferences_save:
bpy.ops.wm.save_userpref()
-1
View File
@@ -31,7 +31,6 @@ class BIM_PT_debug(Panel):
bl_parent_id = "BIM_PT_tab_quality_control"
def draw(self, context):
assert self.layout
layout = self.layout
props = tool.Debug.get_debug_props()
@@ -88,6 +88,7 @@ classes = (
operator.SelectAllDrawings,
operator.SelectAllSheets,
operator.SelectAssignedProduct,
operator.SelectDocIfcFile,
operator.OpenDocumentationWebUi,
prop.Variable,
prop.Drawing,
+8 -13
View File
@@ -86,8 +86,8 @@ class SheetsData:
project = tool.Ifc.get().by_type("IfcProject")[0]
titleblocks_dir = ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "TitleblocksDir")
if not titleblocks_dir:
prefs = tool.Blender.get_addon_preferences()
titleblocks_dir = prefs.doc.titleblocks_dir
props = tool.Drawing.get_document_props()
titleblocks_dir = props.titleblocks_dir
titleblocks_dir = tool.Ifc.resolve_uri(titleblocks_dir)
if os.path.exists(titleblocks_dir):
files.extend([str(f.stem) for f in Path(titleblocks_dir).glob("*.svg")])
@@ -285,7 +285,7 @@ class DecoratorData:
return thickness
@classmethod
def get_section_markers_display_data(cls, obj: bpy.types.Object) -> Union[dict[str, Any], None]:
def get_section_markers_display_data(cls, obj):
"""used by IfcAnnotations with ObjectType = "SECTION" """
result = cls.data.get(obj.name, None)
if result is not None:
@@ -327,11 +327,10 @@ class DecoratorData:
return display_data
@classmethod
def get_text_data(cls, obj: bpy.types.Object) -> dict[str, Any]:
def get_text_data(cls, obj: bpy.types.Object) -> dict:
"""used by Ifc Annotations with ObjectType = "TEXT" / "TEXT_LEADER"\n
returns font size in mm for current ifc text object"""
element = tool.Ifc.get_entity(obj)
assert element
props = tool.Drawing.get_text_props(obj)
# getting font size
pset_data = ifcopenshell.util.element.get_pset(element, "EPset_Annotation") or {}
@@ -372,18 +371,16 @@ class DecoratorData:
return {"Literals": literals_data, "FontSize": font_size, "Symbol": symbol, "Newline_At": newline_at}
@classmethod
def get_dimension_data(cls, obj: bpy.types.Object) -> dict[str, Any]:
def get_dimension_data(cls, obj):
"""used by Ifc Annotations with ObjectType:
DIMENSION / DIAMETER / SECTION_LEVEL / PLAN_LEVEL / RADIUS
"""
element = tool.Ifc.get_entity(obj)
assert element
dimension_style = "arrow"
fill_bg = False
classes = ifcopenshell.util.element.get_pset(element, "EPset_Annotation", "Classes")
if classes:
assert type(classes) is str
classes_split = classes.lower().split()
if "oblique" in classes_split:
dimension_style = "oblique"
@@ -409,17 +406,15 @@ class DecoratorData:
}
@classmethod
def get_fall_data(cls, obj: bpy.types.Object) -> dict[str, Union[str, None]]:
def get_fall_data(cls, obj):
object_type = None
if element := tool.Ifc.get_entity(obj):
object_type = ifcopenshell.util.element.get_predefined_type(element)
return {"object_type": object_type}
@classmethod
def get_symbol_data(cls, obj: bpy.types.Object) -> Union[str, None]:
element = tool.Ifc.get_entity(obj)
assert element
return tool.Drawing.get_annotation_symbol(element)
def get_symbol_data(cls, obj):
return tool.Drawing.get_annotation_symbol(tool.Ifc.get_entity(obj))
@classmethod
def object_decorators(cls, handler):
@@ -24,7 +24,6 @@ import bmesh
import shapely
import ifcopenshell
import ifcopenshell.util.element
import ifcopenshell.util.representation
import ifcopenshell.util.unit
import bonsai.tool as tool
import bonsai.bim.module.drawing.helper as helper
@@ -38,9 +37,8 @@ from bonsai.bim.module.drawing.data import DecoratorData, DrawingsData
from bonsai.bim.module.drawing.shaders import add_verts_sequence, add_offsets
from bonsai.bim.module.drawing.helper import format_distance
from timeit import default_timer as timer
from functools import cache
from typing import Optional, Union
from collections.abc import Iterator
from functools import lru_cache
from typing import Optional, Iterator, Type, Union
UNSPECIAL_ELEMENT_COLOR = (0.2, 0.2, 0.2, 1) # GREY
@@ -426,8 +424,8 @@ class BaseDecorator:
# font_size = 16 <-- this is a good default
# TODO: need to synchronize it better with svg
prefs = tool.Blender.get_addon_preferences()
magic_font_scale = prefs.doc.magic_font_scale
props = tool.Drawing.get_document_props()
magic_font_scale = props.magic_font_scale
font_size_px = int(magic_font_scale * mm_to_px) * font_size_mm / 2.5
pos = pos - line_no * font_size_px * rotation_matrix[1]
@@ -497,7 +495,7 @@ class BaseDecorator:
self.draw_label(context, text=text, line_no=line_number_start, multiline=True, **draw_label_kwargs)
@cache
@lru_cache(maxsize=None)
def format_value(self, context, value, custom_unit=None):
drawing_pset_data = DrawingsData.data["active_drawing_pset_data"]
precision = drawing_pset_data.get("MetricPrecision", None)
@@ -1204,19 +1202,7 @@ class PlanLevelDecorator(BaseDecorator):
text_dir *= -1
def get_text():
abs_z = verts[0].z
z = abs_z
if element:
spatial_container = ifcopenshell.util.element.get_container(element)
if spatial_container:
container_obj = tool.Ifc.get_object(spatial_container)
if container_obj:
spatial_container_z = container_obj.matrix_world.translation.z
z = abs_z - spatial_container_z
z = verts[0].z
rl = self.format_value(context, z)
text = "{}{}".format("" if z < 0 else "+", rl)
return text
@@ -1661,7 +1647,7 @@ class CutDecorator:
all_vertex_i_offset = 0
selected_vertex_i_offset = 0
classes_no_cut_str = self.addon_prefs.doc.classes_no_cut
classes_no_cut_str = tool.Drawing.get_document_props().classes_no_cut
classes_no_cut = [word.strip() for word in classes_no_cut_str.split(",")]
for obj in [o for o in bpy.context.visible_objects if o.type == "MESH"]:
@@ -1712,12 +1698,11 @@ class CutDecorator:
self.shader.bind()
black = (0, 0, 0, 1)
model_props = tool.Model.get_model_props()
if model_props.show_cut_decorator_fill:
for colour, element_fills in fills.items():
for verts_tris in element_fills:
for verts, tris in verts_tris:
self.draw_batch("TRIS", verts, colour, tris)
for colour, element_fills in fills.items():
for verts_tris in element_fills:
for verts, tris in verts_tris:
self.draw_batch("TRIS", verts, colour, tris)
gpu.state.point_size_set(2)
self.line_shader.uniform_float("lineWidth", 3.0)
@@ -1910,7 +1895,7 @@ class CutDecorator:
class DecorationsHandler:
decorators_classes: list[type[BaseDecorator]] = [
decorators_classes: list[Type[BaseDecorator]] = [
DimensionDecorator,
AngleDecorator,
GridDecorator,
@@ -1959,8 +1944,8 @@ class DecorationsHandler:
for object_type in ("SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT"):
self.decorators[object_type] = self.decorators["FALL"]
self.decorators["MULTI_SYMBOL"] = self.decorators["SYMBOL"]
prefs = tool.Blender.get_addon_preferences()
if drawing_font := prefs.doc.drawing_font:
props = tool.Drawing.get_document_props()
if drawing_font := props.drawing_font:
drawing_font_path = tool.Blender.get_data_dir_path(Path("fonts") / drawing_font)
if drawing_font_path.is_file():
font_id = blf.load(drawing_font_path.__str__())
+117 -134
View File
@@ -24,21 +24,18 @@ import bmesh
import shutil
import hashlib
import shapely
import shapely.ops
import subprocess
import numpy as np
import multiprocessing
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.api.document
import ifcopenshell.api.pset
import ifcopenshell.api.style
import ifcopenshell.ifcopenshell_wrapper
import ifcopenshell.geom
import ifcopenshell.util.element
import ifcopenshell.util.representation
import ifcopenshell.util.selector
import ifcopenshell.util.unit
import ifcopenshell.util.representation
import ifcopenshell.util.element
import bonsai.bim.helper
import bonsai.bim.handler
import bonsai.tool as tool
@@ -51,7 +48,7 @@ import bonsai.bim.export_ifc
from bpy_extras.io_utils import ImportHelper
from bonsai.bim.module.drawing.decoration import CutDecorator
from bonsai.bim.module.drawing.data import DecoratorData
from typing import NamedTuple, Union, Optional, Literal, TYPE_CHECKING, Any, TypedDict
from typing import NamedTuple, List, Union, Optional, Literal, TYPE_CHECKING, Any, TypedDict
from lxml import etree
from math import radians
from mathutils import Vector, Color, Matrix
@@ -64,7 +61,7 @@ from bpy_extras.image_utils import load_image
if TYPE_CHECKING:
from bonsai.bim.module.drawing.prop import RenderType
from bonsai.bim.module.project.prop import Link
from bpy.stub_internal import rna_enums
from bpy._typing import rna_enums
cwd = os.path.dirname(os.path.realpath(__file__))
@@ -85,8 +82,8 @@ class profile:
class LineworkContexts(NamedTuple):
body: list[list[int]]
annotation: list[list[int]]
body: List[List[int]]
annotation: List[List[int]]
class AddAnnotationType(bpy.types.Operator, tool.Ifc.Operator):
@@ -188,11 +185,9 @@ class DuplicateDrawing(bpy.types.Operator, tool.Ifc.Operator):
return True
def invoke(self, context, event):
assert context.window_manager
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
assert self.layout
row = self.layout
row.prop(self, "should_duplicate_annotations")
@@ -334,15 +329,7 @@ class CreateDrawing(bpy.types.Operator):
annotation_svg = None
with profile("Generate underlay"):
if ifcopenshell.util.element.get_pset(self.drawing, "EPset_Drawing", "HasUnderlay"):
drawing_style = self.cprops.get_active_drawing_style()
if not drawing_style:
self.report(
{"ERROR"},
f"Failed to create drawing '{self.drawing.Name}' - drawing has underlay but there's no active drawing underlay style.",
)
return {"FINISHED"}
underlay_svg = self.generate_underlay(context)
underlay_svg = self.generate_underlay(context)
with profile("Generate linework"):
if tool.Drawing.is_camera_orthographic():
@@ -882,7 +869,6 @@ class CreateDrawing(bpy.types.Operator):
cached_linework -= edited_guids
bim_props = tool.Blender.get_bim_props()
prefs = tool.Blender.get_addon_preferences()
files = {bim_props.ifc_file: tool.Ifc.get()}
props = tool.Project.get_project_props()
@@ -899,7 +885,7 @@ class CreateDrawing(bpy.types.Operator):
# Don't use draw.main() just whilst we're prototyping and experimenting
# TODO: hash paths are never used
ifc_hash = hashlib.md5(ifc_path.encode("utf-8")).hexdigest()
ifc_cache_path = os.path.join(prefs.cache_dir, f"{ifc_hash}.h5")
ifc_cache_path = os.path.join(bim_props.cache_dir, f"{ifc_hash}.h5")
self.serialiser.setFile(ifc)
drawing_elements = tool.Drawing.get_drawing_elements(self.camera_element, ifc_file=ifc)
@@ -1071,7 +1057,7 @@ class CreateDrawing(bpy.types.Operator):
# file 2 only has the groups we are interested in.
# in fact in the approach, it's only a single group
g2 = next(yield_groups(svg2))
g2 = list(yield_groups(svg2))[0]
# Loop over the cell paths
for pi, p in enumerate(g2.getElementsByTagName("path")):
@@ -1479,9 +1465,6 @@ class CreateDrawing(bpy.types.Operator):
classes.add(el.attrib["{http://www.ifcopenshell.org/ns}guid"])
is_closed_polygon = False
for path in el.findall("{http://www.w3.org/2000/svg}path"):
# Temporary hack.
if "d" not in path.attrib:
continue
for subpath in path.attrib["d"].split("M")[1:]:
subpath_co = "M" + subpath.strip(" Z")
# Round due to inaccuracies from Blender meshes and bisection
@@ -1738,9 +1721,7 @@ class OpenLayout(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
self.props = tool.Drawing.get_document_props()
sheet_item = tool.Drawing.get_active_sheet_item()
assert sheet_item
sheet = tool.Ifc.get().by_id(sheet_item.ifc_definition_id)
sheet = tool.Ifc.get().by_id(self.props.sheets[self.props.active_sheet_index].ifc_definition_id)
sheet_builder = sheeter.SheetBuilder()
sheet_builder.update_sheet_drawing_sizes(sheet)
core.open_layout(tool.Drawing, sheet=sheet)
@@ -1748,7 +1729,7 @@ class OpenLayout(bpy.types.Operator, tool.Ifc.Operator):
class SelectAllSheets(bpy.types.Operator):
bl_idname = "bim.select_all_sheets"
bl_label = "Select All Sheets"
bl_label = "Select All Sheetss"
view: bpy.props.StringProperty()
bl_description = "Select all sheets in the sheet list.\n\n" + "SHIFT+CLICK to deselect all sheets"
select_all: bpy.props.BoolProperty(name="Open All", default=True, options={"SKIP_SAVE"})
@@ -1804,20 +1785,14 @@ class OpenSheet(bpy.types.Operator):
tool.Ifc.get().by_id(s.ifc_definition_id) for s in self.props.sheets if s.is_sheet and s.is_selected
]
else:
sheet_item = tool.Drawing.get_active_sheet_item()
assert sheet_item
sheets = [tool.Ifc.get().by_id(sheet_item.ifc_definition_id)]
sheets = [tool.Ifc.get().by_id(self.props.sheets[self.props.active_sheet_index].ifc_definition_id)]
sheet_uris: list[str] = []
sheets_not_found: list[str] = []
warnings: list[tool.Drawing.SheetWarningType] = []
sheet_uris = []
sheets_not_found = []
for sheet in sheets:
if not sheet.is_a("IfcDocumentInformation"):
continue
warnings.extend(sheets_warnings := tool.Drawing.validate_sheet_files(sheet))
if sheets_warnings:
continue
sheet_builder = sheeter.SheetBuilder()
references = sheet_builder.build(sheet)
sheet_uri = references["SHEET"]
@@ -1827,11 +1802,6 @@ class OpenSheet(bpy.types.Operator):
if not os.path.exists(sheet_uri):
sheets_not_found.append(sheet.Name)
if warnings:
self.report({"ERROR"}, f"There were errors opening sheets. See system console for the details.")
print("-" * 10)
print("\n".join(str(w) for w in warnings))
if sheets_not_found:
msg = "Some sheets .svg/.pdf files were not found, need to create them first: \n{}.".format(
"\n".join(sheets_not_found)
@@ -1857,8 +1827,8 @@ class AddDrawingToSheet(bpy.types.Operator, tool.Ifc.Operator):
def poll(cls, context):
props = tool.Drawing.get_document_props()
# Won't be visible in UI anyway.
prefs = tool.Blender.get_addon_preferences()
if not props.sheets or not prefs.data_dir:
bim_props = tool.Blender.get_bim_props()
if not props.sheets or not bim_props.data_dir:
return False
if not tool.Drawing.get_active_drawing_item():
cls.poll_message_set("No drawing selected.")
@@ -1869,9 +1839,8 @@ class AddDrawingToSheet(bpy.types.Operator, tool.Ifc.Operator):
props = tool.Drawing.get_document_props()
active_drawing = props.drawings[props.active_drawing_index]
assert active_drawing
ifc_file = tool.Ifc.get()
active_sheet = tool.Drawing.get_active_sheet()
active_sheet = tool.Drawing.get_active_sheet(context)
drawing = tool.Ifc.get().by_id(active_drawing.ifc_definition_id)
drawing_reference = tool.Drawing.get_drawing_document(drawing)
@@ -1893,7 +1862,7 @@ class AddDrawingToSheet(bpy.types.Operator, tool.Ifc.Operator):
self.report({"ERROR"}, "The drawing must be generated before adding to a sheet.")
return
reference = ifcopenshell.api.document.add_reference(ifc_file, information=sheet)
reference = tool.Ifc.run("document.add_reference", information=sheet)
attributes = tool.Drawing.generate_reference_attributes(
reference,
Identification=str(
@@ -1909,7 +1878,7 @@ class AddDrawingToSheet(bpy.types.Operator, tool.Ifc.Operator):
Location=drawing_reference.Location,
Description="DRAWING",
)
ifcopenshell.api.document.edit_reference(ifc_file, reference=reference, attributes=attributes)
tool.Ifc.run("document.edit_reference", reference=reference, attributes=attributes)
sheet_builder = sheeter.SheetBuilder()
sheet_builder.add_drawing(reference, drawing, sheet)
@@ -1963,8 +1932,8 @@ class CreateSheets(bpy.types.Operator, tool.Ifc.Operator):
if not tool.Drawing.get_active_sheet_item(is_sheet=True):
cls.poll_message_set("No sheet selected.")
return False
prefs = tool.Blender.get_addon_preferences()
return props.sheets and prefs.data_dir
bim_props = tool.Blender.get_bim_props()
return props.sheets and bim_props.data_dir
def invoke(self, context, event):
# opening all sheets on shift+click
@@ -1980,23 +1949,13 @@ class CreateSheets(bpy.types.Operator, tool.Ifc.Operator):
props = tool.Drawing.get_document_props()
svg2pdf_command = tool.Blender.get_addon_preferences().svg2pdf_command
svg2dxf_command = tool.Blender.get_addon_preferences().svg2dxf_command
ifc_file = tool.Ifc.get()
if self.create_all:
sheets = [tool.Ifc.get().by_id(s.ifc_definition_id) for s in props.sheets if s.is_sheet and s.is_selected]
else:
sheet_item = tool.Drawing.get_active_sheet_item()
assert sheet_item
sheets = [tool.Ifc.get().by_id(sheet_item.ifc_definition_id)]
sheets = [tool.Ifc.get().by_id(props.sheets[props.active_sheet_index].ifc_definition_id)]
warnings: list[tool.Drawing.SheetWarningType] = []
n_sheets_created = 0
for sheet in sheets:
warnings.extend(sheet_warnings := tool.Drawing.validate_sheet_files(sheet))
if sheet_warnings:
continue
# Update any drawing boundary changes
sheet_builder = sheeter.SheetBuilder()
sheet_builder.update_sheet_drawing_sizes(sheet)
@@ -2028,9 +1987,9 @@ class CreateSheets(bpy.types.Operator, tool.Ifc.Operator):
has_sheet_reference = True
if not has_sheet_reference:
reference = ifcopenshell.api.document.add_reference(ifc_file, information=sheet)
ifcopenshell.api.document.edit_reference(
ifc_file,
reference = tool.Ifc.run("document.add_reference", information=sheet)
tool.Ifc.run(
"document.edit_reference",
reference=reference,
attributes=tool.Drawing.generate_reference_attributes(
reference, Location=tool.Ifc.get_uri(svg, use_relative_path=True), Description="SHEET"
@@ -2057,16 +2016,8 @@ class CreateSheets(bpy.types.Operator, tool.Ifc.Operator):
tool.Drawing.open_with_user_command(tool.Blender.get_addon_preferences().pdf_command, pdf)
else:
tool.Drawing.open_with_user_command(tool.Blender.get_addon_preferences().svg_command, svg)
n_sheets_created += 1
if not self.open_viewer:
self.report({"INFO"}, f"{n_sheets_created} sheets created...")
if warnings:
self.report({"ERROR"}, f"There were errors creating sheets. See system console for the details.")
print("-" * 10)
print("\n".join(str(w) for w in warnings))
self.report({"INFO"}, f"{len(sheets)} sheets created...")
class SelectAllDrawings(bpy.types.Operator):
@@ -2151,17 +2102,13 @@ class ActivateModel(bpy.types.Operator):
bl_idname = "bim.activate_model"
bl_label = "Activate Model"
bl_options = {"REGISTER", "UNDO"}
bl_description = (
"Activate the model view.\n\n"
"Show all objects (and apply status filters if they were enabled before) and hide all annotations."
)
bl_description = "Activate the model view, hide all annotations"
def execute(self, context):
dprops = tool.Drawing.get_document_props()
dprops.active_drawing_id = 0
model_props = tool.Model.get_model_props()
if model_props.show_cut_decorator:
CutDecorator.uninstall()
CutDecorator.uninstall()
# Preserve current visibility statuses for:
# - non-ifc objects
@@ -2184,7 +2131,7 @@ class ActivateModel(bpy.types.Operator):
if not bpy.app.background:
with context.temp_override(**tool.Blender.get_viewport_context()):
bpy.ops.object.hide_view_clear()
bpy.ops.bim.activate_status_filters(only_if_enabled=True)
bpy.ops.bim.activate_status_filters()
for obj in context.visible_objects:
element = tool.Ifc.get_entity(obj)
@@ -2254,9 +2201,7 @@ class ActivateDrawingBase(tool.Ifc.Operator):
if tool.Drawing.is_camera_orthographic():
core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=tool.Ifc.get().by_id(self.drawing))
model_props = tool.Model.get_model_props()
if model_props.show_cut_decorator:
CutDecorator.install(context)
CutDecorator.install(context)
tool.Drawing.show_decorations()
# Save drawing bounds to the .ifc file
@@ -2309,6 +2254,22 @@ class ActivateDrawingFromSheet(bpy.types.Operator, ActivateDrawingBase):
return True
# TODO: not exposed to the UI.
class SelectDocIfcFile(bpy.types.Operator, ImportHelper):
bl_idname = "bim.select_doc_ifc_file"
bl_label = "Select Documentation IFC File"
bl_description = "Selection .ifc file for documentation."
bl_options = {"REGISTER", "UNDO"}
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"})
filename_ext = ".ifc"
index: bpy.props.IntProperty()
def execute(self, context):
props = tool.Drawing.get_document_props()
props.ifc_files[self.index].name = self.filepath
return {"FINISHED"}
class RemoveDrawing(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_drawing"
bl_label = "Remove Drawing"
@@ -2320,6 +2281,7 @@ class RemoveDrawing(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
props = tool.Drawing.get_document_props()
if not tool.Drawing.get_active_drawing_item():
cls.poll_message_set("No drawing selected.")
return False
@@ -2465,9 +2427,7 @@ class SaveDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
style = {}
eval_namespace = {"context": context, "scene": scene, "space": space}
def add_prop_to_style(
prop_path: str, context: bpy.types.Context, scene: bpy.types.Scene, space: bpy.types.SpaceView3D
) -> None:
def add_prop_to_style(prop_path, context, scene, space):
value = eval(prop_path)
if not isinstance(value, str):
try:
@@ -2511,7 +2471,6 @@ class SaveDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
return {"FINISHED"}
# TODO: operator is not exposed to UI, move it to tool.
class SaveDrawingStylesData(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.save_drawing_styles_data"
bl_label = "Save Drawing Styles Data"
@@ -2585,6 +2544,7 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
self.drawing_style = drawing_style
self.set_raster_style(context)
self.set_query(context)
props = tool.Drawing.get_document_props()
assert (drawing := props.get_active_drawing())
@@ -2678,6 +2638,46 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
if "PYTEST_VERSION" in os.environ:
raise
def set_query(self, context: bpy.types.Context) -> None:
self.include_global_ids = []
self.exclude_global_ids = []
props = tool.Drawing.get_document_props()
for ifc_file in props.ifc_files:
try:
ifc = ifcopenshell.open(ifc_file.name)
except:
continue
if self.drawing_style.include_query:
results = ifcopenshell.util.selector.filter_elements(ifc, self.drawing_style.include_query)
self.include_global_ids.extend([e.GlobalId for e in results])
if self.drawing_style.exclude_query:
results = ifcopenshell.util.selector.filter_elements(ifc, self.drawing_style.exclude_query)
self.exclude_global_ids.extend([e.GlobalId for e in results])
if self.drawing_style.include_query:
self.parse_filter_query("INCLUDE", context)
if self.drawing_style.exclude_query:
self.parse_filter_query("EXCLUDE", context)
def parse_filter_query(self, mode: Literal["INCLUDE", "EXCLUDE"], context: bpy.types.Context) -> None:
if mode == "INCLUDE":
assert context.scene
objects = context.scene.objects
elif mode == "EXCLUDE":
objects = context.visible_objects
for obj in objects:
if mode == "INCLUDE":
obj.hide_viewport = False # Note: this breaks alt-H
global_id = obj.BIMObjectProperties.attributes.get("GlobalId")
if not global_id:
continue
global_id = global_id.string_value
if mode == "INCLUDE":
if global_id not in self.include_global_ids:
obj.hide_viewport = True # Note: this breaks alt-H
elif mode == "EXCLUDE":
if global_id in self.exclude_global_ids:
obj.hide_viewport = True # Note: this breaks alt-H
class RemoveSheet(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_sheet"
@@ -2750,14 +2750,13 @@ class AddScheduleToSheet(bpy.types.Operator, tool.Ifc.Operator):
if not props.schedules:
cls.poll_message_set("No schedule selected.")
return False
prefs = tool.Blender.get_addon_preferences()
return props.schedules and props.sheets and prefs.data_dir
bim_props = tool.Blender.get_bim_props()
return props.schedules and props.sheets and bim_props.data_dir
def _execute(self, context):
props = tool.Drawing.get_document_props()
active_schedule = props.schedules[props.active_schedule_index]
active_sheet = tool.Drawing.get_active_sheet()
ifc_file = tool.Ifc.get()
active_sheet = tool.Drawing.get_active_sheet(context)
schedule = tool.Ifc.get().by_id(active_schedule.ifc_definition_id)
if tool.Ifc.get_schema() == "IFC2X3":
schedule_location = tool.Drawing.get_path_with_ext(schedule.DocumentReferences[0].Location, "svg")
@@ -2782,7 +2781,7 @@ class AddScheduleToSheet(bpy.types.Operator, tool.Ifc.Operator):
self.report({"ERROR"}, "The schedule must be generated before adding to a sheet.")
return
reference = ifcopenshell.api.document.add_reference(ifc_file, information=sheet)
reference = tool.Ifc.run("document.add_reference", information=sheet)
attributes = tool.Drawing.generate_reference_attributes(
reference,
Identification=str(
@@ -2798,7 +2797,7 @@ class AddScheduleToSheet(bpy.types.Operator, tool.Ifc.Operator):
Location=schedule_location,
Description="SCHEDULE",
)
ifcopenshell.api.document.edit_reference(ifc_file, reference=reference, attributes=attributes)
tool.Ifc.run("document.edit_reference", reference=reference, attributes=attributes)
sheet_builder = sheeter.SheetBuilder()
sheet_builder.add_document(reference, schedule, sheet)
@@ -2824,8 +2823,7 @@ class AddReferenceToSheet(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
props = tool.Drawing.get_document_props()
active_reference = props.references[props.active_reference_index]
active_sheet = tool.Drawing.get_active_sheet()
ifc_file = tool.Ifc.get()
active_sheet = tool.Drawing.get_active_sheet(context)
extref = tool.Ifc.get().by_id(active_reference.ifc_definition_id)
if tool.Ifc.get_schema() == "IFC2X3":
extref_location = tool.Drawing.get_path_with_ext(extref.DocumentReferences[0].Location, "svg")
@@ -2850,7 +2848,7 @@ class AddReferenceToSheet(bpy.types.Operator, tool.Ifc.Operator):
self.report({"ERROR"}, f"Cannot find reference svg by path {extref_location}.")
return
reference = ifcopenshell.api.document.add_reference(ifc_file, information=sheet)
reference = tool.Ifc.run("document.add_reference", information=sheet)
attributes = tool.Drawing.generate_reference_attributes(
reference,
Identification=str(
@@ -2866,7 +2864,7 @@ class AddReferenceToSheet(bpy.types.Operator, tool.Ifc.Operator):
Location=extref_location,
Description="REFERENCE",
)
ifcopenshell.api.document.edit_reference(ifc_file, reference=reference, attributes=attributes)
tool.Ifc.run("document.edit_reference", reference=reference, attributes=attributes)
sheet_builder = sheeter.SheetBuilder()
sheet_builder.add_document(reference, extref, sheet)
@@ -2939,7 +2937,6 @@ class EditTextPopup(bpy.types.Operator):
# need to keep them in sync or move to some common function
# NOTE: that `popup_active_attribute` is used here when it's not used in `BIM_PT_text.draw()`
assert self.layout
obj = context.active_object
assert obj
props = tool.Drawing.get_text_props(obj)
@@ -3054,7 +3051,7 @@ class AddTextLiteral(bpy.types.Operator):
"Path": "RIGHT",
"BoxAlignment": "bottom_left",
}
# emulates `bonsai.bim.helper.import_attributes(ifc_literal, literal_props.attributes)`
# emulates `bonsai.bim.helper.import_attributes2(ifc_literal, literal_props.attributes)`
for attr_name in literal_attr_values:
attr = literal_attributes.add()
attr.name = attr_name
@@ -3207,25 +3204,22 @@ class LoadSheets(bpy.types.Operator, tool.Ifc.Operator):
core.load_sheets(tool.Drawing)
props = tool.Drawing.get_document_props()
warnings: list[tool.Drawing.SheetWarningType] = []
sheets_not_found = []
for sheet_prop in props.sheets:
if not sheet_prop.is_sheet:
continue
sheet = tool.Ifc.get().by_id(sheet_prop.ifc_definition_id)
document_uri = tool.Drawing.get_document_uri(sheet)
assert document_uri is not None
filepath = Path(document_uri)
if not filepath.is_file():
res = core.regenerate_sheet(tool.Drawing, sheet)
if res:
warnings.extend(res)
sheet_name = f"{sheet_prop.identification} - {sheet_prop.name}"
sheets_not_found.append(f'"{sheet_name}" - {document_uri}')
core.regenerate_sheet(tool.Drawing, sheet)
if warnings:
self.report({"WARNING"}, f"There were warnings loading sheets. See system console for the details.")
print("-" * 10)
print("\n".join(str(w) for w in warnings))
if sheets_not_found:
self.report({"ERROR"}, "Some sheets svg files are missing:\n" + "\n".join(sheets_not_found))
class EditSheet(bpy.types.Operator, tool.Ifc.Operator):
@@ -3240,9 +3234,8 @@ class EditSheet(bpy.types.Operator, tool.Ifc.Operator):
def invoke(self, context, event):
assert context.window_manager
sheet_item = tool.Drawing.get_active_sheet_item()
assert sheet_item
sheet = tool.Ifc.get().by_id(sheet_item.ifc_definition_id)
self.props = tool.Drawing.get_document_props()
sheet = tool.Ifc.get().by_id(self.props.sheets[self.props.active_sheet_index].ifc_definition_id)
if sheet.is_a("IfcDocumentInformation"):
self.document_type = "SHEET"
self.name = sheet.Name
@@ -3255,7 +3248,6 @@ class EditSheet(bpy.types.Operator, tool.Ifc.Operator):
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
assert self.layout
props = tool.Drawing.get_document_props()
if self.document_type == "SHEET":
row = self.layout.row()
@@ -3270,22 +3262,18 @@ class EditSheet(bpy.types.Operator, tool.Ifc.Operator):
row.prop(self, "identification", text="Identification")
def _execute(self, context):
props = tool.Drawing.get_document_props()
ifc_file = tool.Ifc.get()
sheet_item = tool.Drawing.get_active_sheet_item()
assert sheet_item
sheet = tool.Ifc.get().by_id(sheet_item.ifc_definition_id)
self.props = tool.Drawing.get_document_props()
sheet = tool.Ifc.get().by_id(self.props.sheets[self.props.active_sheet_index].ifc_definition_id)
if self.document_type == "SHEET":
core.rename_sheet(tool.Ifc, tool.Drawing, sheet=sheet, identification=self.identification, name=self.name)
elif self.document_type == "EMBEDDED":
core.rename_reference(tool.Ifc, tool.Drawing, reference=sheet, identification=self.identification)
elif self.document_type == "TITLEBLOCK":
titleblock = props.titleblock
titleblock = self.props.titleblock
reference = sheet
sheet = tool.Drawing.get_reference_document(reference)
assert sheet
ifcopenshell.api.document.edit_reference(
ifc_file,
tool.Ifc.run(
"document.edit_reference",
reference=reference,
attributes={"Location": tool.Drawing.get_default_titleblock_path(titleblock)},
)
@@ -3470,16 +3458,12 @@ class SelectAssignedProduct(bpy.types.Operator, tool.Ifc.Operator):
class EnableEditingElementFilter(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_element_filter"
bl_label = "Element Filter Mode"
bl_label = "Enable Editing Element Filter"
bl_description = "Enable editing options for the include or exclude filter"
bl_options = {"REGISTER", "UNDO"}
filter_mode: bpy.props.StringProperty()
@classmethod
def description(cls, context, properties):
if properties.filter_mode == "NONE":
return "Cancel filter"
return "Enable editing options for the include or exclude filter"
def _execute(self, context):
assert context.scene
obj = context.scene.camera
@@ -3501,7 +3485,6 @@ class EnableEditingElementFilter(bpy.types.Operator, tool.Ifc.Operator):
class EditElementFilter(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_element_filter"
bl_label = "Edit Element Filter"
bl_description = "Saves the filter"
bl_options = {"REGISTER", "UNDO"}
filter_mode: bpy.props.StringProperty()
@@ -3626,8 +3609,8 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
texture = ifc_file.create_entity("IfcImageTexture", Mode="DIFFUSE", URLReference=image_filepath.as_posix())
textures = [texture]
ifc_file.create_entity("IfcTextureCoordinateGenerator", Maps=textures, Mode="COORD") # UV map
ifcopenshell.api.style.add_surface_style(
ifc_file,
tool.Ifc.run(
"style.add_surface_style",
style=style,
ifc_class="IfcSurfaceStyleWithTextures",
attributes={"Textures": textures},
+61 -10
View File
@@ -46,8 +46,7 @@ from bpy.props import (
CollectionProperty,
BoolVectorProperty,
)
from typing import TYPE_CHECKING, Literal, Any, get_args, Union
from collections.abc import Callable
from typing import TYPE_CHECKING, Literal, Any, Callable, get_args, Union
diagram_scales_enum = []
@@ -119,8 +118,8 @@ def update_is_nts(self: "BIMCameraProperties", context: bpy.types.Context) -> No
if pset:
pset = tool.Ifc.get().by_id(pset["id"])
else:
pset = ifcopenshell.api.pset.add_pset(tool.Ifc.get(), product=element, name="EPset_Drawing")
ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties={"IsNTS": self.is_nts})
pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=element, name="EPset_Drawing")
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"IsNTS": self.is_nts})
def get_diagram_scales(self: "BIMCameraProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
@@ -250,8 +249,8 @@ def update_layer(self: "BIMCameraProperties", context: bpy.types.Context, name:
if pset:
pset = tool.Ifc.get().by_id(pset["id"])
else:
pset = ifcopenshell.api.pset.add_pset(tool.Ifc.get(), product=element, name="EPset_Drawing")
ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties={name: value})
pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=element, name="EPset_Drawing")
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={name: value})
def get_titleblocks(self, context):
@@ -368,20 +367,20 @@ class DrawingStyle(PropertyGroup):
name="Render Type",
default="VIEWPORT",
)
include_query: StringProperty(name="Include Query")
exclude_query: StringProperty(name="Exclude Query")
if TYPE_CHECKING:
name: str
raster_style: str
render_type: RenderType
include_query: str
exclude_query: str
class RasterStyleProperty(enum.Enum):
# EVAL_PROP_ props will be evaluated explicitly
EVAL_PROP_WORLD_COLOR = "bpy.data.worlds[0].color"
EVAL_PROP_EEVEE_USE_SHADOWS = "scene.eevee.use_shadows"
EVAL_PROP_EEVEE_SHADOW_RAY_COUNT = "scene.eevee.shadow_ray_count"
EVAL_PROP_EEVEE_SHADOW_STEP_COUNT = "scene.eevee.shadow_step_count"
EVAL_PROP_EEVEE_SHADOW_RES_SCALE = "scene.eevee.shadow_resolution_scale"
# those props attributes used as a source for shading style properties
RENDER = "scene.render"
@@ -421,8 +420,42 @@ class DocProperties(PropertyGroup):
is_editing_sheets: BoolProperty(name="Is Editing Sheets", default=False)
sheets: CollectionProperty(name="Sheets", type=Sheet)
active_sheet_index: IntProperty(name="Active Sheet Index")
ifc_files: CollectionProperty(name="IFCs", type=StrProperty)
drawing_styles: CollectionProperty(name="Drawing Styles", type=DrawingStyle)
should_draw_decorations: BoolProperty(name="Should Draw Decorations", update=update_should_draw_decorations)
sheets_dir: StringProperty(default=os.path.join("sheets") + os.path.sep, name="Default Sheets Directory")
layouts_dir: StringProperty(default=os.path.join("layouts") + os.path.sep, name="Default Layouts Directory")
titleblocks_dir: StringProperty(
default=os.path.join("layouts", "titleblocks") + os.path.sep, name="Default Titleblocks Directory"
)
drawings_dir: StringProperty(default=os.path.join("drawings") + os.path.sep, name="Default Drawings Directory")
stylesheet_path: StringProperty(
default=os.path.join("drawings", "assets", "default.css"), name="Default Stylesheet"
)
schedules_stylesheet_path: StringProperty(
default=os.path.join("drawings", "assets", "schedule.css"), name="Default Stylesheet for Schedules"
)
markers_path: StringProperty(default=os.path.join("drawings", "assets", "markers.svg"), name="Default Markers")
symbols_path: StringProperty(default=os.path.join("drawings", "assets", "symbols.svg"), name="Default Symbols")
patterns_path: StringProperty(default=os.path.join("drawings", "assets", "patterns.svg"), name="Default Patterns")
shadingstyles_path: StringProperty(
default=os.path.join("drawings", "assets", "shading_styles.json"), name="Default Shading Styles"
)
shadingstyle_default: StringProperty(default="Blender Default", name="Default Shading Style")
drawing_font: StringProperty(default="OpenGost Type B TT.ttf", name="Drawing Font")
magic_font_scale: bpy.props.FloatProperty(default=0.004118616, name="Font Scale Factor")
imperial_precision: StringProperty(default="1/32", name="Imperial Precision")
tolerance: bpy.props.FloatProperty(default=0.00001, name="A tolerance used when selecting objects")
classes_to_wireframe: StringProperty(
default="IfcVirtualElement",
name="Classes to Wireframe",
description="Upon import, these classes will display as wireframe.\nEx: IfcVirtualelement, IfcSpace",
)
classes_no_cut: StringProperty(
default="IfcVirtualElement, IfcSpace",
name="Classes that are not cut",
description="The cut decoractor will be turned off for these classes\nEx: IfcVirtualelement, IfcSpace",
)
if TYPE_CHECKING:
should_use_underlay_cache: bool
@@ -445,8 +478,26 @@ class DocProperties(PropertyGroup):
is_editing_sheets: bool
sheets: bpy.types.bpy_prop_collection_idprop[Sheet]
active_sheet_index: int
ifc_files: bpy.types.bpy_prop_collection_idprop[StrProperty]
drawing_styles: bpy.types.bpy_prop_collection_idprop[DrawingStyle]
should_draw_decorations: bool
sheets_dir: str
layouts_dir: str
titleblocks_dir: str
drawings_dir: str
stylesheet_path: str
schedules_stylesheet_path: str
markers_path: str
symbols_path: str
patterns_path: str
shadingstyles_path: str
shadingstyle_default: str
drawing_font: str
magic_font_scale: float
imperial_precision: str
tolerance: float
classes_to_wireframe: str
classes_no_cut: str
def get_active_drawing(self) -> Union[ifcopenshell.entity_instance, None]:
drawing_id = self.active_drawing_id
@@ -22,8 +22,6 @@ import bpy
import string
import svgwrite
import openpyxl
import openpyxl.cell # Unnecessary, bug in typeshed.
import openpyxl.utils # Unnecessary, bug in typeshed.
import bonsai.tool as tool
from bonsai.bim.module.drawing.svgwriter import SvgWriter
@@ -74,11 +72,10 @@ class Scheduler:
self.schedule_xlsx(infile, outfile)
def parse_css(self, infile: str) -> None:
prefs = tool.Blender.get_addon_preferences()
props = tool.Drawing.get_document_props()
stylesheet_path = os.path.splitext(infile)[0] + ".css"
if not os.path.exists(stylesheet_path):
stylesheet_rel_path = prefs.doc.schedules_stylesheet_path
stylesheet_rel_path = props.schedules_stylesheet_path
ifc_file_path = os.path.dirname(tool.Ifc.get_path())
stylesheet_path = ifc_file_path + "\\" + stylesheet_rel_path
if not os.path.exists(stylesheet_path):
+46 -52
View File
@@ -61,12 +61,12 @@ class SheetBuilder:
shutil.copy(ootb_titleblock_path, titleblock_path)
view_root = ET.parse(titleblock_path).getroot()
view_width = self.convert_to_mm(view_root.attrib["width"])
view_height = self.convert_to_mm(view_root.attrib["height"])
view_width = self.convert_to_mm(view_root.attrib.get("width"))
view_height = self.convert_to_mm(view_root.attrib.get("height"))
view = ET.SubElement(root, "g")
view.attrib["data-type"] = "titleblock"
titleblock = ET.SubElement(view, "image")
titleblock.attrib["xlink:href"] = Path(os.path.relpath(titleblock_path, sheet_dir)).as_posix()
titleblock.attrib["xlink:href"] = os.path.relpath(titleblock_path, sheet_dir)
titleblock.attrib["x"] = "0"
titleblock.attrib["y"] = "0"
titleblock.attrib["width"] = str(view_width)
@@ -87,7 +87,6 @@ class SheetBuilder:
) -> None:
filename = drawing.Name
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
assert layout_path
layout_dir = os.path.dirname(layout_path)
drawing_path = tool.Drawing.get_document_uri(tool.Drawing.get_drawing_reference(drawing))
@@ -111,8 +110,8 @@ class SheetBuilder:
view.attrib["data-type"] = "drawing"
view.attrib["data-id"] = str(reference.id())
view.attrib["data-drawing"] = drawing.GlobalId
view_width = self.convert_to_mm(view_root.attrib["width"])
view_height = self.convert_to_mm(view_root.attrib["height"])
view_width = self.convert_to_mm(view_root.attrib.get("width"))
view_height = self.convert_to_mm(view_root.attrib.get("height"))
x, y = self.next_drawing_location(layout_root, view_width)
@@ -135,16 +134,16 @@ class SheetBuilder:
# how wide is the title block frame
try:
titleblock_width = self.convert_to_mm(titleblocks[0][0].attrib["width"])
titleblock_width = self.convert_to_mm(titleblocks[0][0].attrib.get("width"))
except (IndexError, AttributeError):
titleblock_width = 840.0
# where does the last drawing finish
try:
last = drawings[-1][0]
last_width = self.convert_to_mm(last.attrib["width"])
last_x = self.convert_to_mm(last.attrib["x"])
last_y = self.convert_to_mm(last.attrib["y"])
last_width = self.convert_to_mm(last.attrib.get("width"))
last_x = self.convert_to_mm(last.attrib.get("x"))
last_y = self.convert_to_mm(last.attrib.get("y"))
except (IndexError, AttributeError):
return [DEFAULT_POSITION.x, DEFAULT_POSITION.y]
@@ -156,8 +155,8 @@ class SheetBuilder:
for drawing in drawings:
for image in drawing:
try:
image_y = self.convert_to_mm(image.attrib["y"])
image_height = self.convert_to_mm(image.attrib["height"])
image_y = self.convert_to_mm(image.attrib.get("y"))
image_height = self.convert_to_mm(image.attrib.get("height"))
except AttributeError:
return [DEFAULT_POSITION.x, DEFAULT_POSITION.y]
if image_y + image_height + DRAWING_PADDING > last_y:
@@ -168,7 +167,6 @@ class SheetBuilder:
ET.register_namespace("", "http://www.w3.org/2000/svg")
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
assert layout_path
layout_tree = ET.parse(layout_path)
layout_root = layout_tree.getroot()
ifc_file = tool.Ifc.get()
@@ -212,13 +210,12 @@ class SheetBuilder:
ET.register_namespace("", "http://www.w3.org/2000/svg")
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
assert layout_path
if not os.path.exists(layout_path):
return
layout_tree = ET.parse(layout_path)
layout_root = layout_tree.getroot()
for g in layout_root.findall(f"{SVG}g"):
for g in layout_root.findall("{http://www.w3.org/2000/svg}g"):
if g.attrib.get("data-id") == str(reference.id()):
layout_root.remove(g)
break
@@ -236,7 +233,6 @@ class SheetBuilder:
tool.Drawing.create_svg_document(document)
document_name = os.path.splitext(os.path.basename(view_path))[0]
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
assert layout_path
layout_dir = os.path.dirname(layout_path)
ET.register_namespace("", "http://www.w3.org/2000/svg")
@@ -282,12 +278,13 @@ class SheetBuilder:
title.attrib["xlink:href"] = os.path.relpath(title_path, layout_dir)
title.attrib["x"] = str(x)
title.attrib["y"] = str(y)
title.attrib["width"] = str(self.convert_to_mm(title_root.attrib["width"]))
title.attrib["height"] = str(self.convert_to_mm(title_root.attrib["height"]))
title.attrib["width"] = str(self.convert_to_mm(title_root.attrib.get("width")))
title.attrib["height"] = str(self.convert_to_mm(title_root.attrib.get("height")))
def build(self, sheet: ifcopenshell.entity_instance) -> dict:
self.references = {"SHEET": None, "RASTER": []}
def build(self, sheet: ifcopenshell.entity_instance) -> dict[str, str]:
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
assert layout_path
self.layout_dir = os.path.dirname(layout_path)
sheet_path = tool.Ifc.resolve_uri(tool.Drawing.get_default_sheet_path(sheet[0], sheet.Name))
@@ -311,17 +308,19 @@ class SheetBuilder:
with open(sheet_path, "wb") as output:
tree.write(output)
return {"SHEET": sheet_path}
self.references["SHEET"] = sheet_path
return self.references
def build_titleblock(self, root: ET.Element, sheet: ifcopenshell.entity_instance) -> None:
titleblock = root.findall(f'{SVG}g[@data-type="titleblock"]')[0]
image = titleblock.findall(f"{SVG}image")[0]
titleblock = root.findall('{http://www.w3.org/2000/svg}g[@data-type="titleblock"]')[0]
image = titleblock.findall("{http://www.w3.org/2000/svg}image")[0]
g = self.parse_embedded_svg(image, sheet.get_info())
grid_north = ifcopenshell.util.geolocation.get_grid_north(tool.Ifc.get()) * -1
true_north = ifcopenshell.util.geolocation.get_true_north(tool.Ifc.get()) * -1
for north in g.iterfind(f'.//{SVG}g[@data-type="grid-north"]'):
for north in g.iterfind('.//{http://www.w3.org/2000/svg}g[@data-type="grid-north"]'):
north.attrib["transform"] = f"rotate({grid_north})"
for north in g.iterfind(f'.//{SVG}g[@data-type="true-north"]'):
for north in g.iterfind('.//{http://www.w3.org/2000/svg}g[@data-type="true-north"]'):
north.attrib["transform"] = f"rotate({true_north})"
titleblock.append(g)
titleblock.remove(image)
@@ -334,9 +333,7 @@ class SheetBuilder:
# add .prefix class to all css selectors
style = svg.find(f"{SVG}defs/{SVG}style")
assert style is not None
style_data = style.text
assert style_data is not None
text = ""
brackets_level = 0
for l in style_data:
@@ -377,8 +374,6 @@ class SheetBuilder:
if "filter" in attrib:
# example use "#fill-background" filter
attrib["filter"] = replace_urls(attrib["filter"])
if "style" in attrib:
attrib["style"] = replace_urls(attrib["style"])
if svg_element.tag == f"{SVG}use":
href_attrib = f"{XLINK}href"
if href_attrib in attrib:
@@ -389,16 +384,16 @@ class SheetBuilder:
return svg
def build_drawings(self, root: ET.Element, sheet: ifcopenshell.entity_instance):
for view in root.findall(f'{SVG}g[@data-type="drawing"]'):
for view in root.findall('{http://www.w3.org/2000/svg}g[@data-type="drawing"]'):
drawing_id = int(view.attrib["data-id"])
try:
reference = tool.Ifc.get().by_id(int(view.attrib["data-id"]))
drawing = tool.Ifc.get().by_guid(view.attrib["data-drawing"])
except RuntimeError:
drawing = tool.Ifc.get().by_id(view.attrib["data-drawing"])
except:
# Perhaps the SVG has outdated content or is edited externally which we cannot control.
continue
images = view.findall(f"{SVG}image")
images = view.findall("{http://www.w3.org/2000/svg}image")
foreground = None
view_title = None
@@ -415,7 +410,6 @@ class SheetBuilder:
view.append(svg)
if view_title is not None:
assert foreground is not None
foreground_path = self.get_href(foreground)
data = reference.get_info()
data.update({"Sheet" + k: v for k, v in sheet.get_info().items()})
@@ -440,8 +434,8 @@ class SheetBuilder:
view.remove(image)
def build_documents(self, root: ET.Element, sheet: ifcopenshell.entity_instance) -> None:
schedules = root.findall(f'{SVG}g[@data-type="schedule"]')
references = root.findall(f'{SVG}g[@data-type="reference"]')
schedules = root.findall('{http://www.w3.org/2000/svg}g[@data-type="schedule"]')
references = root.findall('{http://www.w3.org/2000/svg}g[@data-type="reference"]')
documents = schedules + references
for view in documents:
try:
@@ -451,7 +445,7 @@ class SheetBuilder:
# Perhaps the SVG has outdated content or is edited externally which we cannot control.
continue
images = view.findall(f"{SVG}image")
images = view.findall("{http://www.w3.org/2000/svg}image")
table = None
view_title = None
@@ -477,12 +471,13 @@ class SheetBuilder:
view.remove(image)
def get_href(self, element: ET.Element) -> str:
return urllib.parse.unquote(element.attrib[f"{XLINK}href"]).replace("\\", "/")
return urllib.parse.unquote(element.attrib.get("{http://www.w3.org/1999/xlink}href")).replace("\\", "/")
def parse_embedded_svg(self, image: ET.Element, data: dict) -> ET.Element:
group = ET.Element("g")
x, y = self.convert_to_mm(image.attrib["x"]), self.convert_to_mm(image.attrib["y"])
group.attrib["transform"] = f"translate({x},{y})"
group.attrib["transform"] = "translate({},{})".format(
self.convert_to_mm(image.attrib.get("x")), self.convert_to_mm(image.attrib.get("y"))
)
# Convert viewBox into a clip path
clip_id = str(uuid.uuid4())
@@ -492,8 +487,8 @@ class SheetBuilder:
rect = ET.Element("rect")
rect.attrib["x"] = "0"
rect.attrib["y"] = "0"
rect.attrib["width"] = str(self.convert_to_mm(image.attrib["width"]))
rect.attrib["height"] = str(self.convert_to_mm(image.attrib["height"]))
rect.attrib["width"] = str(self.convert_to_mm(image.attrib.get("width")))
rect.attrib["height"] = str(self.convert_to_mm(image.attrib.get("height")))
clip_path.append(rect)
self.defs.append(clip_path)
@@ -504,16 +499,16 @@ class SheetBuilder:
embedded.attrib["viewBox"] = ""
# TODO: This should not be in this function
self.scale = embedded.attrib.get("data-scale")
images = embedded.findall(f"{SVG}image")
images = embedded.findall("{http://www.w3.org/2000/svg}image")
for image in images:
old_href = Path(image.attrib[f"{XLINK}href"])
old_href = Path(image.attrib.get("{http://www.w3.org/1999/xlink}href"))
if not os.path.isabs(old_href):
template_dir = Path(os.path.join(self.layout_dir, svg_path)).resolve().parent
old_href = Path(os.path.join(template_dir, old_href))
old_href = old_href.absolute().resolve().as_posix()
new_href = Path(os.path.join(self.sheets_dir, Path(old_href).name)).absolute().resolve().as_posix()
shutil.copy(old_href, new_href)
image.attrib[f"{XLINK}href"] = Path(old_href).name
image.attrib["{http://www.w3.org/1999/xlink}href"] = Path(old_href).name
for child in embedded:
if "namedview" in child.tag:
continue
@@ -526,7 +521,6 @@ class SheetBuilder:
)
titleblock_path = tool.Ifc.resolve_uri(tool.Drawing.get_default_titleblock_path(titleblock_name))
sheet_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
assert sheet_path is not None
sheet_dir = os.path.dirname(sheet_path)
os.makedirs(sheet_dir, exist_ok=True)
@@ -537,16 +531,16 @@ class SheetBuilder:
ET.register_namespace("", "http://www.w3.org/2000/svg")
ET.register_namespace("xlink", "http://www.w3.org/1999/xlink")
view_root = ET.parse(titleblock_path).getroot()
view_width = self.convert_to_mm(view_root.attrib["width"])
view_height = self.convert_to_mm(view_root.attrib["height"])
view_root = ET.parse(ootb_titleblock_path).getroot()
view_width = self.convert_to_mm(view_root.attrib.get("width"))
view_height = self.convert_to_mm(view_root.attrib.get("height"))
sheet_tree = ET.parse(sheet_path)
root = sheet_tree.getroot()
titleblock = sheet_tree.findall(f'{SVG}g[@data-type="titleblock"]')[0]
image = titleblock.findall(f"{SVG}image[@{XLINK}href]")[0]
image.attrib[f"{XLINK}href"] = os.path.relpath(titleblock_path, sheet_dir)
titleblock = sheet_tree.findall('{http://www.w3.org/2000/svg}g[@data-type="titleblock"]')[0]
image = titleblock.findall("{http://www.w3.org/2000/svg}image[@{http://www.w3.org/1999/xlink}href]")[0]
image.attrib["{http://www.w3.org/1999/xlink}href"] = os.path.relpath(titleblock_path, sheet_dir)
image.attrib["width"] = str(view_width)
image.attrib["height"] = str(view_height)
@@ -26,7 +26,6 @@ from bpy.types import SplineBezierPoints, SplinePoints
import mathutils
import xml.etree.ElementTree as ET
import svgwrite
import svgwrite.container
import svgwrite.text
import ifcopenshell
import ifcopenshell.util.element
@@ -39,8 +38,7 @@ from bonsai.bim.module.drawing.data import DrawingsData
from bonsai.bim.module.drawing.data import DecoratorData
from math import pi, ceil, atan, degrees, acos
from mathutils import geometry, Vector
from typing import Optional, Self, Union
from collections.abc import Callable, Sequence
from typing import Optional, Self, Union, Sequence, Callable
from pathlib import Path
@@ -55,7 +53,7 @@ class External(svgwrite.container.Group):
if elem.tag.startswith(ns):
elem.tag = elem.tag[nsl:]
super().__init__(**extra)
super(External, self).__init__(**extra)
def get_xml(self):
return self.xml
+16 -21
View File
@@ -91,24 +91,6 @@ class BIM_PT_camera(Panel):
row = self.layout.row()
row.prop(props, "height")
render = context.scene.render
MEGAPIXELS_WARNING_THRESHOLD = 50
# See #6686.
if (
props.has_underlay
and str(render.engine) == "BLENDER_EEVEE_NEXT"
and ((megapixels := (render.resolution_x * render.resolution_y / 10**6)) > MEGAPIXELS_WARNING_THRESHOLD)
):
box = self.layout.box()
box.label(
text=f"Resulting image size is {render.resolution_x} x {render.resolution_y} ({round(megapixels, 2)} MP).",
icon="ERROR",
)
box.label(
text=(f"Which is more than {MEGAPIXELS_WARNING_THRESHOLD} megapixels and will require a lot of VRAM.")
)
box.label(text="Underlay render might crash if VRAM requirement is not met.")
row = self.layout.row()
row.prop(camera_data, "clip_end", text="Depth")
@@ -234,6 +216,10 @@ class BIM_PT_drawing_underlay(Panel):
row = layout.row()
row.prop(drawing_style, "render_type")
row = layout.row(align=True)
row.prop(drawing_style, "include_query")
row = layout.row(align=True)
row.prop(drawing_style, "exclude_query")
row = layout.row(align=True)
row.operator("bim.save_drawing_style")
@@ -299,6 +285,16 @@ class BIM_PT_drawings(Panel):
"BIM_UL_drawinglist", "", self.props, "drawings", self.props, "active_drawing_index"
)
# Commented out until federated drawing generation is rebuilt
# row = self.layout.row()
# row.operator("bim.add_ifc_file")
# for index, ifc_file in enumerate(self.props.ifc_files):
# row = self.layout.row(align=True)
# row.prop(ifc_file, "name", text="IFC #{}".format(index + 1))
# row.operator("bim.select_doc_ifc_file", icon="FILE_FOLDER", text="").index = index
# row.operator("bim.remove_ifc_file", icon="X", text="").index = index
class BIM_PT_schedules(Panel):
bl_label = "Schedules"
@@ -419,7 +415,6 @@ class BIM_PT_sheets(Panel):
draw_project_not_saved_ui(self)
return
assert self.layout
self.props = tool.Drawing.get_document_props()
if not self.props.is_editing_sheets:
@@ -433,8 +428,8 @@ class BIM_PT_sheets(Panel):
row.operator("bim.add_sheet", text="", icon="ADD")
row.operator("bim.disable_editing_sheets", text="", icon="CANCEL")
active_sheet = tool.Drawing.get_active_sheet_item()
if active_sheet:
if self.props.sheets and self.props.active_sheet_index < len(self.props.sheets):
active_sheet = self.props.sheets[self.props.active_sheet_index]
row = self.layout.row(align=True)
row2 = row.row(align=True)
if active_sheet.is_sheet:

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