mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-28 07:49:59 +00:00
Compare commits
293 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 290849d43a | |||
| 5e930972d6 | |||
| c08d25de42 | |||
| 5649708ca4 | |||
| 9e81a3da17 | |||
| b509374c27 | |||
| 704bb27cf5 | |||
| 7b62b035c9 | |||
| c5fe80db7b | |||
| 3ffc0bc39c | |||
| 0a266bba5d | |||
| ecf0d255f7 | |||
| c0499a2c88 | |||
| 3ad247b9a7 | |||
| 13b93a9ea8 | |||
| ebdfe2bccd | |||
| 942c3fec87 | |||
| 651c24bbfe | |||
| b2213592ea | |||
| c311b65af5 | |||
| e8b097e2cb | |||
| 193ff1cd0c | |||
| a65aa92c87 | |||
| 6af19bc98d | |||
| 5769c0d442 | |||
| 5c058dff3e | |||
| f86e414055 | |||
| c17febac0c | |||
| 2d811a6786 | |||
| 6eed5607c4 | |||
| 0b46a31c25 | |||
| 4d7f078f8b | |||
| 046f7cbfd2 | |||
| 4152641d1f | |||
| ec55c8183f | |||
| b54741dd0b | |||
| b4190876b0 | |||
| 864eeb42e9 | |||
| 7fcb7c6433 | |||
| 015d8f6722 | |||
| c07c2d7692 | |||
| a57a0eb4eb | |||
| e40644c4f6 | |||
| 8c746b1a2c | |||
| 8e98aecc0b | |||
| 34a2d95524 | |||
| a12427e44a | |||
| 35734f4b67 | |||
| c54ea1e5d2 | |||
| abd05e939a | |||
| d02c5ca1e6 | |||
| a5afba03ab | |||
| 3f914cdf27 | |||
| 1d03e916f3 | |||
| fd279234f3 | |||
| a44439655c | |||
| c4f85dffa4 | |||
| 8d63845f3d | |||
| e0e24948be | |||
| 84420a8973 | |||
| 7ee055f4f3 | |||
| 1f436bcec6 | |||
| 468c1a2580 | |||
| e8cb9e7f3c | |||
| fa98a7e8f7 | |||
| 686a5e7387 | |||
| e1da08579c | |||
| 0a87e92dcf | |||
| 24b3cfc067 | |||
| c163b260cf | |||
| 9e50c89c01 | |||
| b28a3b6e32 | |||
| 9573628bf6 | |||
| 5f034bf4a9 | |||
| de6e54040a | |||
| 50b39fdd10 | |||
| eef04fb91c | |||
| a383f60915 | |||
| 44131a7fe5 | |||
| c66f2131e2 | |||
| 035999d0c3 | |||
| 61d68b74ca | |||
| 133459faec | |||
| 6fb0f5e63a | |||
| 2859e9632e | |||
| 521a8183e7 | |||
| a63f67ad32 | |||
| aeba2885e4 | |||
| f50ae2faa5 | |||
| bd739f8bfd | |||
| 5b2243089d | |||
| 2fe4680ca1 | |||
| 21624d6065 | |||
| b9f40342b2 | |||
| 3e879c6156 | |||
| c360b91f4a | |||
| 23b1a71cba | |||
| 5d17554d04 | |||
| 1f3fc3c2b9 | |||
| d6da8a7ee1 | |||
| cfb4cd8af9 | |||
| 7eff4db4eb | |||
| 689688c76b | |||
| 0ea513818b | |||
| 2fdcb41c4e | |||
| 65c99c57cd | |||
| fe54b24f5f | |||
| 7fdfdbf8b4 | |||
| 53906d824a | |||
| a687a90039 | |||
| c37fd6ae0b | |||
| 06e8e2b35c | |||
| fd51b9a9e9 | |||
| 3f2f296d5f | |||
| 020937835a | |||
| a52e3615d6 | |||
| adb61c5a16 | |||
| a50973ede7 | |||
| 74ca86678a | |||
| ac2e69bd66 | |||
| 595a3b3f87 | |||
| 48f534e7c1 | |||
| 1ed770d454 | |||
| ec600ec72e | |||
| adcc23541a | |||
| 9a77c94758 | |||
| e89d195f9b | |||
| 228e9d0571 | |||
| 7280669725 | |||
| 83b8bb153d | |||
| 88891409fd | |||
| b6ba1aaa8c | |||
| ffb3d2d6e9 | |||
| efede9eb03 | |||
| ac315e8a55 | |||
| b7c18221f4 | |||
| 7a49f1611e | |||
| 5c0a7af830 | |||
| a968b61d02 | |||
| 0ac9d739f8 | |||
| 74c6480808 | |||
| 0601802005 | |||
| 9784fb1c49 | |||
| 039dcbaefa | |||
| 1cd17fd5f8 | |||
| 2f0f9fe08b | |||
| 50ed75fcc2 | |||
| 9eaf41c9f0 | |||
| 581a674654 | |||
| e29f686d3f | |||
| 8cf7057abc | |||
| 316b9554d4 | |||
| 95fa6f39c4 | |||
| 65ffbe081b | |||
| 936984dd31 | |||
| 590c6eed0a | |||
| a696c7bbb5 | |||
| 6ff1d06938 | |||
| 6deb7121cc | |||
| 6aec2f7a02 | |||
| 0eb987fc07 | |||
| 04092d19be | |||
| 8364c87777 | |||
| 2d38d8520c | |||
| 03d3b1e52c | |||
| 79fe1bfcfc | |||
| 59c1d7fa54 | |||
| f0e1be765d | |||
| a2bfc006b7 | |||
| dff9f56ce3 | |||
| 80996dee1f | |||
| a329de9f71 | |||
| 1918138f68 | |||
| aa21fe9060 | |||
| 6eee445502 | |||
| 3eb29af496 | |||
| 2912580087 | |||
| 477725529b | |||
| d915dc9df0 | |||
| 53a2b87c97 | |||
| d10b327c5f | |||
| 293cd83a9c | |||
| 07817ec6fc | |||
| d1ad082979 | |||
| ba9693519b | |||
| 41362e1582 | |||
| 8e77011dda | |||
| 02632440b0 | |||
| 0a4ab32810 | |||
| c446aab9a0 | |||
| 2e7cbb759e | |||
| 68f175fc71 | |||
| a428169a13 | |||
| 7db0196791 | |||
| 4f4c557f61 | |||
| d88adf2d02 | |||
| 956b7dcb92 | |||
| 0a32149be3 | |||
| aafb6fc426 | |||
| cac3d7f699 | |||
| 83add56e6c | |||
| e4412e2f51 | |||
| 4a1422d292 | |||
| 1e448cb170 | |||
| 07bb7e68dd | |||
| cf054e11a1 | |||
| 6b8024f835 | |||
| 66577fd30d | |||
| 0124b7a0e9 | |||
| 737402cae4 | |||
| 8b1d4ca5d4 | |||
| a6e953a550 | |||
| 40dcfd450f | |||
| e220b6981e | |||
| 84931fbbd2 | |||
| 2d5bc49ee0 | |||
| d4ff789688 | |||
| 5c3f0f0df9 | |||
| 519e667464 | |||
| e3abfd60cb | |||
| 864e8d3a48 | |||
| d2b05ea3dc | |||
| d063bdbbf1 | |||
| a42a49e088 | |||
| ae22eec958 | |||
| b657eee564 | |||
| 9d1daa4dc5 | |||
| dbf33283ab | |||
| 0983f812fd | |||
| 3ced6e7bb1 | |||
| 933d845472 | |||
| 8313c05884 | |||
| f6e588b48c | |||
| 547c5fb8a9 | |||
| 570e29cfe4 | |||
| 2e35b07fa6 | |||
| 1b6ee26854 | |||
| 7b21858cd4 | |||
| 12800bbeee | |||
| 3a7488d54b | |||
| 7bfa926e6d | |||
| fd7a7d44c3 | |||
| fe6a6d1a0d | |||
| 98a9550260 | |||
| fe253af5ae | |||
| e933ff08f8 | |||
| 25af639dae | |||
| 5b3e3fc646 | |||
| 57dbea2974 | |||
| cc3074cdcc | |||
| 4c9697d260 | |||
| 8dfcea0e24 | |||
| f0fac7ad8b | |||
| f6c34ac196 | |||
| 5ebaaa6c8c | |||
| 9838a54173 | |||
| 2c3c0b9d95 | |||
| 548f6e0566 | |||
| 3874d19caa | |||
| 87fee0b850 | |||
| 3bccc993b7 | |||
| 31ae2c5437 | |||
| 85c48f6a63 | |||
| 833cc0604c | |||
| 3dd83598e9 | |||
| 3050abdb87 | |||
| f7c4b13d0a | |||
| 91d3b8161e | |||
| e9cc205ddd | |||
| c48f50a67e | |||
| 6d9ee8828e | |||
| 28c95a7d9a | |||
| 9a6ff7c37c | |||
| 226ea10de8 | |||
| 3c590f097a | |||
| cbdbd5bd83 | |||
| ccdf1cee0a | |||
| 21d3b1d472 | |||
| 0c7a0a6bda | |||
| 3dcded819f | |||
| 6f11f74aac | |||
| 0daf4c9324 | |||
| 7f838f81b4 | |||
| cf9446c4a5 | |||
| 26ba761c68 | |||
| bb9be861c8 | |||
| ad815b697e | |||
| ca90ddc0b4 | |||
| fb35e80be0 | |||
| 21a62116a8 | |||
| c2708b13d9 | |||
| b949efe8ba | |||
| 463289e462 |
@@ -25,7 +25,9 @@ jobs:
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
find .. -maxdepth 2
|
||||
VERSION=`cat IfcOpenShell/VERSION`
|
||||
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 && pip install ./pyodide-build' >> script.sh
|
||||
@@ -38,7 +40,7 @@ jobs:
|
||||
chmod +x script.sh
|
||||
sed -i s/--tty// pyodide/run_docker
|
||||
pyodide/run_docker ./script.sh
|
||||
mv dist/IfcOpenShell-0.8.0-py3-none-any.whl dist/ifcopenshell_python-v0.8.0+${GITHUB_SHA:0:7}-cp312-cp312-emscripten_3_1_58_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
|
||||
|
||||
@@ -17,6 +17,7 @@ jobs:
|
||||
sqlite-devel bzip2-devel zlib-devel openssl-devel xz-devel \
|
||||
readline-devel ncurses-devel libffi-devel libuuid-devel git-lfs \
|
||||
findutils
|
||||
git config --global --add safe.directory '*'
|
||||
|
||||
- name: Install aws cli
|
||||
run: |
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
name: Build and Deploy Unstable Documentation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main # Trigger the workflow on pushes to the main branch
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cd src/bonsai/docs # Navigate to the docs directory
|
||||
pip install -r requirements.txt # Install dependencies from requirements.txt
|
||||
|
||||
- name: Build documentation
|
||||
run: |
|
||||
cd src/bonsai/docs # Navigate to the docs directory
|
||||
make html # Build the documentation
|
||||
|
||||
- name: Deploy to GitHub Pages (Unstable)
|
||||
uses: peaceiris/actions-gh-pages@v4
|
||||
with:
|
||||
deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }} # SSH key for deployment
|
||||
external_repository: IfcOpenShell/bonsaibim_org_docs_unstable # Target repository
|
||||
publish_branch: main # Branch to deploy to
|
||||
cname: docs-unstable.bonsaibim.org # Custom domain for unstable docs
|
||||
publish_dir: src/bonsai/docs/_build/html # Directory containing built docs
|
||||
@@ -0,0 +1,36 @@
|
||||
name: Build and Deploy Stable Documentation
|
||||
|
||||
on:
|
||||
workflow_dispatch: # Manual trigger
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cd src/bonsai/docs
|
||||
pip install -r requirements.txt # Run pip install from the docs directory
|
||||
|
||||
- name: Build documentation
|
||||
run: |
|
||||
cd src/bonsai/docs
|
||||
make html
|
||||
|
||||
- name: Deploy to GitHub Pages (Stable)
|
||||
uses: peaceiris/actions-gh-pages@v4
|
||||
with:
|
||||
deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }}
|
||||
external_repository: IfcOpenShell/bonsaibim_org_docs
|
||||
publish_branch: main
|
||||
cname: docs.bonsaibim.org
|
||||
publish_dir: src/bonsai/docs/_build/html
|
||||
@@ -0,0 +1,48 @@
|
||||
name: Deploy Pyodide Demo App to GitHub Pages
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
pages: write
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'src/pyodide/**'
|
||||
- '.github/workflows/publish-pyodide-demo-app.yml'
|
||||
|
||||
jobs:
|
||||
activate:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.repository == 'IfcOpenShell/IfcOpenShell'
|
||||
steps:
|
||||
- name: Set env
|
||||
run: echo ok go
|
||||
|
||||
build:
|
||||
needs: activate
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout (recursive)
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v5
|
||||
- name: Upload static files as artifact
|
||||
id: deployment
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: src/pyodide/demo-app/
|
||||
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
@@ -98,3 +98,5 @@ src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.py
|
||||
|
||||
# Brickschema
|
||||
src/bonsai/bonsai/bim/schema/Brick.ttl
|
||||
bonsaiDecoratorForLoads.code-workspace
|
||||
dev_environment.bat
|
||||
|
||||
@@ -20,3 +20,6 @@
|
||||
[submodule "src/ifcopenshell-python/ifcopenshell/simple_spf"]
|
||||
path = src/ifcopenshell-python/ifcopenshell/simple_spf
|
||||
url = https://github.com/IfcOpenShell/step-file-parser
|
||||
[submodule "src/pyodide/demo-app/wheels"]
|
||||
path = src/pyodide/demo-app/wheels
|
||||
url = https://github.com/IfcOpenShell/wasm-wheels
|
||||
|
||||
@@ -6,6 +6,7 @@ IfcOpenShell
|
||||
<img src="https://github.com/IfcOpenShell/IfcOpenShell/assets/88302/34901387-e2dd-4a0c-8e38-9ffc32a66cde">
|
||||
</p>
|
||||
|
||||
|
||||
IfcOpenShell is an open source ([LGPL]) software library for working with Industry Foundation Classes ([IFC]). Complete
|
||||
parsing support is provided for [IFC2x3 TC1], [IFC4 Add2 TC1], IFC4x1, IFC4x2, and [IFC4x3 Add2]. Extensive geometric support
|
||||
is implemented for the IFC releases [IFC2x3 TC1] and [IFC4 Add2 TC1]. Extending with support for arbitrary IFC schemas
|
||||
|
||||
@@ -1148,6 +1148,7 @@ if(ADD_COMMIT_SHA)
|
||||
message("git found: ${GIT_EXECUTABLE} with version ${GIT_VERSION_STRING}")
|
||||
execute_process(
|
||||
COMMAND ${GIT_EXECUTABLE} branch -a --contains HEAD
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
OUTPUT_VARIABLE git_branches
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
@@ -1165,11 +1166,18 @@ if(ADD_COMMIT_SHA)
|
||||
|
||||
execute_process(
|
||||
COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
OUTPUT_VARIABLE git_sha
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
|
||||
message(STATUS "IfcOpenShell branch: \"${git_branch}\"")
|
||||
message(STATUS "IfcOpenShell commit: \"${git_sha}\"")
|
||||
|
||||
if ("${git_branch}" STREQUAL "" OR "${git_sha}" STREQUAL "")
|
||||
message(FATAL_ERROR "Unable to determine commit sha and/or branch")
|
||||
endif()
|
||||
|
||||
add_definitions(-DIFCOPENSHELL_BRANCH=${git_branch})
|
||||
add_definitions(-DIFCOPENSHELL_COMMIT=${git_sha})
|
||||
endif()
|
||||
|
||||
+2
-2
@@ -307,7 +307,7 @@ if platform.system() == "Darwin":
|
||||
BOOST_VERSION_UNDERSCORE = BOOST_VERSION.replace(".", "_")
|
||||
|
||||
OCE_LOCATION = f"https://github.com/tpaviot/oce/archive/OCE-{OCE_VERSION}.tar.gz"
|
||||
BOOST_LOCATION = f"https://boostorg.jfrog.io/artifactory/main/release/{BOOST_VERSION}/source/"
|
||||
BOOST_LOCATION = f"https://github.com/boostorg/boost/releases/download/boost-{BOOST_VERSION}/"
|
||||
|
||||
# Helper functions
|
||||
|
||||
@@ -747,7 +747,7 @@ if "boost" in targets:
|
||||
download_url=BOOST_LOCATION,
|
||||
# don't remember what this is, but fail on 1.86
|
||||
# patch="./patches/boost/boostorg_regex_62.patch",
|
||||
download_name=f"boost_{BOOST_VERSION_UNDERSCORE}.tar.bz2"
|
||||
download_name=f"boost-{BOOST_VERSION}-b2-nodocs.tar.gz"
|
||||
)
|
||||
if "wasm" in flags:
|
||||
# only supported on nix for now
|
||||
|
||||
+2
-2
@@ -1,11 +1,11 @@
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
setup(name='IfcOpenShell',
|
||||
setup(name='ifcopenshell',
|
||||
version='0.8.0',
|
||||
description='IfcOpenShell is an open source (LGPL) software library for working with the Industry Foundation Classes (IFC) file format.',
|
||||
author='Thomas Krijnen',
|
||||
author_email='thomas@aecgeeks.com',
|
||||
url='http://ifcopenshell.org',
|
||||
packages=find_packages(),
|
||||
package_data={'': ['*.so']},
|
||||
package_data={'': ['*.so', '*.json']},
|
||||
)
|
||||
|
||||
+15
-4
@@ -85,7 +85,7 @@ BLENDER_PLATFORM:=windows-x64
|
||||
endif
|
||||
|
||||
# Current build commit hash.
|
||||
OLD:=a2538fd
|
||||
OLD:=f425794
|
||||
.PHONY: bump
|
||||
bump:
|
||||
ifndef NEW
|
||||
@@ -177,6 +177,8 @@ endif
|
||||
# Is not platform specific but has platform specific dependencies.
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download python-socketio[asyncio_client] $(PYPI_PLATFORM) --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --dest=./wheels
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download aiohttp $(PYPI_PLATFORM) --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --dest=./wheels
|
||||
# Required to access platform specific paths
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download platformdirs --dest=./wheels
|
||||
# Required by light module
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download pytz --dest=./wheels
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download tzfpy $(PYPI_PLATFORM) --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --dest=./wheels
|
||||
@@ -226,8 +228,16 @@ endif
|
||||
)" bdist_wheel
|
||||
cp -r build/bpypolyskel-master/dist/*.whl build/wheels/
|
||||
|
||||
# Required for Desktop icon and file association
|
||||
cp -r bonsai/libs/desktop build/bonsai/libs/
|
||||
# folder for executable files
|
||||
mkdir -p build/bonsai/libs/bin
|
||||
|
||||
# required for three-way git merging
|
||||
ifeq ($(PLATFORM), win)
|
||||
cd build/bonsai/libs/bin && wget https://github.com/brunopostle/ifcmerge/releases/download/2024-06-24/ifcmerge.zip
|
||||
cd build/bonsai/libs/bin && unzip ifcmerge.zip && rm ifcmerge.zip
|
||||
else
|
||||
cd build/bonsai/libs/bin && wget https://raw.githubusercontent.com/brunopostle/ifcmerge/main/ifcmerge && chmod +x ifcmerge
|
||||
endif
|
||||
|
||||
# Generate translations module for Bonsai build
|
||||
git clone https://github.com/IfcOpenShell/bonsai-translations.git build/working
|
||||
@@ -330,7 +340,7 @@ endif
|
||||
# Use '-n' as on macos x86-64 doesn't have a binary wheel and it autoincludes 'cp311'.
|
||||
prev_whl_name=$$(find build/bonsai/wheels/tzfpy-*.whl); \
|
||||
whl_name=$$(echo $$prev_whl_name | sed "s/-cp39-/-cp$(PYNUMBER)-/"); \
|
||||
mv -n "$$prev_whl_name" "$$whl_name";
|
||||
mv --update=none "$$prev_whl_name" "$$whl_name";
|
||||
|
||||
ifneq ($(PLATFORM), linux)
|
||||
# Safeguard: in case one of `pip download` will break,
|
||||
@@ -348,6 +358,7 @@ endif
|
||||
rm -rf build/bonsai/bim/
|
||||
rm -rf build/bonsai/core/
|
||||
rm -rf build/bonsai/tool/
|
||||
rm -rf build/bonsai/libs/
|
||||
|
||||
ifeq ($(IS_STABLE), TRUE)
|
||||
cd build && zip -r bonsai_$(PYVERSION)-$(VERSION)-$(BLENDER_PLATFORM).zip ./bonsai
|
||||
|
||||
Binary file not shown.
@@ -99,6 +99,7 @@ classes = [
|
||||
operator.BIM_OT_add_section_plane,
|
||||
operator.BIM_OT_delete_object,
|
||||
operator.BIM_OT_remove_section_plane,
|
||||
operator.BIM_OT_select_entity,
|
||||
operator.BIM_OT_select_object,
|
||||
operator.BIM_OT_show_description,
|
||||
operator.BIM_OT_multiple_file_selector,
|
||||
@@ -115,6 +116,7 @@ classes = [
|
||||
operator.RemoveIfcFile,
|
||||
operator.RevertClippingPlaneCut,
|
||||
operator.SelectDataDir,
|
||||
operator.SelectCacheDir,
|
||||
operator.SelectIfcFile,
|
||||
operator.SelectSchemaDir,
|
||||
operator.SelectURIAttribute,
|
||||
@@ -136,6 +138,7 @@ classes = [
|
||||
prop.BIMMeshProperties,
|
||||
prop.BIMFacet,
|
||||
prop.BIMFilterGroup,
|
||||
prop.BIMSnapProperties,
|
||||
ui.BIM_UL_clipping_plane,
|
||||
ui.BIM_UL_generic,
|
||||
ui.BIM_UL_topics,
|
||||
@@ -191,6 +194,8 @@ classes = [
|
||||
# TODO: move this somewhere else and clean it up
|
||||
ui.BIM_PT_section_plane,
|
||||
ui.BIM_PT_section_with_cappings,
|
||||
ui.BIM_PT_decorators_overlay,
|
||||
ui.BIM_PT_snappping,
|
||||
]
|
||||
|
||||
for mod in modules.values():
|
||||
@@ -223,6 +228,7 @@ def register():
|
||||
bpy.app.handlers.load_post.append(handler.load_post)
|
||||
bpy.app.handlers.load_post.append(handler.loadIfcStore)
|
||||
bpy.types.Scene.BIMProperties = bpy.props.PointerProperty(type=prop.BIMProperties)
|
||||
bpy.types.Scene.BIMSnapProperties = bpy.props.PointerProperty(type=prop.BIMSnapProperties)
|
||||
bpy.types.Screen.BIMAreaProperties = bpy.props.CollectionProperty(type=prop.BIMAreaProperties)
|
||||
bpy.types.Screen.BIMTabProperties = bpy.props.PointerProperty(type=prop.BIMTabProperties)
|
||||
bpy.types.Collection.BIMCollectionProperties = bpy.props.PointerProperty(type=prop.BIMCollectionProperties)
|
||||
@@ -257,6 +263,10 @@ def register():
|
||||
icons = icon_preview
|
||||
bpy.app.translations.register("bonsai", translations_dict)
|
||||
|
||||
import bonsai.tool as tool
|
||||
|
||||
tool.Blender.ensure_bin_in_path()
|
||||
|
||||
|
||||
def unregister():
|
||||
global icons
|
||||
|
||||
@@ -30,6 +30,10 @@ from bonsai.bim.ifc import IfcStore
|
||||
from bonsai.bim.module.owner.prop import get_user_person, get_user_organisation
|
||||
from bonsai.bim.module.model.data import AuthoringData
|
||||
from bonsai.bim.module.model.workspace import LIST_OF_TOOLS, TOOLS_TO_CLASSES_MAP
|
||||
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
|
||||
from bonsai.bim.module.nest.decorator import NestDecorator
|
||||
from mathutils import Vector
|
||||
from math import cos, degrees
|
||||
from typing import Union, Callable
|
||||
@@ -134,7 +138,7 @@ def update_bim_tool_props():
|
||||
if AuthoringData.data["active_material_usage"] == "LAYER2":
|
||||
x_angle = get_x_angle(extrusion)
|
||||
axis = tool.Model.get_wall_axis(obj)["reference"]
|
||||
props.extrusion_depth = extrusion.Depth * si_conversion * cos(x_angle)
|
||||
props.extrusion_depth = abs(extrusion.Depth * si_conversion * cos(x_angle))
|
||||
props.length = (axis[1] - axis[0]).length
|
||||
props.x_angle = x_angle
|
||||
|
||||
@@ -305,8 +309,6 @@ def load_post(scene):
|
||||
# After appending the workspace to ensure BIM viewport is affected.
|
||||
subscribe_to_viewport_shading_changes()
|
||||
|
||||
bpy.ops.bim.override_escape("INVOKE_DEFAULT")
|
||||
|
||||
# To improve usability for new users, we hijack the scene properties
|
||||
# tab. We override default scene properties panels with our own poll
|
||||
# to hide them unless the user has chosen to view Blender properties.
|
||||
@@ -318,3 +320,19 @@ def load_post(scene):
|
||||
|
||||
if tool.Ifc.get() and bpy.data.is_saved:
|
||||
bpy.context.scene.BIMProperties.has_blend_warning = True
|
||||
|
||||
# Bonsai overlays
|
||||
georeference_props = bpy.context.scene.BIMGeoreferenceProperties
|
||||
aggregate_props = bpy.context.scene.BIMAggregateProperties
|
||||
nest_props = bpy.context.scene.BIMNestProperties
|
||||
model_props = bpy.context.scene.BIMModelProperties
|
||||
if georeference_props.should_visualise:
|
||||
GeoreferenceDecorator.install(bpy.context)
|
||||
if aggregate_props.aggregate_decorator:
|
||||
AggregateDecorator.install(bpy.context)
|
||||
if nest_props.nest_decorator:
|
||||
NestDecorator.install(bpy.context)
|
||||
if model_props.show_wall_axis:
|
||||
WallAxisDecorator.install(bpy.context)
|
||||
if model_props.show_slab_direction:
|
||||
SlabDirectionDecorator.install(bpy.context)
|
||||
|
||||
@@ -98,9 +98,10 @@ def draw_attribute(
|
||||
if attribute.is_uri:
|
||||
op = layout.operator("bim.select_uri_attribute", text="", icon="FILE_FOLDER")
|
||||
op.data_path = attribute.path_from_id("string_value")
|
||||
elif attribute.special_type == "DATE":
|
||||
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.is_optional:
|
||||
layout.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="")
|
||||
@@ -170,6 +171,8 @@ def import_attribute(
|
||||
new.is_uri = True
|
||||
elif attribute.type_of_attribute()._is("IfcDate"):
|
||||
new.special_type = "DATE"
|
||||
elif attribute.type_of_attribute()._is("IfcDateTime"):
|
||||
new.special_type = "DATETIME"
|
||||
elif data_type == "boolean":
|
||||
new.bool_value = False if new.is_null else bool(data[attribute.name()])
|
||||
elif data_type == "integer":
|
||||
@@ -254,11 +257,25 @@ def export_attributes(
|
||||
ENUM_ITEMS_DATA = Union[bpy.types.PropertyGroup, bpy.types.ID, bpy.types.Operator, bpy.types.OperatorProperties]
|
||||
|
||||
|
||||
def get_display_value(value: str, float_decimal_precision: int = 6) -> str:
|
||||
"""
|
||||
This will get rid of the floating point precision artifacts in float values stored as a string
|
||||
"""
|
||||
try:
|
||||
digits = len(value.split(".")[1])
|
||||
value = float(value)
|
||||
if digits > 6: # Maximal decimal float precision
|
||||
value = round(value, float_decimal_precision)
|
||||
except (ValueError, IndexError): # Not castable to a float or no decimal places (eg integer)
|
||||
pass
|
||||
return str(value)
|
||||
|
||||
|
||||
def prop_with_search(
|
||||
layout: bpy.types.UILayout,
|
||||
data: ENUM_ITEMS_DATA,
|
||||
prop_name: str,
|
||||
should_click_ok_to_validate: bool = False,
|
||||
should_click_ok: bool = False,
|
||||
original_operator_path: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
@@ -271,7 +288,7 @@ def prop_with_search(
|
||||
row.context_pointer_set(name="data", data=data)
|
||||
op = row.operator("bim.enum_property_search", text="", icon="VIEWZOOM")
|
||||
op.prop_name = prop_name
|
||||
op.should_click_ok_to_validate = should_click_ok_to_validate
|
||||
op.should_click_ok = should_click_ok
|
||||
op.original_operator_path = original_operator_path or ""
|
||||
except TypeError: # Prop is not iterable
|
||||
pass
|
||||
@@ -315,6 +332,13 @@ def get_enum_items(
|
||||
return items
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def convert_property_group_from_si(property_group: bpy.types.PropertyGroup, skip_props: tuple[str, ...] = ()) -> None:
|
||||
"""Method converts property group values from si to current ifc project units
|
||||
|
||||
|
||||
@@ -121,7 +121,8 @@ class IfcStore:
|
||||
if IfcStore.cache is None and IfcStore.path:
|
||||
ifc_key = IfcStore.path + IfcStore.file.wrapped_data.header.file_name.time_stamp
|
||||
ifc_hash = hashlib.md5(ifc_key.encode("utf-8")).hexdigest()
|
||||
IfcStore.cache_path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "cache", f"{ifc_hash}.h5")
|
||||
os.makedirs(bpy.context.scene.BIMProperties.cache_dir, exist_ok=True)
|
||||
IfcStore.cache_path = os.path.join(bpy.context.scene.BIMProperties.cache_dir, f"{ifc_hash}.h5")
|
||||
cache_path = Path(IfcStore.cache_path)
|
||||
cache_settings = ifcopenshell.geom.settings()
|
||||
serializer_settings = ifcopenshell.geom.serializer_settings()
|
||||
@@ -161,7 +162,7 @@ class IfcStore:
|
||||
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()
|
||||
new_cache_path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "cache", f"{ifc_hash}.h5")
|
||||
new_cache_path = os.path.join(bpy.context.scene.BIMProperties.cache_dir, f"{ifc_hash}.h5")
|
||||
IfcStore.cache = None
|
||||
try:
|
||||
shutil.move(IfcStore.cache_path, new_cache_path)
|
||||
@@ -404,9 +405,6 @@ class IfcStore:
|
||||
obj.BIMStyleProperties.ifc_definition_id = 0
|
||||
else: # bpy.types.Object
|
||||
obj.BIMObjectProperties.ifc_definition_id = 0
|
||||
# NOTE: in theory this will also remove listeners added by other addons
|
||||
# though never had a report when this would be a problem.
|
||||
bpy.msgbus.clear_by_owner(obj)
|
||||
|
||||
@staticmethod
|
||||
def execute_ifc_operator(operator: tool.Ifc.Operator, context: bpy.types.Context, is_invoke=False) -> set[str]:
|
||||
|
||||
@@ -260,8 +260,6 @@ class IfcImporter:
|
||||
self.profile_code("Setup arrays")
|
||||
tool.Project.load_linked_models_from_ifc()
|
||||
self.profile_code("Load linked models")
|
||||
self.lock_scales()
|
||||
self.profile_code("Lock objects scales")
|
||||
self.add_project_to_scene()
|
||||
self.profile_code("Add project to scene")
|
||||
if self.ifc_import_settings.should_clean_mesh and len(self.file.by_type("IfcElement")) < 1000:
|
||||
@@ -895,9 +893,9 @@ class IfcImporter:
|
||||
tool.Loader.set_unit_scale(self.unit_scale)
|
||||
|
||||
def set_units(self):
|
||||
if not (units := self.file.by_type("IfcUnitAssignment")):
|
||||
if not (assignment := self.file.by_type("IfcProject")[0].UnitsInContext):
|
||||
return # Geometry is optional in IFC
|
||||
for unit in units[0].Units:
|
||||
for unit in assignment.Units:
|
||||
if unit.is_a("IfcNamedUnit") and unit.UnitType == "LENGTHUNIT":
|
||||
if unit.is_a("IfcSIUnit"):
|
||||
bpy.context.scene.unit_settings.system = "METRIC"
|
||||
@@ -1118,20 +1116,6 @@ class IfcImporter:
|
||||
tool.Blender.Modifier.Array.set_children_lock_state(element, i, True)
|
||||
tool.Blender.Modifier.Array.constrain_children_to_parent(element)
|
||||
|
||||
def lock_scales(self) -> None:
|
||||
elements = set(self.file.by_type("IfcProduct"))
|
||||
while elements:
|
||||
element = elements.pop()
|
||||
if not getattr(element, "HasOpenings", False):
|
||||
continue
|
||||
voided_elements = tool.Aggregate.get_parts_recursively(element)
|
||||
voided_elements.add(element)
|
||||
elements.difference_update(voided_elements)
|
||||
for element in voided_elements:
|
||||
if not (obj := tool.Ifc.get_object(element)):
|
||||
continue
|
||||
tool.Geometry.lock_scale(obj)
|
||||
|
||||
|
||||
class IfcImportSettings:
|
||||
def __init__(self):
|
||||
|
||||
@@ -30,7 +30,11 @@ classes = (
|
||||
operator.BIM_OT_aggregate_unassign_object,
|
||||
operator.BIM_OT_break_link_to_other_aggregates,
|
||||
operator.BIM_OT_select_linked_aggregates,
|
||||
operator.BIM_OT_disable_aggregate_mode,
|
||||
operator.BIM_OT_toggle_aggregate_mode_local_view,
|
||||
prop.BIMObjectAggregateProperties,
|
||||
prop.Objects,
|
||||
prop.BIMAggregateProperties,
|
||||
ui.BIM_PT_aggregate,
|
||||
ui.BIM_PT_linked_aggregate,
|
||||
)
|
||||
@@ -38,7 +42,9 @@ classes = (
|
||||
|
||||
def register():
|
||||
bpy.types.Object.BIMObjectAggregateProperties = bpy.props.PointerProperty(type=prop.BIMObjectAggregateProperties)
|
||||
bpy.types.Scene.BIMAggregateProperties = bpy.props.PointerProperty(type=prop.BIMAggregateProperties)
|
||||
|
||||
|
||||
def unregister():
|
||||
del bpy.types.Object.BIMObjectAggregateProperties
|
||||
del bpy.types.Scene.BIMAggregateProperties
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2024 Bruno Perdigão <contact@brunopo.com>
|
||||
#
|
||||
# This file is part of Bonsai.
|
||||
#
|
||||
# Bonsai is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Bonsai is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import blf
|
||||
import bpy
|
||||
import gpu
|
||||
import ifcopenshell
|
||||
import bonsai.tool as tool
|
||||
from bpy.types import SpaceView3D
|
||||
from bpy_extras import view3d_utils
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
from mathutils import Vector
|
||||
from bonsai.bim.module.geometry.decorator import ItemDecorator
|
||||
|
||||
|
||||
def transparent_color(color, alpha=0.1):
|
||||
color = [i for i in color]
|
||||
color[3] = alpha
|
||||
return color
|
||||
|
||||
|
||||
def create_bounding_box(objs):
|
||||
# Initialize the bounding box coordinates
|
||||
min_x, min_y, min_z = float("inf"), float("inf"), float("inf")
|
||||
max_x, max_y, max_z = float("-inf"), float("-inf"), float("-inf")
|
||||
|
||||
# Iterate over the selected objects
|
||||
for obj in objs:
|
||||
if not obj.data:
|
||||
continue
|
||||
# Get the object's bounding box coordinates
|
||||
|
||||
bbox_world = [obj.matrix_world @ Vector(b) for b in obj.bound_box]
|
||||
obj_min = []
|
||||
obj_min.append(min([b[0] for b in bbox_world]))
|
||||
obj_min.append(min([b[1] for b in bbox_world]))
|
||||
obj_min.append(min([b[2] for b in bbox_world]))
|
||||
obj_max = []
|
||||
obj_max.append(max([b[0] for b in bbox_world]))
|
||||
obj_max.append(max([b[1] for b in bbox_world]))
|
||||
obj_max.append(max([b[2] for b in bbox_world]))
|
||||
|
||||
# Update the overall bounding box coordinates
|
||||
min_x = min(min_x, min(obj_min[0], obj_max[0]))
|
||||
min_y = min(min_y, min(obj_min[1], obj_max[1]))
|
||||
min_z = min(min_z, min(obj_min[2], obj_max[2]))
|
||||
max_x = max(max_x, max(obj_min[0], obj_max[0]))
|
||||
max_y = max(max_y, max(obj_min[1], obj_max[1]))
|
||||
max_z = max(max_z, max(obj_min[2], obj_max[2]))
|
||||
|
||||
indices = [
|
||||
(min_x, min_y, min_z),
|
||||
(max_x, min_y, min_z),
|
||||
(max_x, max_y, min_z),
|
||||
(min_x, max_y, min_z),
|
||||
(min_x, min_y, max_z),
|
||||
(max_x, min_y, max_z),
|
||||
(max_x, max_y, max_z),
|
||||
(min_x, max_y, max_z),
|
||||
]
|
||||
|
||||
edges = [(0, 1), (1, 2), (2, 3), (3, 0), (4, 5), (5, 6), (6, 7), (7, 4), (0, 4), (1, 5), (2, 6), (3, 7)]
|
||||
|
||||
return indices, edges
|
||||
|
||||
|
||||
class AggregateDecorator:
|
||||
is_installed = False
|
||||
handlers = []
|
||||
|
||||
@classmethod
|
||||
def install(cls, context):
|
||||
if cls.is_installed:
|
||||
cls.uninstall()
|
||||
handler = cls()
|
||||
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_aggregate, (context,), "WINDOW", "POST_VIEW"))
|
||||
cls.is_installed = True
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls):
|
||||
for handler in cls.handlers:
|
||||
try:
|
||||
SpaceView3D.draw_handler_remove(handler, "WINDOW")
|
||||
except ValueError:
|
||||
pass
|
||||
cls.is_installed = False
|
||||
|
||||
def dotted_line_shader(self):
|
||||
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface")
|
||||
vert_out.smooth("FLOAT", "v_ArcLength")
|
||||
|
||||
shader_info = gpu.types.GPUShaderCreateInfo()
|
||||
shader_info.push_constant("MAT4", "u_ViewProjectionMatrix")
|
||||
shader_info.push_constant("FLOAT", "u_Scale")
|
||||
shader_info.vertex_in(0, "VEC3", "position")
|
||||
shader_info.vertex_in(1, "FLOAT", "arcLength")
|
||||
shader_info.vertex_out(vert_out)
|
||||
shader_info.fragment_out(0, "VEC4", "FragColor")
|
||||
shader_info.push_constant("VEC4", "color")
|
||||
|
||||
shader_info.vertex_source(
|
||||
"void main()"
|
||||
"{"
|
||||
" v_ArcLength = arcLength;"
|
||||
" gl_Position = u_ViewProjectionMatrix * vec4(position, 1.0f);"
|
||||
"}"
|
||||
)
|
||||
|
||||
shader_info.fragment_source(
|
||||
"void main()" "{" " if (step(sin(v_ArcLength * u_Scale), 0.4) == 1) discard;" " FragColor = color;" "}"
|
||||
)
|
||||
|
||||
shader = gpu.shader.create_from_info(shader_info)
|
||||
del vert_out
|
||||
del shader_info
|
||||
return shader
|
||||
|
||||
def draw_custom_batch(self, coords, color):
|
||||
shader = self.dotted_line_shader()
|
||||
|
||||
arc_lengths = [0]
|
||||
for a, b in zip(coords[:-1], coords[1:]):
|
||||
arc_lengths.append(arc_lengths[-1] + (a - b).length)
|
||||
|
||||
batch = batch_for_shader(
|
||||
shader,
|
||||
"LINE_STRIP",
|
||||
{"position": coords, "arcLength": arc_lengths},
|
||||
)
|
||||
|
||||
matrix = bpy.context.region_data.perspective_matrix
|
||||
shader.uniform_float("color", color)
|
||||
shader.uniform_float("u_ViewProjectionMatrix", matrix)
|
||||
shader.uniform_float("u_Scale", 25)
|
||||
batch.draw(shader)
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
|
||||
def draw_aggregate(self, context):
|
||||
if context.scene.BIMAggregateProperties.in_aggregate_mode:
|
||||
return
|
||||
self.addon_prefs = tool.Blender.get_addon_preferences()
|
||||
decorator_color_special = self.addon_prefs.decorator_color_special
|
||||
decorator_color_selected = self.addon_prefs.decorator_color_selected
|
||||
decorator_color_error = self.addon_prefs.decorator_color_error
|
||||
decorator_color_unselected = self.addon_prefs.decorator_color_unselected
|
||||
decorator_color_background = self.addon_prefs.decorator_color_background
|
||||
theme = context.preferences.themes.items()[0][1]
|
||||
selected_object_color = (*theme.view_3d.object_active, 1)
|
||||
|
||||
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
self.shader.bind()
|
||||
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
|
||||
self.line_shader.bind()
|
||||
# POLYLINE_UNIFORM_COLOR specific uniforms
|
||||
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
|
||||
aggregates = []
|
||||
if not (selected_objects := context.selected_objects):
|
||||
return
|
||||
for obj in selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not element.is_a("IfcElement"):
|
||||
return
|
||||
|
||||
parts = ifcopenshell.util.element.get_parts(element)
|
||||
if parts:
|
||||
aggregates.append(obj)
|
||||
continue
|
||||
|
||||
aggregate = ifcopenshell.util.element.get_aggregate(element)
|
||||
if aggregate:
|
||||
aggregates.append(tool.Ifc.get_object(aggregate))
|
||||
|
||||
aggregates = set(aggregates)
|
||||
for aggregate in aggregates:
|
||||
self.line_shader.uniform_float("lineWidth", 1.0)
|
||||
color = decorator_color_unselected
|
||||
if aggregate in selected_objects:
|
||||
color = selected_object_color
|
||||
size = aggregate.empty_display_size
|
||||
location = aggregate.location
|
||||
line_x = (location - Vector((size, 0.0, 0.0)), location + Vector((size, 0.0, 0.0)))
|
||||
self.draw_batch("LINES", line_x, color, [(0, 1)])
|
||||
line_y = (location - Vector((0.0, size, 0.0)), location + Vector((0.0, size, 0.0)))
|
||||
self.draw_batch("LINES", line_y, color, [(0, 1)])
|
||||
line_z = (location - Vector((0.0, 0.0, size)), location + Vector((0.0, 0.0, size)))
|
||||
self.draw_batch("LINES", line_z, color, [(0, 1)])
|
||||
if context.scene.BIMAggregateProperties.in_aggregate_mode:
|
||||
return
|
||||
parts = ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(aggregate))
|
||||
parts_objs = [tool.Ifc.get_object(p) for p in parts]
|
||||
|
||||
indices, edges = create_bounding_box(parts_objs)
|
||||
self.line_shader.uniform_float("lineWidth", 0.5)
|
||||
self.draw_batch("LINES", indices, color, edges)
|
||||
line = (Vector(indices[0]), location)
|
||||
self.draw_custom_batch(line, decorator_color_unselected)
|
||||
|
||||
|
||||
class AggregateModeDecorator:
|
||||
is_installed = False
|
||||
handlers = []
|
||||
|
||||
@classmethod
|
||||
def install(cls, context):
|
||||
if cls.is_installed:
|
||||
cls.uninstall()
|
||||
handler = cls()
|
||||
cls.handlers.append(
|
||||
SpaceView3D.draw_handler_add(handler.draw_aggregate_name, (context,), "WINDOW", "POST_PIXEL")
|
||||
)
|
||||
cls.handlers.append(
|
||||
SpaceView3D.draw_handler_add(handler.draw_aggregate_empty, (context,), "WINDOW", "POST_VIEW")
|
||||
)
|
||||
cls.is_installed = True
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls):
|
||||
for handler in cls.handlers:
|
||||
try:
|
||||
SpaceView3D.draw_handler_remove(handler, "WINDOW")
|
||||
except ValueError:
|
||||
pass
|
||||
cls.is_installed = False
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
|
||||
def draw_aggregate_name(self, context):
|
||||
if context.mode == "EDIT_MESH":
|
||||
return
|
||||
region = context.region
|
||||
rv3d = region.data
|
||||
props = context.scene.BIMAggregateProperties
|
||||
|
||||
aggregate_obj = props.editing_aggregate
|
||||
if not aggregate_obj:
|
||||
return
|
||||
self.addon_prefs = tool.Blender.get_addon_preferences()
|
||||
self.font_id = 0
|
||||
font_size = tool.Blender.scale_font_size(12)
|
||||
blf.size(self.font_id, font_size)
|
||||
blf.enable(self.font_id, blf.SHADOW)
|
||||
blf.shadow(self.font_id, 6, 0, 0, 0, 1)
|
||||
color = self.addon_prefs.decorator_color_selected
|
||||
if aggregate_obj in context.selected_objects:
|
||||
color = self.addon_prefs.decorator_color_selected
|
||||
blf.color(self.font_id, *color)
|
||||
text = aggregate_obj.name
|
||||
text_coords = view3d_utils.location_3d_to_region_2d(region, rv3d, aggregate_obj.location)
|
||||
text_length = blf.dimensions(self.font_id, text)
|
||||
text_coords[0] -= text_length[0] / 2
|
||||
text_coords[1] -= 20
|
||||
blf.position(self.font_id, text_coords[0], text_coords[1], 0)
|
||||
|
||||
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
|
||||
blf.draw(self.font_id, text)
|
||||
|
||||
def draw_aggregate_empty(self, context):
|
||||
if context.mode == "EDIT_MESH":
|
||||
return
|
||||
props = context.scene.BIMAggregateProperties
|
||||
aggregate_obj = props.editing_aggregate
|
||||
if not aggregate_obj:
|
||||
return
|
||||
self.addon_prefs = tool.Blender.get_addon_preferences()
|
||||
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
|
||||
self.line_shader.bind()
|
||||
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
|
||||
self.line_shader.uniform_float("lineWidth", 2.0)
|
||||
theme = context.preferences.themes.items()[0][1]
|
||||
selected_object_color = (*theme.view_3d.object_active, 1)
|
||||
color = self.addon_prefs.decorator_color_selected
|
||||
if aggregate_obj in context.selected_objects:
|
||||
color = selected_object_color
|
||||
size = aggregate_obj.empty_display_size
|
||||
location = aggregate_obj.location
|
||||
line_x = (location - Vector((size, 0.0, 0.0)), location + Vector((size, 0.0, 0.0)))
|
||||
self.draw_batch("LINES", line_x, color, [(0, 1)])
|
||||
line_y = (location - Vector((0.0, size, 0.0)), location + Vector((0.0, size, 0.0)))
|
||||
self.draw_batch("LINES", line_y, color, [(0, 1)])
|
||||
line_z = (location - Vector((0.0, 0.0, size)), location + Vector((0.0, 0.0, size)))
|
||||
self.draw_batch("LINES", line_z, color, [(0, 1)])
|
||||
parts = ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(aggregate_obj))
|
||||
if parts:
|
||||
for part in parts:
|
||||
part_obj = tool.Ifc.get_object(part)
|
||||
if part.is_a("IfcElementAssembly"):
|
||||
self.line_shader.uniform_float("lineWidth", 1.0)
|
||||
size = part_obj.empty_display_size
|
||||
location = part_obj.location
|
||||
color = self.addon_prefs.decorator_color_unselected
|
||||
if part_obj in context.selected_objects:
|
||||
color = selected_object_color
|
||||
line_x = (location - Vector((size, 0.0, 0.0)), location + Vector((size, 0.0, 0.0)))
|
||||
self.draw_batch("LINES", line_x, color, [(0, 1)])
|
||||
line_y = (location - Vector((0.0, size, 0.0)), location + Vector((0.0, size, 0.0)))
|
||||
self.draw_batch("LINES", line_y, color, [(0, 1)])
|
||||
line_z = (location - Vector((0.0, 0.0, size)), location + Vector((0.0, 0.0, size)))
|
||||
self.draw_batch("LINES", line_z, color, [(0, 1)])
|
||||
parts = ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(aggregate_obj))
|
||||
|
||||
color = self.addon_prefs.decorator_color_selected
|
||||
parts_objs = [tool.Ifc.get_object(p) for p in parts]
|
||||
indices, edges = create_bounding_box(parts_objs)
|
||||
self.line_shader.uniform_float("lineWidth", 0.5)
|
||||
self.draw_batch("LINES", indices, color, edges)
|
||||
@@ -23,6 +23,7 @@ import ifcopenshell.util.element
|
||||
import bonsai.tool as tool
|
||||
import bonsai.core.aggregate as core
|
||||
import bonsai.core.spatial
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
|
||||
|
||||
class BIM_OT_aggregate_assign_object(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -66,6 +67,10 @@ class BIM_OT_aggregate_assign_object(bpy.types.Operator, tool.Ifc.Operator):
|
||||
relating_obj=relating_obj,
|
||||
related_obj=obj,
|
||||
)
|
||||
props = context.scene.BIMAggregateProperties
|
||||
if relating_obj == props.editing_aggregate and props.in_aggregate_mode:
|
||||
new_editing_obj = props.editing_objects.add()
|
||||
new_editing_obj.obj = obj
|
||||
except core.IncompatibleAggregateError:
|
||||
self.report({"ERROR"}, f"Cannot aggregate {obj.name} to {relating_obj.name}")
|
||||
except core.AggregateRepresentationError:
|
||||
@@ -365,3 +370,33 @@ class BIM_OT_select_linked_aggregates(bpy.types.Operator):
|
||||
obj.select_set(True)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class BIM_OT_disable_aggregate_mode(bpy.types.Operator):
|
||||
bl_idname = "bim.disable_aggregate_mode"
|
||||
bl_label = "Disable Aggregate Mode"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
bonsai.core.aggregate.disable_aggregate_mode(tool.Aggregate)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class BIM_OT_toggle_aggregate_mode_local_view(bpy.types.Operator):
|
||||
bl_idname = "bim.toggle_aggregate_mode_local_view"
|
||||
bl_label = "Toggle Aggregate Mode Local View"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.BIMAggregateProperties
|
||||
objs = [o.obj for o in props.editing_objects]
|
||||
if props.in_aggregate_mode:
|
||||
if context.space_data.local_view:
|
||||
bpy.ops.view3d.localview()
|
||||
else:
|
||||
for obj in objs:
|
||||
obj.select_set(True)
|
||||
bpy.ops.view3d.localview()
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -32,6 +32,7 @@ from bpy.props import (
|
||||
FloatVectorProperty,
|
||||
CollectionProperty,
|
||||
)
|
||||
from bonsai.bim.module.aggregate.decorator import AggregateDecorator, AggregateModeDecorator
|
||||
|
||||
|
||||
def can_aggregate(relating_obj: bpy.types.Object, related_obj: bpy.types.Object) -> bool:
|
||||
@@ -73,6 +74,20 @@ def poll_related_object(self: "BIMObjectAggregateProperties", related_obj: bpy.t
|
||||
return True
|
||||
|
||||
|
||||
def update_aggregate_decorator(self, context):
|
||||
if self.aggregate_decorator:
|
||||
AggregateDecorator.install(bpy.context)
|
||||
else:
|
||||
AggregateDecorator.uninstall()
|
||||
|
||||
|
||||
def update_aggregate_mode_decorator(self, context):
|
||||
if self.in_aggregate_mode:
|
||||
AggregateModeDecorator.install(bpy.context)
|
||||
else:
|
||||
AggregateModeDecorator.uninstall()
|
||||
|
||||
|
||||
class BIMObjectAggregateProperties(PropertyGroup):
|
||||
is_editing: BoolProperty(name="Is Editing")
|
||||
relating_object: PointerProperty(name="Relating Whole", type=bpy.types.Object, poll=poll_relating_object)
|
||||
@@ -82,3 +97,20 @@ class BIMObjectAggregateProperties(PropertyGroup):
|
||||
type=bpy.types.Object,
|
||||
poll=poll_related_object,
|
||||
)
|
||||
|
||||
|
||||
class Objects(bpy.types.PropertyGroup):
|
||||
obj: PointerProperty(type=bpy.types.Object)
|
||||
previous_display_type: bpy.props.StringProperty(default="TEXTURED")
|
||||
|
||||
|
||||
class BIMAggregateProperties(PropertyGroup):
|
||||
in_aggregate_mode: BoolProperty(name="In Edit Mode", update=update_aggregate_mode_decorator)
|
||||
editing_aggregate: PointerProperty(name="Editing Aggregate", type=bpy.types.Object)
|
||||
editing_objects: CollectionProperty(type=Objects)
|
||||
not_editing_objects: CollectionProperty(type=Objects)
|
||||
aggregate_decorator: BoolProperty(
|
||||
name="Display Aggregate",
|
||||
default=False,
|
||||
update=update_aggregate_decorator,
|
||||
)
|
||||
|
||||
@@ -47,6 +47,9 @@ class BIM_PT_aggregate(Panel):
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
row = layout.row()
|
||||
row.label(text="Aggregate Decorator")
|
||||
row.prop(context.scene.BIMAggregateProperties, "aggregate_decorator", icon="HIDE_OFF", text="")
|
||||
if not AggregateData.is_loaded:
|
||||
AggregateData.load()
|
||||
|
||||
|
||||
@@ -28,20 +28,33 @@ import bonsai.core.attribute as core
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
|
||||
|
||||
def get_objs_for_operation(operator_properties, context):
|
||||
if operator_properties.obj:
|
||||
return [bpy.data.objects[operator_properties.obj]]
|
||||
if operator_properties.mass_operation:
|
||||
return context.selected_objects[:]
|
||||
return [context.active_object]
|
||||
|
||||
|
||||
class EnableEditingAttributes(bpy.types.Operator):
|
||||
bl_idname = "bim.enable_editing_attributes"
|
||||
bl_label = "Enable Editing Attributes"
|
||||
bl_description = "ALT + Left Click to enable editing attributes on all selected objects"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
obj: bpy.props.StringProperty(options={"SKIP_SAVE"})
|
||||
mass_operation: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
|
||||
|
||||
def execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
obj = bpy.data.objects[self.obj]
|
||||
def invoke(self, context, event):
|
||||
self.mass_operation = event.alt
|
||||
return self.execute(context)
|
||||
|
||||
def enable_editing_attribute_on_obj(self, obj):
|
||||
props = obj.BIMAttributeProperties
|
||||
props.attributes.clear()
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
assert element
|
||||
if not element:
|
||||
return
|
||||
has_inherited_predefined_type = False
|
||||
if not element.is_a("IfcTypeObject") and (element_type := ifcopenshell.util.element.get_type(element)):
|
||||
# Allow for None due to https://github.com/buildingSMART/IFC4.3.x-development/issues/818
|
||||
@@ -67,31 +80,48 @@ class EnableEditingAttributes(bpy.types.Operator):
|
||||
|
||||
bonsai.bim.helper.import_attributes2(element, props.attributes, callback=callback)
|
||||
props.is_editing_attributes = True
|
||||
|
||||
def execute(self, context):
|
||||
for obj in get_objs_for_operation(self, context):
|
||||
self.enable_editing_attribute_on_obj(obj)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DisableEditingAttributes(bpy.types.Operator):
|
||||
bl_idname = "bim.disable_editing_attributes"
|
||||
bl_label = "Disable Editing Attributes"
|
||||
bl_description = "ALT + Left Click to disable editing attributes on all selected objects"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
obj: bpy.props.StringProperty(options={"SKIP_SAVE"})
|
||||
mass_operation: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
|
||||
|
||||
def execute(self, context):
|
||||
obj = bpy.data.objects.get(self.obj)
|
||||
def invoke(self, context, event):
|
||||
self.mass_operation = event.alt
|
||||
return self.execute(context)
|
||||
|
||||
def disable_editing_attributes_on_obj(self, obj):
|
||||
props = obj.BIMAttributeProperties
|
||||
props.is_editing_attributes = False
|
||||
|
||||
def execute(self, context):
|
||||
for obj in get_objs_for_operation(self, context):
|
||||
self.disable_editing_attributes_on_obj(obj)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class EditAttributes(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.edit_attributes"
|
||||
bl_label = "Edit Attributes"
|
||||
bl_description = "ALT + Left Click to edit attributes on all selected objects"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
obj: bpy.props.StringProperty(options={"SKIP_SAVE"})
|
||||
mass_operation: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
|
||||
|
||||
def _execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
obj = bpy.data.objects.get(self.obj)
|
||||
def invoke(self, context, event):
|
||||
self.mass_operation = event.alt
|
||||
return self.execute(context)
|
||||
|
||||
def edit_attributes_on_obj(self, obj):
|
||||
props = obj.BIMAttributeProperties
|
||||
product = tool.Ifc.get_entity(obj)
|
||||
|
||||
@@ -112,6 +142,11 @@ class EditAttributes(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if (name := tool.Loader.get_name(product)) and obj.name != name:
|
||||
obj.name = name
|
||||
bpy.ops.bim.disable_editing_attributes(obj=obj.name)
|
||||
|
||||
def _execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
for obj in get_objs_for_operation(self, context):
|
||||
self.edit_attributes_on_obj(obj)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import bonsai.bim.helper
|
||||
from bpy.types import Panel
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
from bonsai.bim.module.attribute.data import AttributesData
|
||||
import bonsai.tool as tool
|
||||
|
||||
|
||||
def draw_ui(context, layout, attributes):
|
||||
@@ -30,21 +31,18 @@ def draw_ui(context, layout, attributes):
|
||||
if props.is_editing_attributes:
|
||||
row = layout.row(align=True)
|
||||
op = row.operator("bim.edit_attributes", icon="CHECKMARK", text="Save Attributes")
|
||||
op.obj = obj.name
|
||||
op = row.operator("bim.disable_editing_attributes", icon="CANCEL", text="")
|
||||
op.obj = obj.name
|
||||
|
||||
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")
|
||||
op.obj = obj.name
|
||||
|
||||
for attribute in attributes:
|
||||
row = layout.row(align=True)
|
||||
row.label(text=attribute["name"])
|
||||
# row.label(text=attribute["value"])
|
||||
op = row.operator("bim.select_similar", text=attribute["value"], icon="NONE", emboss=False)
|
||||
value = bonsai.bim.helper.get_display_value(attribute["value"])
|
||||
op = row.operator("bim.select_similar", text=value, icon="NONE", emboss=False)
|
||||
op.key = attribute["name"]
|
||||
|
||||
# TODO: reimplement, see #1222
|
||||
@@ -62,11 +60,7 @@ class BIM_PT_object_attributes(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not context.active_object:
|
||||
return False
|
||||
if not IfcStore.get_element(context.active_object.BIMObjectProperties.ifc_definition_id):
|
||||
return False
|
||||
return bool(context.active_object.BIMObjectProperties.ifc_definition_id)
|
||||
return tool.Ifc.get_entity(context.active_object)
|
||||
|
||||
def draw(self, context):
|
||||
if not AttributesData.is_loaded:
|
||||
|
||||
@@ -616,7 +616,7 @@ class AddBcfViewpoint(bpy.types.Operator):
|
||||
old_file_format = blender_render.image_settings.file_format
|
||||
blender_render.image_settings.file_format = "PNG"
|
||||
old_filepath = blender_render.filepath
|
||||
blender_render.filepath = os.path.join(context.scene.BIMProperties.data_dir, "snapshot.png")
|
||||
blender_render.filepath = tool.Blender.get_data_dir_path("snapshot.png").__str__()
|
||||
bpy.ops.render.opengl(write_still=True)
|
||||
with open(blender_render.filepath, "rb") as f:
|
||||
snapshot = f.read()
|
||||
|
||||
@@ -31,4 +31,3 @@ class BIMCadProperties(PropertyGroup):
|
||||
gable_roof_edge_angle: bpy.props.FloatProperty(
|
||||
name="Gable Roof Edge Angle", default=pi / 2, soft_min=0, soft_max=pi / 2, subtype="ANGLE"
|
||||
)
|
||||
gable_roof_separate_verts: bpy.props.BoolProperty(name="Separate Verts", default=True)
|
||||
|
||||
@@ -26,24 +26,10 @@ from bonsai.bim.module.model.data import AuthoringData, RailingData, RoofData
|
||||
from typing import Union
|
||||
|
||||
|
||||
# TODO duplicate code in cad/workspace and model/workspace
|
||||
def check_display_mode():
|
||||
global display_mode
|
||||
try:
|
||||
theme = bpy.context.preferences.themes["Default"]
|
||||
text_color = theme.user_interface.wcol_menu_item.text
|
||||
if sum(text_color) < 2.6:
|
||||
display_mode = "lm"
|
||||
else:
|
||||
display_mode = "dm"
|
||||
except:
|
||||
display_mode = "dm"
|
||||
|
||||
|
||||
def load_custom_icons():
|
||||
global custom_icon_previews
|
||||
global custom_icon_previews, display_mode
|
||||
if display_mode is None:
|
||||
check_display_mode()
|
||||
display_mode = tool.Blender.detect_icon_color_mode("user_interface.wcol_tool.text")
|
||||
|
||||
icons_dir = os.path.join(os.path.dirname(__file__), "..", "..", "data", "icons")
|
||||
custom_icon_previews = bpy.utils.previews.new()
|
||||
@@ -236,7 +222,6 @@ class CadHotkey(bpy.types.Operator):
|
||||
and bpy.context.active_object.BIMRoofProperties.is_editing_path
|
||||
):
|
||||
self.layout.row().prop(props, "gable_roof_edge_angle")
|
||||
self.layout.row().prop(props, "gable_roof_separate_verts")
|
||||
|
||||
elif self.hotkey == "S_V":
|
||||
if not tool.Geometry.is_profile_object_active():
|
||||
@@ -285,9 +270,7 @@ class CadHotkey(bpy.types.Operator):
|
||||
and RoofData.data["pset_data"]
|
||||
and bpy.context.active_object.BIMRoofProperties.is_editing_path
|
||||
):
|
||||
bpy.ops.bim.set_gable_roof_edge_angle(
|
||||
angle=self.props.gable_roof_edge_angle, separate_verts=self.props.gable_roof_separate_verts
|
||||
)
|
||||
bpy.ops.bim.set_gable_roof_edge_angle(angle=self.props.gable_roof_edge_angle)
|
||||
|
||||
def hotkey_S_T(self):
|
||||
bpy.ops.bim.cad_mitre()
|
||||
|
||||
@@ -266,7 +266,7 @@ class ExecuteIfcClash(bpy.types.Operator):
|
||||
context.scene.render.resolution_x = 480
|
||||
context.scene.render.resolution_y = 270
|
||||
context.scene.render.image_settings.file_format = "PNG"
|
||||
context.scene.render.filepath = os.path.join(context.scene.BIMProperties.data_dir, "snapshot.png")
|
||||
context.scene.render.filepath = tool.Blender.get_data_dir_path("shapshot.png").__str__()
|
||||
bpy.ops.render.opengl(write_still=True)
|
||||
with open(context.scene.render.filepath, "rb") as f:
|
||||
return ("snapshot.png", f.read())
|
||||
|
||||
@@ -313,10 +313,8 @@ class BIM_PT_material_classifications(Panel, ReferenceUI):
|
||||
if not tool.Ifc.get():
|
||||
return False
|
||||
props = context.scene.BIMMaterialProperties
|
||||
if props.materials and props.active_material_index < len(props.materials):
|
||||
material = props.materials[props.active_material_index]
|
||||
if material.ifc_definition_id:
|
||||
return True
|
||||
if props.is_editing and (material := props.active_material) and material.ifc_definition_id:
|
||||
return True
|
||||
return False
|
||||
|
||||
def draw(self, context):
|
||||
|
||||
@@ -713,9 +713,9 @@ class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return {"CANCELLED"}
|
||||
plural_object_type = f"{object_type.lower()}s"
|
||||
if merged_data:
|
||||
print(f"Merged {plural_object_type}:")
|
||||
for element_type, element_names in merged_data.items():
|
||||
print(f"- {element_type}: {', '.join(element_names)}")
|
||||
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 ""
|
||||
|
||||
@@ -22,6 +22,7 @@ import math
|
||||
import bmesh
|
||||
import bonsai.tool as tool
|
||||
import ifcopenshell.util.element
|
||||
from pathlib import Path
|
||||
from mathutils import Vector, Matrix
|
||||
from typing import Optional
|
||||
|
||||
@@ -43,7 +44,7 @@ class Annotator:
|
||||
font = bpy.data.fonts.get("OpenGost TypeB TT")
|
||||
if not font:
|
||||
font = bpy.data.fonts.load(
|
||||
os.path.join(bpy.context.scene.BIMProperties.data_dir, "fonts", "OpenGost Type B TT.ttf")
|
||||
tool.Blender.get_data_dir_path(Path("fonts") / "OpenGost Type B TT.ttf").__str__()
|
||||
)
|
||||
font.name = "OpenGost Type B TT"
|
||||
obj.data.font = font
|
||||
|
||||
@@ -82,8 +82,7 @@ class SheetsData:
|
||||
|
||||
@classmethod
|
||||
def titleblocks(cls):
|
||||
files = Path(os.path.join(bpy.context.scene.BIMProperties.data_dir, "templates", "titleblocks")).glob("*.svg")
|
||||
files = [str(f.stem) for f in files]
|
||||
files = [p.stem for p in tool.Blender.get_data_dir_paths(Path("templates") / "titleblocks", "*.svg")]
|
||||
|
||||
if tool.Ifc.get():
|
||||
project = tool.Ifc.get().by_type("IfcProject")[0]
|
||||
|
||||
@@ -28,6 +28,7 @@ import ifcopenshell.util.element
|
||||
import ifcopenshell.util.unit
|
||||
import bonsai.tool as tool
|
||||
import bonsai.bim.module.drawing.helper as helper
|
||||
from pathlib import Path
|
||||
from math import pi, sin, cos, tan, acos, atan, degrees, radians, ceil
|
||||
from bpy.types import SpaceView3D
|
||||
from mathutils import Vector, Matrix
|
||||
@@ -2022,9 +2023,9 @@ class DecorationsHandler:
|
||||
self.decorators[object_type] = self.decorators["FALL"]
|
||||
self.decorators["MULTI_SYMBOL"] = self.decorators["SYMBOL"]
|
||||
if drawing_font := bpy.context.scene.DocProperties.drawing_font:
|
||||
drawing_font_path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "fonts", drawing_font)
|
||||
if os.path.exists(drawing_font_path):
|
||||
font_id = blf.load(drawing_font_path)
|
||||
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__())
|
||||
for decorator in self.decorators.values():
|
||||
decorator.font_id = font_id
|
||||
|
||||
|
||||
@@ -276,7 +276,6 @@ class CreateDrawing(bpy.types.Operator):
|
||||
self.svg_writer = svgwriter.SvgWriter()
|
||||
self.svg_writer.human_scale = self.human_scale
|
||||
self.svg_writer.scale = self.scale
|
||||
self.svg_writer.data_dir = context.scene.BIMProperties.data_dir
|
||||
self.svg_writer.camera = self.camera
|
||||
self.svg_writer.camera_width, self.svg_writer.camera_height = self.get_camera_dimensions()
|
||||
self.svg_writer.camera_projection = tuple(
|
||||
@@ -730,7 +729,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(context.scene.BIMProperties.data_dir, "cache", f"{ifc_hash}.h5")
|
||||
ifc_cache_path = os.path.join(context.scene.BIMProperties.cache_dir, f"{ifc_hash}.h5")
|
||||
|
||||
self.serialiser.setFile(ifc)
|
||||
drawing_elements = tool.Drawing.get_drawing_elements(self.camera_element, ifc_file=ifc)
|
||||
@@ -1423,27 +1422,33 @@ class AddAnnotation(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_annotation"
|
||||
bl_label = "Add Annotation"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
object_type: bpy.props.StringProperty()
|
||||
data_type: bpy.props.StringProperty()
|
||||
description: bpy.props.StringProperty()
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return IfcStore.get_file() and context.scene.camera
|
||||
return tool.Ifc.get() and context.scene.camera
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, operator):
|
||||
return operator.description or ""
|
||||
|
||||
def _execute(self, context):
|
||||
drawing = tool.Ifc.get_entity(context.scene.camera)
|
||||
if not drawing:
|
||||
props = bpy.context.scene.BIMAnnotationProperties
|
||||
if not (drawing := tool.Ifc.get_entity(context.scene.camera)):
|
||||
self.report({"WARNING"}, "Not a BIM camera")
|
||||
return
|
||||
|
||||
r = core.add_annotation(tool.Ifc, tool.Collector, tool.Drawing, drawing=drawing, object_type=self.object_type)
|
||||
if isinstance(r, str):
|
||||
self.report({"WARNING"}, r)
|
||||
obj = core.add_annotation(
|
||||
tool.Ifc,
|
||||
tool.Collector,
|
||||
tool.Drawing,
|
||||
drawing=drawing,
|
||||
object_type=props.object_type,
|
||||
relating_type=tool.Ifc.get().by_id(int(props.relating_type_id)) if props.relating_type_id != "0" else None,
|
||||
enable_editing=True,
|
||||
)
|
||||
if props.object_type == "IMAGE":
|
||||
bpy.ops.bim.add_reference_image("INVOKE_DEFAULT", use_existing_object_by_name=obj.name)
|
||||
|
||||
|
||||
class AddSheet(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -1661,7 +1666,6 @@ class AddDrawingToSheet(bpy.types.Operator, tool.Ifc.Operator):
|
||||
)
|
||||
tool.Ifc.run("document.edit_reference", reference=reference, attributes=attributes)
|
||||
sheet_builder = sheeter.SheetBuilder()
|
||||
sheet_builder.data_dir = context.scene.BIMProperties.data_dir
|
||||
sheet_builder.add_drawing(reference, drawing, sheet)
|
||||
|
||||
tool.Drawing.import_sheets()
|
||||
@@ -1691,7 +1695,6 @@ class RemoveDrawingFromSheet(bpy.types.Operator, tool.Ifc.Operator):
|
||||
sheet = tool.Drawing.get_reference_document(reference)
|
||||
|
||||
sheet_builder = sheeter.SheetBuilder()
|
||||
sheet_builder.data_dir = context.scene.BIMProperties.data_dir
|
||||
sheet_builder.remove_drawing(reference, sheet)
|
||||
|
||||
tool.Ifc.run("document.remove_reference", reference=reference)
|
||||
@@ -1754,7 +1757,6 @@ class CreateSheets(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
name = os.path.splitext(os.path.basename(tool.Drawing.get_document_uri(sheet)))[0]
|
||||
sheet_builder = sheeter.SheetBuilder()
|
||||
sheet_builder.data_dir = scene.BIMProperties.data_dir
|
||||
|
||||
references = sheet_builder.build(sheet)
|
||||
raster_references = [tool.Ifc.get_relative_uri(r) for r in references["RASTER"]]
|
||||
@@ -2178,7 +2180,7 @@ class ReloadDrawingStyles(bpy.types.Operator):
|
||||
|
||||
json_path = Path(tool.Ifc.resolve_uri(rel_path))
|
||||
if not json_path.exists():
|
||||
ootb_resource = Path(context.scene.BIMProperties.data_dir) / "assets" / "shading_styles.json"
|
||||
ootb_resource = tool.Blender.get_data_dir_path(Path("assets") / "shading_styles.json")
|
||||
print(
|
||||
f"WARNING. Couldn't find shading_styles for the drawing by the path: {json_path}. "
|
||||
f"Default BBIM resource will be copied from {ootb_resource}"
|
||||
@@ -2576,7 +2578,6 @@ class AddScheduleToSheet(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tool.Ifc.run("document.edit_reference", reference=reference, attributes=attributes)
|
||||
|
||||
sheet_builder = sheeter.SheetBuilder()
|
||||
sheet_builder.data_dir = context.scene.BIMProperties.data_dir
|
||||
sheet_builder.add_document(reference, schedule, sheet)
|
||||
|
||||
tool.Drawing.import_sheets()
|
||||
@@ -2643,7 +2644,6 @@ class AddReferenceToSheet(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tool.Ifc.run("document.edit_reference", reference=reference, attributes=attributes)
|
||||
|
||||
sheet_builder = sheeter.SheetBuilder()
|
||||
sheet_builder.data_dir = context.scene.BIMProperties.data_dir
|
||||
sheet_builder.add_document(reference, extref, sheet)
|
||||
|
||||
tool.Drawing.import_sheets()
|
||||
@@ -3073,7 +3073,6 @@ class EditSheet(bpy.types.Operator, tool.Ifc.Operator):
|
||||
attributes={"Location": tool.Drawing.get_default_titleblock_path(titleblock)},
|
||||
)
|
||||
sheet_builder = sheeter.SheetBuilder()
|
||||
sheet_builder.data_dir = context.scene.BIMProperties.data_dir
|
||||
sheet_builder.change_titleblock(sheet, titleblock)
|
||||
tool.Drawing.import_sheets()
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import bpy
|
||||
import string
|
||||
import svgwrite
|
||||
import openpyxl
|
||||
import bonsai.tool as tool
|
||||
|
||||
from bonsai.bim.module.drawing.svgwriter import SvgWriter
|
||||
from odf.opendocument import load as load_ods
|
||||
@@ -77,7 +78,7 @@ class Scheduler:
|
||||
ifc_file_path = os.path.dirname(IfcStore.path)
|
||||
stylesheet_path = ifc_file_path + "\\" + stylesheet_rel_path
|
||||
if not os.path.exists(stylesheet_path):
|
||||
stylesheet_path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "assets", "schedule.css")
|
||||
stylesheet_path = tool.Blender.get_data_dir_path(Path("assets") / "schedule.css")
|
||||
with open(stylesheet_path, "r") as stylesheet:
|
||||
css = stylesheet.read()
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import urllib.parse
|
||||
import xml.etree.ElementTree as ET
|
||||
import bonsai.tool as tool
|
||||
import ifcopenshell.util.geolocation
|
||||
from pathlib import Path
|
||||
from xml.dom import minidom
|
||||
from mathutils import Vector
|
||||
import re
|
||||
@@ -38,7 +39,6 @@ XLINK = "{http://www.w3.org/1999/xlink}"
|
||||
|
||||
class SheetBuilder:
|
||||
def __init__(self):
|
||||
self.data_dir = None
|
||||
self.scale = "NTS"
|
||||
|
||||
def create(self, layout_path: str, titleblock_name: str) -> None:
|
||||
@@ -49,7 +49,9 @@ class SheetBuilder:
|
||||
root.attrib["version"] = "1.1"
|
||||
|
||||
sheet_dir = os.path.dirname(layout_path)
|
||||
ootb_titleblock_path = os.path.join(self.data_dir, "templates", "titleblocks", titleblock_name + ".svg")
|
||||
ootb_titleblock_path = tool.Blender.get_data_dir_path(
|
||||
Path("templates") / "titleblock" / (titleblock_name + ".svg")
|
||||
)
|
||||
titleblock_path = tool.Ifc.resolve_uri(tool.Drawing.get_default_titleblock_path(titleblock_name))
|
||||
|
||||
os.makedirs(sheet_dir, exist_ok=True)
|
||||
@@ -241,7 +243,7 @@ class SheetBuilder:
|
||||
title_path = os.path.join(layout_dir, "assets", "view-title.svg")
|
||||
os.makedirs(os.path.dirname(title_path), exist_ok=True)
|
||||
if not os.path.exists(title_path):
|
||||
ootb_title = os.path.join(bpy.context.scene.BIMProperties.data_dir, "assets", "view-title.svg")
|
||||
ootb_title = tool.Blender.get_data_dir_path(Path("assets") / "view-title.svg")
|
||||
shutil.copy(ootb_title, title_path)
|
||||
|
||||
title_tree = ET.parse(title_path)
|
||||
@@ -492,7 +494,9 @@ class SheetBuilder:
|
||||
return group
|
||||
|
||||
def change_titleblock(self, sheet: ifcopenshell.entity_instance, titleblock_name: str) -> None:
|
||||
ootb_titleblock_path = os.path.join(self.data_dir, "templates", "titleblocks", titleblock_name + ".svg")
|
||||
ootb_titleblock_path = tool.Blender.get_data_dir_path(
|
||||
Path("templates") / "titleblocks" / (titleblock_name + ".svg")
|
||||
)
|
||||
titleblock_path = tool.Ifc.resolve_uri(tool.Drawing.get_default_titleblock_path(titleblock_name))
|
||||
sheet_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
|
||||
sheet_dir = os.path.dirname(sheet_path)
|
||||
|
||||
@@ -38,6 +38,7 @@ 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
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class External(svgwrite.container.Group):
|
||||
@@ -98,7 +99,7 @@ class SvgWriter:
|
||||
os.makedirs(os.path.dirname(resource_path), exist_ok=True)
|
||||
if not os.path.exists(resource_path):
|
||||
resource_basename = os.path.basename(resource_path)
|
||||
ootb_resource = os.path.join(bpy.context.scene.BIMProperties.data_dir, "assets", resource_basename)
|
||||
ootb_resource = tool.Blender.get_data_dir_path(Path("assets") / resource_basename)
|
||||
print(
|
||||
f"WARNING. Couldn't find {resource} for the drawing by the path: {resource_path}. Default BBIM resource will be copied from {ootb_resource}"
|
||||
)
|
||||
|
||||
@@ -20,7 +20,9 @@
|
||||
import os
|
||||
import bpy
|
||||
import bonsai.core.type
|
||||
import bonsai.core.drawing as core
|
||||
import bonsai.tool as tool
|
||||
import ifcopenshell.util.representation
|
||||
from bonsai.bim.module.drawing.data import DecoratorData, AnnotationData
|
||||
from bonsai.bim.helper import prop_with_search
|
||||
from bpy.types import WorkSpaceTool
|
||||
@@ -161,12 +163,12 @@ def create_annotation_occurrence(context):
|
||||
obj = tool.Drawing.create_annotation_object(drawing, object_type)
|
||||
obj.name = relating_type.Name
|
||||
ifc_context = tool.Drawing.get_annotation_context(tool.Drawing.get_drawing_target_view(drawing), object_type)
|
||||
relating_type_repr = tool.Drawing.get_annotation_representation(relating_type)
|
||||
relating_type_rep = tool.Drawing.get_annotation_representation(relating_type)
|
||||
element = tool.Drawing.run_root_assign_class(
|
||||
obj=obj,
|
||||
ifc_class="IfcAnnotation",
|
||||
predefined_type=object_type,
|
||||
should_add_representation=not bool(relating_type_repr),
|
||||
should_add_representation=not relating_type_rep,
|
||||
context=ifc_context,
|
||||
ifc_representation_class=tool.Drawing.get_ifc_representation_class(object_type),
|
||||
)
|
||||
@@ -175,27 +177,23 @@ def create_annotation_occurrence(context):
|
||||
|
||||
tool.Ifc.run("group.assign_group", group=tool.Drawing.get_drawing_group(drawing), products=[element])
|
||||
tool.Collector.assign(obj)
|
||||
tool.Blender.select_and_activate_single_object(context, obj)
|
||||
|
||||
if relating_type_repr is None and props.object_type == "IMAGE":
|
||||
if relating_type_rep is None and props.object_type == "IMAGE":
|
||||
bpy.ops.bim.add_reference_image("INVOKE_DEFAULT", use_existing_object_by_name=obj.name)
|
||||
|
||||
|
||||
def create_annotation():
|
||||
props = bpy.context.scene.BIMAnnotationProperties
|
||||
if props.relating_type_id != "0":
|
||||
create_annotation_occurrence(bpy.context)
|
||||
else:
|
||||
object_type = props.object_type
|
||||
if not bpy.ops.bim.add_annotation.poll():
|
||||
return
|
||||
bpy.ops.bim.add_annotation(
|
||||
object_type=object_type, data_type=tool.Drawing.ANNOTATION_TYPES_DATA[object_type][-1]
|
||||
if representation := ifcopenshell.util.representation.get_representation(element, ifc_context):
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
if props.object_type == "IMAGE":
|
||||
bpy.ops.bim.add_reference_image(
|
||||
"INVOKE_DEFAULT", use_existing_object_by_name=bpy.context.active_object.name
|
||||
)
|
||||
|
||||
if obj.data and not relating_type_rep:
|
||||
tool.Drawing.enable_editing(obj)
|
||||
|
||||
|
||||
class AnnotationToolUI:
|
||||
@@ -294,21 +292,30 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
def hotkey_S_T(self):
|
||||
props = bpy.context.scene.BIMAnnotationProperties
|
||||
annotation_type = props.object_type
|
||||
object_type = props.object_type
|
||||
|
||||
if annotation_type not in tool.Drawing.ANNOTATION_TYPES_SUPPORT_SETUP:
|
||||
self.report({"ERROR"}, f"Annotation type {annotation_type} is not supported for tagging.")
|
||||
if object_type not in tool.Drawing.ANNOTATION_TYPES_SUPPORT_SETUP:
|
||||
self.report({"ERROR"}, f"Annotation type {object_type} is not supported for tagging.")
|
||||
return
|
||||
|
||||
related_objects = bpy.context.selected_objects
|
||||
for related_object in related_objects:
|
||||
create_annotation()
|
||||
obj = bpy.context.active_object
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
tool.Drawing.setup_annotation_object(obj, annotation_type, related_object)
|
||||
obj = core.add_annotation(
|
||||
tool.Ifc,
|
||||
tool.Collector,
|
||||
tool.Drawing,
|
||||
drawing=tool.Ifc.get_entity(bpy.context.scene.camera),
|
||||
object_type=object_type,
|
||||
relating_type=(
|
||||
tool.Ifc.get().by_id(int(props.relating_type_id)) if props.relating_type_id != "0" else None
|
||||
),
|
||||
enable_editing=False,
|
||||
)
|
||||
tool.Drawing.setup_annotation_object(obj, object_type, related_object)
|
||||
|
||||
def hotkey_S_A(self):
|
||||
create_annotation()
|
||||
if bpy.ops.bim.add_annotation.poll():
|
||||
bpy.ops.bim.add_annotation()
|
||||
|
||||
def hotkey_S_E(self):
|
||||
if not bpy.context.active_object:
|
||||
|
||||
@@ -18,9 +18,11 @@
|
||||
|
||||
import bpy
|
||||
from . import ui, prop, operator
|
||||
from bpy.app.handlers import persistent
|
||||
|
||||
classes = (
|
||||
operator.AddCurvelikeItem,
|
||||
operator.AddHalfSpaceSolidItem,
|
||||
operator.AddMeshlikeItem,
|
||||
operator.AddRepresentation,
|
||||
operator.AddSweptAreaSolidItem,
|
||||
@@ -50,6 +52,8 @@ classes = (
|
||||
operator.OverrideMeshSeparate,
|
||||
operator.OverrideModeSetEdit,
|
||||
operator.OverrideModeSetObject,
|
||||
operator.OverrideMove,
|
||||
operator.OverrideMoveMacro,
|
||||
operator.OverrideOriginSet,
|
||||
operator.OverrideOutlinerDelete,
|
||||
operator.OverridePasteBuffer,
|
||||
@@ -87,13 +91,28 @@ classes = (
|
||||
addon_keymaps = []
|
||||
|
||||
|
||||
@persistent
|
||||
def block_scale(scene):
|
||||
if obj := (getattr(bpy.context, "active_object", None) or bpy.context.view_layer.objects.active):
|
||||
if isinstance(obj, bpy.types.Object) and obj.BIMObjectProperties.ifc_definition_id:
|
||||
if obj.scale != (1, 1, 1):
|
||||
obj.scale = (1, 1, 1)
|
||||
elif isinstance(obj, bpy.types.Mesh) and obj.BIMMeshProperties.ifc_definition_id:
|
||||
if obj.scale != (1, 1, 1):
|
||||
obj.scale = (1, 1, 1)
|
||||
|
||||
|
||||
def register():
|
||||
bpy.app.handlers.depsgraph_update_pre.append(block_scale)
|
||||
|
||||
operator.OverrideDuplicateMoveMacro.define("BIM_OT_override_object_duplicate_move")
|
||||
operator.OverrideDuplicateMoveMacro.define("TRANSFORM_OT_translate")
|
||||
operator.OverrideDuplicateMoveLinkedMacro.define("BIM_OT_override_object_duplicate_move_linked")
|
||||
operator.OverrideDuplicateMoveLinkedMacro.define("TRANSFORM_OT_translate")
|
||||
operator.DuplicateMoveLinkedAggregateMacro.define("BIM_OT_object_duplicate_move_linked_aggregate")
|
||||
operator.DuplicateMoveLinkedAggregateMacro.define("TRANSFORM_OT_translate")
|
||||
operator.OverrideMoveMacro.define("BIM_OT_override_move")
|
||||
operator.OverrideMoveMacro.define("TRANSFORM_OT_translate")
|
||||
|
||||
bpy.types.Object.BIMGeometryProperties = bpy.props.PointerProperty(type=prop.BIMObjectGeometryProperties)
|
||||
bpy.types.Scene.BIMGeometryProperties = bpy.props.PointerProperty(type=prop.BIMGeometryProperties)
|
||||
@@ -116,6 +135,8 @@ def register():
|
||||
"bim.object_duplicate_move_linked_aggregate_macro", "D", "PRESS", ctrl=True, shift=True
|
||||
)
|
||||
addon_keymaps.append((km, kmi))
|
||||
kmi = km.keymap_items.new("bim.override_move_macro", "G", "PRESS")
|
||||
addon_keymaps.append((km, kmi))
|
||||
kmi = km.keymap_items.new("bim.override_paste_buffer", "V", "PRESS", ctrl=True)
|
||||
addon_keymaps.append((km, kmi))
|
||||
kmi = km.keymap_items.new("bim.override_mode_set_edit", "TAB", "PRESS")
|
||||
@@ -138,6 +159,10 @@ def register():
|
||||
kmi = km.keymap_items.new("bim.override_mode_set_object", "TAB", "PRESS")
|
||||
addon_keymaps.append((km, kmi))
|
||||
|
||||
km = wm.keyconfigs.addon.keymaps.new(name="3D View", space_type="VIEW_3D")
|
||||
kmi = km.keymap_items.new("bim.override_escape", "ESC", "PRESS")
|
||||
addon_keymaps.append((km, kmi))
|
||||
|
||||
km = wm.keyconfigs.addon.keymaps.new(name="Outliner", space_type="OUTLINER")
|
||||
kmi = km.keymap_items.new("bim.override_paste_buffer", "V", "PRESS", ctrl=True)
|
||||
addon_keymaps.append((km, kmi))
|
||||
@@ -148,6 +173,8 @@ def register():
|
||||
|
||||
|
||||
def unregister():
|
||||
bpy.app.handlers.depsgraph_update_pre.remove(block_scale)
|
||||
|
||||
bpy.types.VIEW3D_MT_object.remove(ui.object_menu)
|
||||
bpy.types.OUTLINER_MT_object.remove(ui.outliner_menu)
|
||||
bpy.types.VIEW3D_MT_object_context_menu.remove(ui.outliner_menu)
|
||||
|
||||
@@ -78,9 +78,6 @@ class ViewportData:
|
||||
modes.append(edit_mode)
|
||||
elif element.is_a("IfcGridAxis"):
|
||||
modes.append(edit_mode)
|
||||
elif tool.Blender.Modifier.is_editing_parameters(obj):
|
||||
# This should go BEFORE the modifiers
|
||||
pass
|
||||
elif tool.Blender.Modifier.is_roof(element):
|
||||
modes.append(edit_mode)
|
||||
elif tool.Blender.Modifier.is_railing(element):
|
||||
|
||||
@@ -36,15 +36,19 @@ class ItemDecorator:
|
||||
if cls.is_installed:
|
||||
cls.uninstall()
|
||||
obj_is_selected = {}
|
||||
obj_is_boolean = {}
|
||||
objs = {}
|
||||
for item_obj in context.scene.BIMGeometryProperties.item_objs:
|
||||
if obj := item_obj.obj:
|
||||
objs[obj.name] = cls.get_obj_data(obj)
|
||||
obj_is_selected[obj.name] = obj.select_get()
|
||||
item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
|
||||
obj_is_boolean[obj.name] = [i for i in tool.Ifc.get().get_inverse(item) if i.is_a("IfcBooleanResult")]
|
||||
|
||||
handler = cls()
|
||||
handler.objs = objs
|
||||
handler.obj_is_selected = obj_is_selected
|
||||
handler.obj_is_boolean = obj_is_boolean
|
||||
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_text, (context,), "WINDOW", "POST_PIXEL"))
|
||||
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw, (context,), "WINDOW", "POST_VIEW"))
|
||||
cls.is_installed = True
|
||||
@@ -54,6 +58,8 @@ class ItemDecorator:
|
||||
verts = []
|
||||
edges = []
|
||||
tris = []
|
||||
special_verts = []
|
||||
special_edges = []
|
||||
|
||||
if len(obj.data.loop_triangles) > 0:
|
||||
verts = [tuple(obj.matrix_world @ v.co) for v in obj.data.vertices]
|
||||
@@ -78,7 +84,26 @@ class ItemDecorator:
|
||||
]
|
||||
edges = bbox_edges
|
||||
verts.extend(bbox_verts)
|
||||
return {"verts": verts, "edges": edges, "tris": tris}
|
||||
|
||||
if "HalfSpaceSolid" in obj.name:
|
||||
# Arrow shape
|
||||
special_verts = [
|
||||
tuple(obj.matrix_world @ Vector((0, 0, 0))),
|
||||
tuple(obj.matrix_world @ Vector((0, 0, 0.5))),
|
||||
tuple(obj.matrix_world @ Vector((0.05, 0, 0.45))),
|
||||
tuple(obj.matrix_world @ Vector((-0.05, 0, 0.45))),
|
||||
tuple(obj.matrix_world @ Vector((0, 0.05, 0.45))),
|
||||
tuple(obj.matrix_world @ Vector((0, -0.05, 0.45))),
|
||||
]
|
||||
special_edges = [(0, 1), (1, 2), (1, 3), (1, 4), (1, 5)]
|
||||
|
||||
return {
|
||||
"verts": verts,
|
||||
"edges": edges,
|
||||
"tris": tris,
|
||||
"special_verts": special_verts,
|
||||
"special_edges": special_edges,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls):
|
||||
@@ -138,7 +163,7 @@ class ItemDecorator:
|
||||
self.line_shader.bind() # required to be able to change uniforms of the shader
|
||||
# POLYLINE_UNIFORM_COLOR specific uniforms
|
||||
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
|
||||
self.line_shader.uniform_float("lineWidth", 1.0)
|
||||
self.line_shader.uniform_float("lineWidth", 2.0)
|
||||
|
||||
# general shader
|
||||
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
@@ -154,6 +179,14 @@ class ItemDecorator:
|
||||
continue
|
||||
self.draw_batch("LINES", data["verts"], selected_elements_color, data["edges"])
|
||||
self.draw_batch("TRIS", data["verts"], transparent_color(selected_elements_color), data["tris"])
|
||||
else:
|
||||
self.draw_batch("LINES", data["verts"], decorator_color_background, data["edges"])
|
||||
self.draw_batch("LINES", data["special_verts"], selected_elements_color, data["special_edges"])
|
||||
elif self.obj_is_boolean[obj_name]:
|
||||
self.draw_batch("LINES", data["verts"], special_elements_color, data["edges"])
|
||||
self.draw_batch("TRIS", data["verts"], transparent_color(special_elements_color), data["tris"])
|
||||
self.draw_batch("LINES", data["special_verts"], special_elements_color, data["special_edges"])
|
||||
else:
|
||||
self.draw_batch(
|
||||
"LINES", data["verts"], transparent_color(unselected_elements_color, alpha=0.2), data["edges"]
|
||||
)
|
||||
self.draw_batch("TRIS", data["verts"], transparent_color(special_elements_color), data["tris"])
|
||||
self.draw_batch("LINES", data["special_verts"], special_elements_color, data["special_edges"])
|
||||
|
||||
@@ -454,7 +454,7 @@ class Helper:
|
||||
x_axis = (mesh.vertices[loop[0]].co - center).normalized()
|
||||
else:
|
||||
x_axis = (mesh.vertices[loop[1]].co - mesh.vertices[loop[0]].co).normalized()
|
||||
z_axis = profile_face.polygons[0].normal.normalized()
|
||||
z_axis = profile_face.polygons[0].normal.normalized() * -1
|
||||
y_axis = z_axis.cross(x_axis).normalized()
|
||||
matrix = Matrix((x_axis, y_axis, z_axis))
|
||||
matrix.normalize()
|
||||
|
||||
@@ -983,7 +983,9 @@ class OverrideDuplicateMove(bpy.types.Operator):
|
||||
OverrideDuplicateMove.duplicate_item(self, obj)
|
||||
continue
|
||||
|
||||
linked_non_ifc_object = linked and not element
|
||||
tracked_opening_type = tool.Model.get_tracked_opening_type(obj)
|
||||
is_tracked_opening = bool(tracked_opening_type)
|
||||
keep_data_linked = linked and not element and not is_tracked_opening
|
||||
|
||||
# Prior to duplicating, sync the object placement to make decomposition recreation more stable.
|
||||
if tool.Ifc.is_moved(obj):
|
||||
@@ -1000,11 +1002,17 @@ class OverrideDuplicateMove(bpy.types.Operator):
|
||||
if tool.Ifc.is_edited(obj, ignore_scale=True):
|
||||
tool.Ifc.edit(new_obj)
|
||||
|
||||
if obj.data and not linked_non_ifc_object:
|
||||
if obj.data and not keep_data_linked:
|
||||
# assure root.copy_class won't replace the previous mesh globally
|
||||
temp_data = obj.data.copy()
|
||||
new_obj.data = temp_data
|
||||
|
||||
# Unlink from previous boolean element
|
||||
# and keep object tracked for decorations.
|
||||
if is_tracked_opening:
|
||||
new_obj.data.BIMMeshProperties.ifc_boolean_id = 0
|
||||
tool.Root.add_tracked_opening(new_obj, tracked_opening_type)
|
||||
|
||||
if obj == context.active_object:
|
||||
self.new_active_obj = new_obj
|
||||
for collection in obj.users_collection:
|
||||
@@ -1012,7 +1020,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
|
||||
obj.select_set(False)
|
||||
new_obj.select_set(True)
|
||||
|
||||
if linked_non_ifc_object:
|
||||
if not element:
|
||||
continue
|
||||
|
||||
# clear object's collection so it will be able to have it's own
|
||||
@@ -1846,19 +1854,22 @@ class OverridePasteBuffer(bpy.types.Operator):
|
||||
class OverrideEscape(bpy.types.Operator):
|
||||
bl_idname = "bim.override_escape"
|
||||
bl_label = "Override Escape"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
self.report({"INFO"}, "Operator executed")
|
||||
return {"FINISHED"}
|
||||
|
||||
def modal(self, context, event):
|
||||
if event.type == "ESC" and context.scene.BIMGeometryProperties.mode == "ITEM":
|
||||
if context.scene.BIMGeometryProperties.mode == "ITEM":
|
||||
tool.Geometry.disable_item_mode()
|
||||
return {"PASS_THROUGH"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.modal_handler_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
elif context.scene.BIMGeometryProperties.mode == "EDIT":
|
||||
bpy.ops.bim.override_mode_set_object("INVOKE_DEFAULT", should_save=False)
|
||||
tool.Geometry.disable_item_mode()
|
||||
elif context.scene.BIMModelProperties.openings:
|
||||
bpy.ops.bim.hide_all_openings()
|
||||
elif context.scene.BIMAggregateProperties.in_aggregate_mode:
|
||||
bpy.ops.bim.disable_aggregate_mode()
|
||||
elif active_object := context.active_object:
|
||||
if tool.Blender.Modifier.try_canceling_editing_modifier_parameters_or_path(active_object):
|
||||
pass
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -1878,9 +1889,25 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
|
||||
def _execute(self, context):
|
||||
selected_objs = context.selected_objects # Purposely exclude active object
|
||||
|
||||
if self.has_aggregates(selected_objs):
|
||||
if not context.scene.BIMAggregateProperties.in_aggregate_mode:
|
||||
bonsai.core.aggregate.enable_aggregate_mode(tool.Aggregate, context.active_object)
|
||||
return {"FINISHED"}
|
||||
|
||||
if self.has_nests(selected_objs):
|
||||
if not context.scene.BIMNestProperties.in_nest_mode:
|
||||
bonsai.core.nest.enable_nest_mode(tool.Nest, context.active_object)
|
||||
return {"FINISHED"}
|
||||
|
||||
if len(selected_objs) == 1 and context.active_object == selected_objs[0]:
|
||||
self.handle_single_object(context, context.active_object)
|
||||
elif len(selected_objs) == 0:
|
||||
if context.scene.BIMAggregateProperties.in_aggregate_mode:
|
||||
bonsai.core.aggregate.disable_aggregate_mode(tool.Aggregate)
|
||||
return {"FINISHED"}
|
||||
if context.scene.BIMNestProperties.in_nest_mode:
|
||||
bonsai.core.nest.disable_nest_mode(tool.Nest)
|
||||
return {"FINISHED"}
|
||||
tool.Geometry.disable_item_mode()
|
||||
elif len(selected_objs) > 1:
|
||||
self.handle_multiple_selected_objects(context)
|
||||
@@ -1889,6 +1916,11 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if obj == context.scene.BIMGeometryProperties.representation_obj:
|
||||
self.report({"ERROR"}, f"Element '{obj.name}' is in item mode and cannot be edited directly")
|
||||
elif obj in [o.obj for o in context.scene.BIMAggregateProperties.not_editing_objects]:
|
||||
obj.select_set(False)
|
||||
self.report(
|
||||
{"ERROR"}, f"Element '{obj.name}' does not belong to this aggregate and cannot be edited directly"
|
||||
)
|
||||
elif obj in bpy.context.scene.BIMProjectProperties.clipping_planes_objs:
|
||||
self.report({"ERROR"}, "Clipping planes cannot be edited")
|
||||
elif element:
|
||||
@@ -1908,13 +1940,8 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bpy.ops.bim.enable_editing_boundary_geometry()
|
||||
elif element.is_a("IfcGridAxis"):
|
||||
self.enable_edit_mode(context)
|
||||
elif tool.Blender.Modifier.is_editing_parameters(obj):
|
||||
# This should go BEFORE the modifiers
|
||||
self.report({"INFO"}, "Can't edit while modifier parameters are being modified")
|
||||
elif tool.Blender.Modifier.is_roof(element):
|
||||
bpy.ops.bim.enable_editing_roof_path()
|
||||
elif tool.Blender.Modifier.is_railing(element):
|
||||
bpy.ops.bim.enable_editing_railing_path()
|
||||
elif tool.Blender.Modifier.try_applying_edit_mode(obj, element):
|
||||
pass
|
||||
else:
|
||||
bpy.ops.bim.import_representation_items()
|
||||
elif tool.Geometry.is_representation_item(obj):
|
||||
@@ -1962,6 +1989,13 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
|
||||
ProfileDecorator.install(context)
|
||||
if not bpy.app.background:
|
||||
tool.Blender.set_viewport_tool("bim.cad_tool")
|
||||
elif item.is_a("IfcAnnotationFillArea"):
|
||||
tool.Model.import_annotation_fill_area(item, obj=obj)
|
||||
obj.data.BIMMeshProperties.ifc_definition_id = item.id()
|
||||
self.enable_edit_mode(context)
|
||||
ProfileDecorator.install(context)
|
||||
if not bpy.app.background:
|
||||
tool.Blender.set_viewport_tool("bim.cad_tool")
|
||||
elif tool.Geometry.is_curvelike_item(item):
|
||||
tool.Model.import_curve(item, obj=obj)
|
||||
obj.data.BIMMeshProperties.ifc_definition_id = item.id()
|
||||
@@ -1980,6 +2014,30 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
|
||||
context.scene.BIMGeometryProperties.mode = "EDIT"
|
||||
context.scene.BIMGeometryProperties.is_changing_mode = False
|
||||
|
||||
def has_aggregates(self, objs):
|
||||
for obj in objs:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
aggregate = ifcopenshell.util.element.get_aggregate(element)
|
||||
parts = ifcopenshell.util.element.get_parts(element)
|
||||
if (aggregate or parts) and not bpy.context.scene.BIMAggregateProperties.in_aggregate_mode:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def has_nests(self, objs):
|
||||
for obj in objs:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
nest = ifcopenshell.util.element.get_nest(element)
|
||||
components = ifcopenshell.util.element.get_components(element)
|
||||
if (nest or components) and not bpy.context.scene.BIMNestProperties.in_nest_mode:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_description = "Switch from Edit to Item or Object mode"
|
||||
@@ -2010,7 +2068,7 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
|
||||
context.scene.BIMGeometryProperties.mode = "OBJECT"
|
||||
context.scene.BIMGeometryProperties.is_changing_mode = False
|
||||
|
||||
if context.active_object:
|
||||
if context.active_object and self.should_save:
|
||||
element = tool.Ifc.get_entity(context.active_object)
|
||||
if element and element.is_a("IfcRelSpaceBoundary"):
|
||||
return bpy.ops.bim.edit_boundary_geometry()
|
||||
@@ -2103,10 +2161,7 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tool.Geometry.import_item(obj)
|
||||
elif item.is_a("IfcSweptAreaSolid"):
|
||||
ProfileDecorator.uninstall()
|
||||
|
||||
profile = tool.Model.export_profile(obj)
|
||||
|
||||
if not profile:
|
||||
if not (profile := tool.Model.export_profile(obj)):
|
||||
|
||||
def msg(self, context):
|
||||
self.layout.label(text="INVALID PROFILE")
|
||||
@@ -2163,6 +2218,26 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
|
||||
product=element,
|
||||
representation=new_footprint,
|
||||
)
|
||||
elif item.is_a("IfcAnnotationFillArea"):
|
||||
ProfileDecorator.uninstall()
|
||||
if not (profile := tool.Model.export_annotation_fill_area(obj)):
|
||||
|
||||
def msg(self, context):
|
||||
self.layout.label(text="INVALID PROFILE")
|
||||
|
||||
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
|
||||
ProfileDecorator.install(bpy.context)
|
||||
self.enable_edit_mode(bpy.context)
|
||||
return
|
||||
|
||||
for inverse in tool.Ifc.get().get_inverse(item):
|
||||
ifcopenshell.util.element.replace_attribute(inverse, item, profile)
|
||||
ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), item)
|
||||
obj.data.BIMMeshProperties.ifc_definition_id = profile.id()
|
||||
|
||||
tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj)
|
||||
tool.Geometry.import_item(obj)
|
||||
tool.Geometry.import_item_attributes(obj)
|
||||
elif tool.Geometry.is_curvelike_item(item):
|
||||
ProfileDecorator.uninstall()
|
||||
new = tool.Model.export_curves(obj)
|
||||
@@ -2220,7 +2295,7 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class FlipObject(bpy.types.Operator):
|
||||
bl_idname = "bim.flip_object"
|
||||
bl_label = "Flip Object"
|
||||
bl_description = "Flip object's local axes, keep the position"
|
||||
bl_description = "Flip Element about its local axes, keep the position"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
flip_local_axes: bpy.props.EnumProperty(
|
||||
@@ -2639,8 +2714,19 @@ class ImportRepresentationItems(bpy.types.Operator, tool.Ifc.Operator):
|
||||
data = obj.data
|
||||
assert data
|
||||
item_ids = data["ios_item_ids"] if "ios_item_ids" in data else data["ios_edges_item_ids"]
|
||||
for item_id in set(item_ids):
|
||||
queue = list(set(item_ids))
|
||||
processed_ids = set()
|
||||
boolean_ids = set()
|
||||
while queue:
|
||||
item_id = queue.pop()
|
||||
if item_id in processed_ids:
|
||||
continue # In theory, an item can be used multiple times (e.g. in a boolean stack).
|
||||
item = tool.Ifc.get().by_id(item_id)
|
||||
if item.is_a("IfcBooleanResult"):
|
||||
queue.append(item.FirstOperand.id())
|
||||
queue.append(item.SecondOperand.id())
|
||||
boolean_ids.add(item.SecondOperand.id())
|
||||
continue
|
||||
item_mesh = bpy.data.meshes.new(f"Item/{item.is_a()}/{item_id}")
|
||||
item_mesh.BIMMeshProperties.ifc_definition_id = item_id
|
||||
|
||||
@@ -2657,11 +2743,9 @@ class ImportRepresentationItems(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if not tool.Geometry.is_movable(item):
|
||||
tool.Geometry.lock_object(item_obj)
|
||||
|
||||
if "IfcOpeningElement" in obj.name:
|
||||
item_obj.display_type = "WIRE"
|
||||
|
||||
item_obj.select_set(True) # so you can quickly hit tab again, for edit mode
|
||||
context.view_layer.objects.active = item_obj
|
||||
processed_ids.add(item_id)
|
||||
|
||||
tool.Root.reload_item_decorator()
|
||||
|
||||
@@ -2719,8 +2803,6 @@ class AddMeshlikeItem(bpy.types.Operator, tool.Ifc.Operator):
|
||||
obj.show_in_front = True
|
||||
new = props.item_objs.add()
|
||||
new.obj = obj
|
||||
tool.Root.reload_item_decorator()
|
||||
|
||||
tool.Geometry.lock_object(obj)
|
||||
|
||||
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
|
||||
@@ -2744,6 +2826,7 @@ class AddMeshlikeItem(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj)
|
||||
obj.name = obj.data.name = f"Item/{item.is_a()}/{item.id()}"
|
||||
obj.data.BIMMeshProperties.ifc_definition_id = item.id()
|
||||
tool.Root.reload_item_decorator()
|
||||
|
||||
|
||||
class AddSweptAreaSolidItem(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -2795,6 +2878,7 @@ class AddSweptAreaSolidItem(bpy.types.Operator, tool.Ifc.Operator):
|
||||
obj.data.BIMMeshProperties.ifc_definition_id = item.id()
|
||||
tool.Geometry.import_item(obj)
|
||||
tool.Geometry.import_item_attributes(obj)
|
||||
tool.Root.reload_item_decorator()
|
||||
|
||||
|
||||
class AddCurvelikeItem(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -2866,3 +2950,176 @@ class AddCurvelikeItem(bpy.types.Operator, tool.Ifc.Operator):
|
||||
obj.data.BIMMeshProperties.ifc_definition_id = item.id()
|
||||
tool.Geometry.import_item(obj)
|
||||
tool.Geometry.import_item_attributes(obj)
|
||||
|
||||
|
||||
class AddHalfSpaceSolidItem(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_half_space_solid_item"
|
||||
bl_label = "Add Half Space Solid Item"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
if (
|
||||
not tool.Blender.get_selected_objects()
|
||||
or not (active_obj := tool.Blender.get_active_object())
|
||||
or not tool.Geometry.is_representation_item(active_obj)
|
||||
):
|
||||
# In theory we can have half space solid as a top level item but let's not go there today.
|
||||
self.report({"ERROR"}, "Select an item to apply the half space solid to.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
props = context.scene.BIMGeometryProperties
|
||||
mesh = bpy.data.meshes.new("Tmp")
|
||||
obj = bpy.data.objects.new("Tmp", mesh)
|
||||
scene = bpy.context.scene
|
||||
scene.collection.objects.link(obj)
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
obj.select_set(True)
|
||||
new = props.item_objs.add()
|
||||
new.obj = obj
|
||||
|
||||
matrix = props.representation_obj.matrix_world.copy()
|
||||
matrix.translation = context.scene.cursor.location
|
||||
local_matrix = props.representation_obj.matrix_world.inverted() @ matrix
|
||||
|
||||
obj.show_in_front = True
|
||||
obj.matrix_world = matrix
|
||||
tool.Geometry.record_object_position(obj)
|
||||
|
||||
ifc_file = tool.Ifc.get()
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
|
||||
local_z = ifc_file.createIfcDirection((0.0, 0.0, 1.0))
|
||||
local_x = ifc_file.createIfcDirection((1.0, 0.0, 0.0))
|
||||
point = ifc_file.createIfcCartesianPoint((np.array(local_matrix.translation) / unit_scale).tolist())
|
||||
placement = ifc_file.createIfcAxis2Placement3D(point, local_z, local_x)
|
||||
plane = ifc_file.createIfcPlane(placement)
|
||||
item = ifc_file.createIfcHalfSpaceSolid(plane, AgreementFlag=False)
|
||||
|
||||
representation = tool.Geometry.get_active_representation(props.representation_obj)
|
||||
representation = ifcopenshell.util.representation.resolve_representation(representation)
|
||||
|
||||
representation.Items = list(representation.Items) + [item]
|
||||
tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj)
|
||||
|
||||
obj.name = obj.data.name = f"Item/{item.is_a()}/{item.id()}"
|
||||
obj.data.BIMMeshProperties.ifc_definition_id = item.id()
|
||||
tool.Geometry.import_item(obj)
|
||||
|
||||
# TODO refactor to core and not rely on selection
|
||||
tool.Blender.select_and_activate_single_object(context, active_obj)
|
||||
tool.Blender.select_object(obj)
|
||||
bpy.ops.bim.add_boolean()
|
||||
|
||||
|
||||
class OverrideMoveMacro(bpy.types.Macro):
|
||||
bl_idname = "bim.override_move_macro"
|
||||
bl_label = "IFC Move Aggregate"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
|
||||
class OverrideMove(bpy.types.Operator):
|
||||
bl_idname = "bim.override_move"
|
||||
bl_label = "IFC Move"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return len(context.selected_objects) > 0
|
||||
|
||||
def execute(self, context):
|
||||
# Deep magick from the dawn of time
|
||||
if IfcStore.get_file():
|
||||
IfcStore.execute_ifc_operator(self, context)
|
||||
if self.new_active_obj:
|
||||
context.view_layer.objects.active = self.new_active_obj
|
||||
return {"FINISHED"}
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
def _execute(self, context):
|
||||
# Get filling objects
|
||||
selection = []
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
selection.append(obj)
|
||||
for rel in getattr(element, "HasOpenings", []) or []:
|
||||
opening = rel.RelatedOpeningElement
|
||||
for rel2 in getattr(opening, "HasFillings", []) or []:
|
||||
filling = rel2.RelatedBuildingElement
|
||||
selection.append(tool.Ifc.get_object(filling))
|
||||
for obj in selection:
|
||||
obj.select_set(True)
|
||||
self.new_active_obj = obj
|
||||
|
||||
# Get aggregates
|
||||
props = context.scene.BIMAggregateProperties
|
||||
not_editing_objs = [o.obj for o in props.not_editing_objects]
|
||||
aggregates_to_move = []
|
||||
for obj in context.selected_objects:
|
||||
self.new_active_obj = None
|
||||
if obj in not_editing_objs:
|
||||
obj.select_set(False)
|
||||
continue
|
||||
if obj == props.editing_aggregate:
|
||||
continue
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not element.is_a("IfcElement"):
|
||||
continue
|
||||
parts = ifcopenshell.util.element.get_parts(element)
|
||||
if parts:
|
||||
aggregates_to_move.append(tool.Ifc.get_object(element))
|
||||
continue
|
||||
if not parts and props.in_aggregate_mode:
|
||||
continue
|
||||
aggregate = ifcopenshell.util.element.get_aggregate(element)
|
||||
if aggregate:
|
||||
aggregates_to_move.append(tool.Ifc.get_object(aggregate))
|
||||
obj.select_set(False)
|
||||
aggregates_to_move = set(aggregates_to_move)
|
||||
|
||||
if aggregates_to_move:
|
||||
for obj in aggregates_to_move:
|
||||
obj.select_set(True)
|
||||
for part in tool.Aggregate.get_parts_recursively(tool.Ifc.get_entity(obj)):
|
||||
part_obj = tool.Ifc.get_object(part)
|
||||
part_obj.select_set(True)
|
||||
self.new_active_obj = obj
|
||||
return {"FINISHED"}
|
||||
|
||||
# Get nests
|
||||
props = context.scene.BIMNestProperties
|
||||
not_editing_objs = [o.obj for o in props.not_editing_objects]
|
||||
nests_to_move = []
|
||||
for obj in context.selected_objects:
|
||||
self.new_active_obj = None
|
||||
if obj in not_editing_objs:
|
||||
obj.select_set(False)
|
||||
continue
|
||||
if obj == props.editing_nest:
|
||||
continue
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not element.is_a("IfcElement"):
|
||||
continue
|
||||
components = ifcopenshell.util.element.get_components(element)
|
||||
if components:
|
||||
nests_to_move.append(tool.Ifc.get_object(element))
|
||||
continue
|
||||
if not components and props.in_nest_mode:
|
||||
continue
|
||||
nest = ifcopenshell.util.element.get_nest(element)
|
||||
if nest:
|
||||
nests_to_move.append(tool.Ifc.get_object(nest))
|
||||
obj.select_set(False)
|
||||
nests_to_move = set(nests_to_move)
|
||||
|
||||
if nests_to_move:
|
||||
for obj in nests_to_move:
|
||||
obj.select_set(True)
|
||||
for component in tool.Nest.get_components_recursively(tool.Ifc.get_entity(obj)):
|
||||
component_obj = tool.Ifc.get_object(component)
|
||||
component_obj.select_set(True)
|
||||
self.new_active_obj = obj
|
||||
return {"FINISHED"}
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -129,12 +129,7 @@ class RepresentationItem(PropertyGroup):
|
||||
|
||||
class RepresentationItemObject(PropertyGroup):
|
||||
name: StringProperty(name="Name")
|
||||
ifc_definition_id: IntProperty(name="IFC Definition ID")
|
||||
obj: PointerProperty(type=bpy.types.Object)
|
||||
verts: StringProperty(name="Verts")
|
||||
edges: StringProperty(name="Edges")
|
||||
special_verts: StringProperty(name="Special Verts")
|
||||
special_edges: StringProperty(name="Special Edges")
|
||||
|
||||
|
||||
class ShapeAspect(PropertyGroup):
|
||||
|
||||
@@ -64,8 +64,11 @@ class GeoreferenceDecorator:
|
||||
if not GeoreferenceData.is_loaded:
|
||||
GeoreferenceData.load()
|
||||
|
||||
self.calculate_angles(context)
|
||||
props = context.scene.BIMGeoreferenceProperties
|
||||
if not props.model_origin: # If this is empty, no georeferencing data has been loaded.
|
||||
return
|
||||
|
||||
self.calculate_angles(context)
|
||||
e, n, h = [round(float(o), 7) for o in props.model_origin.split(",")]
|
||||
if (operation := GeoreferenceData.data["coordinate_operation"]) and (scale := operation.get("Scale", None)):
|
||||
e *= scale
|
||||
@@ -75,8 +78,7 @@ class GeoreferenceDecorator:
|
||||
self.addon_prefs = tool.Blender.get_addon_preferences()
|
||||
|
||||
self.font_id = 0
|
||||
font_size = tool.Blender.scale_font_size(12 + (self.scale / 20))
|
||||
blf.size(self.font_id, font_size)
|
||||
blf.size(self.font_id, 12)
|
||||
color = self.addon_prefs.decorations_colour
|
||||
blf.color(self.font_id, *color)
|
||||
blf.enable(self.font_id, blf.SHADOW)
|
||||
@@ -175,6 +177,10 @@ class GeoreferenceDecorator:
|
||||
if not GeoreferenceData.is_loaded:
|
||||
GeoreferenceData.load()
|
||||
|
||||
props = context.scene.BIMGeoreferenceProperties
|
||||
if not props.model_origin: # If this is empty, no georeferencing data has been loaded.
|
||||
return
|
||||
|
||||
self.calculate_angles(context)
|
||||
|
||||
self.addon_prefs = tool.Blender.get_addon_preferences()
|
||||
@@ -308,7 +314,6 @@ class GeoreferenceDecorator:
|
||||
self.draw_batch("LINES", verts, decorator_color_special, edges)
|
||||
self.draw_dashed_line(location * 3, location * 6, decorator_color_error)
|
||||
|
||||
props = context.scene.BIMGeoreferenceProperties
|
||||
if props.has_blender_offset:
|
||||
location = GeoreferenceData.data["local_origin"]["location"]
|
||||
if location.length < 1000:
|
||||
|
||||
@@ -206,7 +206,11 @@ class BIMGeoreferenceProperties(PropertyGroup):
|
||||
update=update_should_visualise,
|
||||
)
|
||||
visualization_scale: FloatProperty(
|
||||
name="Visualization Scale", description="Affects the georeference decorator size", default=1, min=0.1, max=50
|
||||
name="Visualization Scale",
|
||||
description="Affects the georeference decorator size",
|
||||
default=1,
|
||||
soft_min=0.1,
|
||||
soft_max=50,
|
||||
)
|
||||
grid_north_angle: StringProperty(name="Grid North Angle", update=update_grid_north_angle)
|
||||
x_axis_abscissa: StringProperty(name="X Axis Abscissa", update=update_grid_north_vector)
|
||||
|
||||
@@ -25,12 +25,21 @@ from bonsai.bim.module.georeference.data import GeoreferenceData
|
||||
class BIM_PT_gis(Panel):
|
||||
bl_label = "Georeferencing"
|
||||
bl_idname = "BIM_PT_gis"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
bl_options = {"DEFAULT_CLOSED", "HEADER_LAYOUT_EXPAND"}
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_tab_geometry"
|
||||
|
||||
def draw_header(self, context):
|
||||
props = context.scene.BIMGeoreferenceProperties
|
||||
row = self.layout.row(align=True)
|
||||
icon = "HIDE_OFF" if props.should_visualise else "HIDE_ON"
|
||||
row.label(text="") # empty text occupies the left of the row
|
||||
if props.should_visualise:
|
||||
row.prop(props, "visualization_scale", text="Size", slider=True)
|
||||
row.prop(props, "should_visualise", icon=icon, text="")
|
||||
|
||||
def draw(self, context):
|
||||
self.layout.use_property_split = True
|
||||
self.layout.use_property_decorate = False
|
||||
@@ -87,19 +96,14 @@ class BIM_PT_gis(Panel):
|
||||
if not GeoreferenceData.data["projected_crs"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Not Georeferenced", icon="ERROR")
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "should_visualise", icon="HIDE_OFF")
|
||||
row.prop(props, "visualization_scale", text="Size", slider=True)
|
||||
if tool.Ifc.get_schema() != "IFC2X3":
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "coordinate_operation_class", text="")
|
||||
row.operator("bim.add_georeferencing", icon="ADD", text="")
|
||||
|
||||
if GeoreferenceData.data["projected_crs"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Projected CRS", icon="WORLD")
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "should_visualise", icon="HIDE_OFF")
|
||||
row.prop(props, "visualization_scale", text="Size", slider=True)
|
||||
if tool.Ifc.get_schema() != "IFC2X3":
|
||||
row.operator("bim.enable_editing_georeferencing", icon="GREASEPENCIL", text="")
|
||||
row.operator("bim.remove_georeferencing", icon="X", text="")
|
||||
|
||||
@@ -37,6 +37,7 @@ classes = (
|
||||
operator.Push,
|
||||
operator.RefreshGit,
|
||||
operator.SwitchRevision,
|
||||
operator.InstallGit,
|
||||
prop.IfcGitTag,
|
||||
prop.IfcGitListItem,
|
||||
prop.IfcGitProperties,
|
||||
|
||||
@@ -386,3 +386,25 @@ class ObjectLog(bpy.types.Operator):
|
||||
step_id = context.active_object.BIMObjectProperties.ifc_definition_id
|
||||
core.entity_log(tool.IfcGit, tool.Ifc, step_id, self)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class InstallGit(bpy.types.Operator):
|
||||
"""Install Git Version Control System from the
|
||||
Windows Package Manager Community Repository,
|
||||
requires restarting Blender after installation"""
|
||||
|
||||
bl_label = "Install Git"
|
||||
bl_idname = "ifcgit.install_git"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
IfcGitData.make_sure_is_loaded()
|
||||
if IfcGitData.data["git_exe"]:
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
core.install_git(tool.IfcGit, self)
|
||||
refresh()
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import bpy
|
||||
import time
|
||||
import os
|
||||
import platform
|
||||
|
||||
from bonsai.bim.module.ifcgit.data import IfcGitData
|
||||
|
||||
@@ -26,6 +27,12 @@ class IFCGIT_PT_panel(bpy.types.Panel):
|
||||
if not IfcGitData.data["git_exe"]:
|
||||
row = layout.row()
|
||||
row.label(text="Git is not installed", icon="ERROR")
|
||||
if platform.system() == "Windows":
|
||||
row.operator(
|
||||
"ifcgit.install_git",
|
||||
text="Install Git",
|
||||
icon="PACKAGE",
|
||||
)
|
||||
return
|
||||
|
||||
props = context.scene.IfcGitProperties
|
||||
|
||||
@@ -49,6 +49,8 @@ class LayersData:
|
||||
return []
|
||||
results = dict()
|
||||
shape = tool.Ifc.get().by_id(data.BIMMeshProperties.ifc_definition_id)
|
||||
for inverse in shape.LayerAssignments:
|
||||
for inverse in getattr(shape, "LayerAssignments", []):
|
||||
results[inverse.id()] = inverse.Name or "Unnamed"
|
||||
for inverse in getattr(shape, "LayerAssignment", []):
|
||||
results[inverse.id()] = inverse.Name or "Unnamed"
|
||||
return results
|
||||
|
||||
@@ -29,7 +29,7 @@ import bonsai.core.style
|
||||
import bonsai.core.material as core
|
||||
import bonsai.bim.module.model.profile as model_profile
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
from typing import Any, Union
|
||||
from typing import Any, Union, TYPE_CHECKING
|
||||
|
||||
|
||||
class LoadMaterials(bpy.types.Operator):
|
||||
@@ -155,7 +155,7 @@ class AddMaterial(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
class DuplicateMaterial(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.duplicate_material"
|
||||
bl_label = "Diplicate Material"
|
||||
bl_label = "Duplicate Material"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
material: bpy.props.IntProperty(name="Material ID")
|
||||
|
||||
@@ -202,9 +202,17 @@ class RemoveMaterialSet(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class AssignMaterialToSelected(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.assign_material_to_selected"
|
||||
bl_label = "Assign Material To Selected"
|
||||
bl_description = "Assign currently selected material in Materials UI to the selected objects"
|
||||
bl_description = (
|
||||
"Assign currently selected material in Materials UI to the selected objects.\n\n"
|
||||
"ALT+CLICK to assign material as a usage."
|
||||
)
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
material: bpy.props.IntProperty(name="Material IFC ID")
|
||||
assign_as_usage: bpy.props.BoolProperty(
|
||||
name="Assign Material As A Usage",
|
||||
default=False,
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@@ -213,13 +221,25 @@ class AssignMaterialToSelected(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return False
|
||||
return True
|
||||
|
||||
def invoke(self, context, event):
|
||||
if event.type == "LEFTMOUSE" and event.alt:
|
||||
material_class = tool.Ifc.get().by_id(self.material).is_a()
|
||||
if material_class not in ("IfcMaterialProfileSet", "IfcMaterialLayerSet"):
|
||||
self.report({"ERROR"}, f"{material_class} cannot be assigned as a usage.")
|
||||
return {"CANCELLED"}
|
||||
self.assign_as_usage = True
|
||||
return self.execute(context)
|
||||
|
||||
def _execute(self, context):
|
||||
material = tool.Ifc.get().by_id(self.material)
|
||||
objects = tool.Blender.get_selected_objects()
|
||||
material_type = material.is_a()
|
||||
if self.assign_as_usage:
|
||||
material_type += "Usage"
|
||||
core.assign_material(
|
||||
tool.Ifc,
|
||||
tool.Material,
|
||||
material_type=tool.Material.get_active_material_type(),
|
||||
material_type=material_type,
|
||||
objects=objects,
|
||||
material=material,
|
||||
)
|
||||
@@ -866,16 +886,30 @@ class SelectMaterialInMaterialsUI(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
material_id: bpy.props.IntProperty()
|
||||
|
||||
if TYPE_CHECKING:
|
||||
material_id: int
|
||||
|
||||
def execute(self, context):
|
||||
props = bpy.context.scene.BIMMaterialProperties
|
||||
ifc_file = tool.Ifc.get()
|
||||
material = ifc_file.by_id(self.material_id)
|
||||
core.load_materials(tool.Material, material.is_a())
|
||||
material_id = self.material_id
|
||||
material = ifc_file.by_id(material_id)
|
||||
material_class = material.is_a()
|
||||
|
||||
# Current we do not support editing usaged not on instance,
|
||||
# so fallback to the material set it's referring to.
|
||||
if material_class.endswith("Usage"):
|
||||
material_class = material_class.removesuffix("Usage")
|
||||
if material_class == "IfcMaterialProfileSet":
|
||||
material = material.ForProfileSet
|
||||
else: # IfcMaterialLayerSet
|
||||
material = material.ForLayerSet
|
||||
material_id = material.id()
|
||||
|
||||
core.load_materials(tool.Material, material_class)
|
||||
|
||||
def get_material_item() -> Union[tuple[int, bpy.types.PropertyGroup], None]:
|
||||
return next(
|
||||
((i, m) for i, m in enumerate(props.materials) if m.ifc_definition_id == self.material_id), None
|
||||
)
|
||||
return next(((i, m) for i, m in enumerate(props.materials) if m.ifc_definition_id == material_id), None)
|
||||
|
||||
material_item = get_material_item()
|
||||
|
||||
|
||||
@@ -124,6 +124,11 @@ class BIMMaterialProperties(PropertyGroup):
|
||||
styles: EnumProperty(items=get_styles, name="Styles")
|
||||
contexts: EnumProperty(items=get_contexts, name="Contexts")
|
||||
|
||||
@property
|
||||
def active_material(self):
|
||||
if self.active_material_index < len(self.materials):
|
||||
return self.materials[self.active_material_index]
|
||||
|
||||
|
||||
class BIMObjectMaterialProperties(PropertyGroup):
|
||||
material_type: EnumProperty(items=get_object_material_type, name="Material Type")
|
||||
|
||||
@@ -77,21 +77,23 @@ classes = (
|
||||
wall.SplitWall,
|
||||
wall.UnjoinWalls,
|
||||
opening.AddBoolean,
|
||||
opening.AddFilledOpening,
|
||||
opening.AddPotentialHalfSpaceSolid,
|
||||
opening.AddPotentialOpening,
|
||||
opening.EditOpenings,
|
||||
opening.CloneOpening,
|
||||
opening.EditOpenings,
|
||||
opening.FlipFill,
|
||||
opening.HideBooleans,
|
||||
opening.HideAllOpenings,
|
||||
opening.HideOpenings,
|
||||
opening.PurgeUnusedOpenings,
|
||||
opening.RecalculateFill,
|
||||
opening.RemoveBooleans,
|
||||
opening.ShowBooleans,
|
||||
opening.RemoveBoolean,
|
||||
opening.SelectBoolean,
|
||||
opening.ShowOpenings,
|
||||
opening.UpdateOpeningsFocus,
|
||||
profile.ChangeCardinalPoint,
|
||||
profile.ChangeProfileDepth,
|
||||
profile.DisableEditingExtrusionAxis,
|
||||
profile.DrawPolylineProfile,
|
||||
profile.EditExtrusionAxis,
|
||||
profile.EnableEditingExtrusionAxis,
|
||||
profile.ExtendProfile,
|
||||
@@ -124,6 +126,7 @@ classes = (
|
||||
prop.SnapMousePoint,
|
||||
prop.PolylinePoint,
|
||||
prop.Polyline,
|
||||
prop.ProductPreviewItem,
|
||||
prop.BIMModelProperties,
|
||||
prop.BIMArrayProperties,
|
||||
prop.BIMStairProperties,
|
||||
@@ -133,6 +136,7 @@ classes = (
|
||||
prop.BIMRailingProperties,
|
||||
prop.BIMRoofProperties,
|
||||
prop.BIMPolylineProperties,
|
||||
prop.BIMProductPreviewProperties,
|
||||
ui.BIM_PT_array,
|
||||
ui.BIM_PT_stair,
|
||||
ui.BIM_PT_sverchok,
|
||||
@@ -170,6 +174,7 @@ classes = (
|
||||
door.EnableEditingDoor,
|
||||
door.RemoveDoor,
|
||||
railing.BIM_OT_add_railing,
|
||||
railing.CopyRailingParameters,
|
||||
railing.AddRailing,
|
||||
railing.CancelEditingRailing,
|
||||
railing.FinishEditingRailing,
|
||||
@@ -182,6 +187,7 @@ classes = (
|
||||
roof.BIM_OT_add_roof,
|
||||
roof.AddRoof,
|
||||
roof.CancelEditingRoof,
|
||||
roof.CopyRoofParameters,
|
||||
roof.FinishEditingRoof,
|
||||
roof.EnableEditingRoof,
|
||||
roof.CancelEditingRoofPath,
|
||||
@@ -213,6 +219,7 @@ def register():
|
||||
|
||||
bpy.types.Scene.BIMModelProperties = bpy.props.PointerProperty(type=prop.BIMModelProperties)
|
||||
bpy.types.Scene.BIMPolylineProperties = bpy.props.PointerProperty(type=prop.BIMPolylineProperties)
|
||||
bpy.types.Scene.BIMProductPreviewProperties = bpy.props.PointerProperty(type=prop.BIMProductPreviewProperties)
|
||||
bpy.types.Object.BIMArrayProperties = bpy.props.PointerProperty(type=prop.BIMArrayProperties)
|
||||
bpy.types.Object.BIMStairProperties = bpy.props.PointerProperty(type=prop.BIMStairProperties)
|
||||
bpy.types.Object.BIMSverchokProperties = bpy.props.PointerProperty(type=prop.BIMSverchokProperties)
|
||||
@@ -243,6 +250,7 @@ def unregister():
|
||||
|
||||
del bpy.types.Scene.BIMModelProperties
|
||||
del bpy.types.Scene.BIMPolylineProperties
|
||||
del bpy.types.Scene.BIMProductPreviewProperties
|
||||
del bpy.types.Object.BIMArrayProperties
|
||||
del bpy.types.Object.BIMStairProperties
|
||||
del bpy.types.Object.BIMSverchokProperties
|
||||
|
||||
@@ -57,20 +57,23 @@ class AuthoringData:
|
||||
cls.data["ifc_element_type"] = cls.ifc_element_type
|
||||
cls.data["ifc_classes"] = cls.ifc_classes()
|
||||
cls.data["ifc_class_current"] = cls.ifc_class_current()
|
||||
cls.data["relating_type_id"] = cls.relating_type_id() # only after .ifc_classes()
|
||||
cls.data["relating_type_id_current"] = cls.relating_type_id_current() # only after .ifc_classes()
|
||||
cls.data["relating_type_name"] = cls.relating_type_name() # only after .relating_type_id()
|
||||
cls.data["relating_type_description"] = cls.relating_type_description() # only after .relating_type_id()
|
||||
cls.data["predefined_type"] = cls.predefined_type() # only after .relating_type_id()
|
||||
|
||||
# only after .ifc_classes()
|
||||
# Make sure .ifc_classes() was run before next lines
|
||||
cls.data["type_elements"] = cls.type_elements()
|
||||
cls.data["type_elements_filtered"] = cls.type_elements_filtered()
|
||||
cls.data["relating_type_id"] = cls.relating_type_id()
|
||||
# Make sure .relating_type_id() was run before next lines
|
||||
cls.data["relating_type_id_current"] = cls.relating_type_id_current()
|
||||
cls.data["relating_type_name"] = cls.relating_type_name()
|
||||
cls.data["relating_type_description"] = cls.relating_type_description()
|
||||
cls.data["predefined_type"] = cls.predefined_type()
|
||||
# Make sure .type_elements_filtered() was run before next lines
|
||||
cls.data["total_types"] = cls.total_types()
|
||||
cls.data["total_pages"] = cls.total_pages()
|
||||
cls.data["total_pages"] = cls.total_pages() # Only after .total_types()
|
||||
cls.data["next_page"] = cls.next_page()
|
||||
cls.data["prev_page"] = cls.prev_page()
|
||||
cls.data["paginated_relating_types"] = cls.paginated_relating_types()
|
||||
|
||||
cls.data["type_thumbnail"] = cls.type_thumbnail() # only after .relating_type_id()
|
||||
cls.data["type_thumbnail"] = cls.type_thumbnail() # Only after .relating_type_id_current()
|
||||
cls.data["is_voidable_element"] = cls.is_voidable_element()
|
||||
cls.data["has_visible_openings"] = cls.has_visible_openings()
|
||||
cls.data["has_visible_boundaries"] = cls.has_visible_boundaries()
|
||||
@@ -81,6 +84,10 @@ class AuthoringData:
|
||||
cls.data["boundary_class"] = cls.boundary_class()
|
||||
cls.data["selected_material_usages"] = cls.selected_material_usages()
|
||||
|
||||
# Only after .active_material_usage() and .active_class()
|
||||
cls.data["is_flippable_element"] = cls.is_flippable_element()
|
||||
cls.data["is_regenable_element"] = cls.is_regenable_element()
|
||||
|
||||
@classmethod
|
||||
def default_container(cls) -> str | None:
|
||||
props = bpy.context.scene.BIMSpatialDecompositionProperties
|
||||
@@ -100,23 +107,15 @@ class AuthoringData:
|
||||
|
||||
@classmethod
|
||||
def type_thumbnail(cls):
|
||||
if not cls.data["relating_type_id"]:
|
||||
return 0
|
||||
relating_type_id = tool.Blender.get_enum_safe(cls.props, "relating_type_id")
|
||||
if relating_type_id is None:
|
||||
return 0
|
||||
element = tool.Ifc.get().by_id(int(relating_type_id))
|
||||
return cls.type_thumbnails.get(element.id(), None) or 0
|
||||
return cls.type_thumbnails.get(int(cls.data["relating_type_id_current"] or 0), 0)
|
||||
|
||||
@classmethod
|
||||
def total_types(cls):
|
||||
ifc_class = cls.data["ifc_class_current"]
|
||||
return len(tool.Ifc.get().by_type(ifc_class)) if ifc_class else 0
|
||||
return len(cls.data["type_elements_filtered"])
|
||||
|
||||
@classmethod
|
||||
def total_pages(cls):
|
||||
ifc_class = cls.data["ifc_class_current"]
|
||||
total_types = len(tool.Ifc.get().by_type(ifc_class)) if ifc_class else 0
|
||||
total_types = cls.data["total_types"]
|
||||
return math.ceil(total_types / cls.types_per_page)
|
||||
|
||||
@classmethod
|
||||
@@ -130,12 +129,35 @@ class AuthoringData:
|
||||
return cls.props.type_page - 1
|
||||
|
||||
@classmethod
|
||||
def paginated_relating_types(cls):
|
||||
def type_elements(cls):
|
||||
ifc_class = cls.data["ifc_class_current"]
|
||||
if not ifc_class:
|
||||
return []
|
||||
elements = list(tool.Ifc.get().by_type(ifc_class))
|
||||
return natsorted(elements, key=lambda s: (s.Name or "Unnamed").lower())
|
||||
|
||||
@classmethod
|
||||
def type_elements_filtered(cls):
|
||||
search_query = cls.props.search_name.lower()
|
||||
|
||||
def filter_element(element):
|
||||
if search_query in (element.Name or "Unnamed").lower():
|
||||
return True
|
||||
if search_query in (element.Description or "").lower():
|
||||
return True
|
||||
if search_query in (ifcopenshell.util.element.get_predefined_type(element) or "").lower():
|
||||
return True
|
||||
return False
|
||||
|
||||
elements = cls.data["type_elements"]
|
||||
if cls.props.search_name:
|
||||
return [e for e in elements if filter_element(e)]
|
||||
return elements
|
||||
|
||||
@classmethod
|
||||
def paginated_relating_types(cls):
|
||||
results = []
|
||||
elements = natsorted(tool.Ifc.get().by_type(ifc_class), key=lambda e: e.Name or "Unnamed")
|
||||
elements = cls.data["type_elements_filtered"]
|
||||
elements = elements[(cls.props.type_page - 1) * cls.types_per_page : cls.props.type_page * cls.types_per_page]
|
||||
for element in elements:
|
||||
predefined_type = ifcopenshell.util.element.get_predefined_type(element)
|
||||
@@ -159,6 +181,28 @@ class AuthoringData:
|
||||
element = tool.Ifc.get_entity(active_object)
|
||||
return element and element.is_a("IfcElement") and not element.is_a("IfcOpeningElement")
|
||||
|
||||
@classmethod
|
||||
def is_flippable_element(cls):
|
||||
return cls.data["active_material_usage"] in ("LAYER2", "PROFILE") or cls.data["active_class"] in (
|
||||
"IfcWindow",
|
||||
"IfcWindowStandardCase",
|
||||
"IfcDoor",
|
||||
"IfcDoorStandardCase",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def is_regenable_element(cls):
|
||||
if cls.data["active_material_usage"] in ("LAYER2", "PROFILE") and cls.data["active_class"] not in (
|
||||
"IfcCableCarrierSegment",
|
||||
"IfcCableSegment",
|
||||
"IfcDuctSegment",
|
||||
"IfcPipeSegment",
|
||||
):
|
||||
return True
|
||||
if cls.data["active_class"] in ("IfcWindow", "IfcWindowStandardCase", "IfcDoor", "IfcDoorStandardCase"):
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def has_visible_openings(cls):
|
||||
if active_object := tool.Blender.get_active_object():
|
||||
@@ -197,10 +241,9 @@ class AuthoringData:
|
||||
|
||||
@classmethod
|
||||
def is_representation_item_active(cls) -> bool:
|
||||
object = bpy.context.active_object
|
||||
if not object:
|
||||
if not (obj := tool.Blender.get_active_object()):
|
||||
return False
|
||||
return tool.Geometry.is_representation_item(object)
|
||||
return tool.Geometry.is_representation_item(obj)
|
||||
|
||||
@classmethod
|
||||
def active_representation_type(cls):
|
||||
@@ -240,13 +283,8 @@ class AuthoringData:
|
||||
|
||||
@classmethod
|
||||
def relating_type_id(cls):
|
||||
results = []
|
||||
ifc_class = cls.data["ifc_class_current"]
|
||||
if ifc_class:
|
||||
elements = natsorted(tool.Ifc.get().by_type(ifc_class), key=lambda s: (s.Name or "Unnamed").lower())
|
||||
results.extend(elements)
|
||||
return [(str(e.id()), e.Name or "Unnamed", e.Description or "") for e in results]
|
||||
return []
|
||||
elements = cls.data["type_elements"]
|
||||
return [(str(e.id()), e.Name or "Unnamed", e.Description or "") for e in elements]
|
||||
|
||||
@classmethod
|
||||
def relating_type_id_current(cls):
|
||||
@@ -543,6 +581,7 @@ class RoofData:
|
||||
if not cls.data["pset_data"]:
|
||||
return
|
||||
cls.data["general_params"] = cls.general_params()
|
||||
cls.data["path_data"] = cls.path_data()
|
||||
|
||||
@classmethod
|
||||
def pset_data(cls):
|
||||
@@ -563,6 +602,10 @@ class RoofData:
|
||||
general_params[prop_readable_name] = prop_value
|
||||
return general_params
|
||||
|
||||
@classmethod
|
||||
def path_data(cls):
|
||||
return cls.data["pset_data"]["data_dict"]["path_data"]
|
||||
|
||||
|
||||
class ItemData:
|
||||
data = {}
|
||||
|
||||
@@ -25,15 +25,14 @@ import bmesh
|
||||
import ifcopenshell
|
||||
import bonsai.tool as tool
|
||||
import math
|
||||
from math import sin, cos, tan, radians
|
||||
from math import sin, cos, radians
|
||||
from bpy.types import SpaceView3D
|
||||
from bpy_extras import view3d_utils
|
||||
from mathutils import Vector, Matrix, Quaternion
|
||||
from mathutils import Vector, Matrix
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
from gpu_extras.presets import draw_circle_2d
|
||||
from typing import Union
|
||||
from bonsai.bim.module.drawing.helper import format_distance
|
||||
from bonsai.bim.module.geometry.decorator import ItemDecorator
|
||||
|
||||
|
||||
def transparent_color(color, alpha=0.1):
|
||||
@@ -209,11 +208,11 @@ class ProfileDecorator:
|
||||
if draw_faces:
|
||||
self.draw_faces(bm, all_vertices)
|
||||
|
||||
self.draw_batch("LINES", all_vertices, transparent_color(unselected_elements_color), unselected_edges)
|
||||
self.draw_batch("LINES", all_vertices, selected_elements_color, selected_edges)
|
||||
self.draw_batch("LINES", all_vertices, background_elements_color, arc_edges)
|
||||
self.draw_batch("LINES", all_vertices, special_elements_color, preview_edges)
|
||||
self.draw_batch("LINES", all_vertices, special_elements_color, roof_angle_edges)
|
||||
self.draw_batch("LINES", all_vertices, unselected_elements_color, unselected_edges)
|
||||
self.draw_batch("LINES", all_vertices, selected_elements_color, selected_edges)
|
||||
|
||||
self.draw_batch("POINTS", unselected_vertices, transparent_color(unselected_elements_color, 0.5))
|
||||
self.draw_batch("POINTS", error_vertices, error_elements_color)
|
||||
@@ -580,7 +579,7 @@ class PolylineDecorator:
|
||||
padding = 8
|
||||
verts = []
|
||||
edges = []
|
||||
if snap_prop.snap_type in ["Edge", "Edge-Intersection", "Vertex"]:
|
||||
if snap_prop.snap_type in ["Edge", "Edge Intersection", "Vertex"]:
|
||||
p1 = (coords[0] - padding, coords[1] + padding)
|
||||
p2 = (coords[0] + padding, coords[1] + padding)
|
||||
p3 = (coords[0] + padding, coords[1] - padding)
|
||||
@@ -588,7 +587,7 @@ class PolylineDecorator:
|
||||
verts = [p1, p2, p3, p4]
|
||||
if snap_prop.snap_type == "Edge":
|
||||
edges = [[0, 1], [1, 3], [3, 2], [2, 0]]
|
||||
elif snap_prop.snap_type == "Edge-Intersection":
|
||||
elif snap_prop.snap_type == "Edge Intersection":
|
||||
edges = [[0, 2], [1, 3]]
|
||||
else:
|
||||
edges = [[0, 1], [1, 2], [2, 3], [3, 0]]
|
||||
@@ -786,6 +785,10 @@ class ProductDecorator:
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls):
|
||||
props = bpy.context.scene.BIMProductPreviewProperties # updated by model/polyline.py
|
||||
props.verts.clear()
|
||||
props.edges.clear()
|
||||
props.tris.clear()
|
||||
for handler in cls.handlers:
|
||||
try:
|
||||
SpaceView3D.draw_handler_remove(handler, "WINDOW")
|
||||
@@ -799,315 +802,12 @@ class ProductDecorator:
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
|
||||
def get_wall_preview_data(cls, context, relating_type):
|
||||
def create_bmesh_from_vertices(vertices):
|
||||
bm = bmesh.new()
|
||||
|
||||
new_verts = [bm.verts.new(v) for v in polyline_vertices]
|
||||
if is_closed:
|
||||
new_edges = [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
|
||||
new_edges.append(
|
||||
bm.edges.new((new_verts[-1], new_verts[0]))
|
||||
) # Add an edge between the last an first point to make it closed.
|
||||
else:
|
||||
new_edges = [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
return bm
|
||||
|
||||
# Get properties from object type
|
||||
layers = tool.Model.get_material_layer_parameters(relating_type)
|
||||
if not layers["thickness"]:
|
||||
return
|
||||
thickness = layers["thickness"]
|
||||
model_props = context.scene.BIMModelProperties
|
||||
direction_sense = model_props.direction_sense
|
||||
direction = 1
|
||||
if direction_sense == "NEGATIVE":
|
||||
direction = -1
|
||||
offset_type = model_props.offset_type
|
||||
offset = 0
|
||||
if offset_type == "CENTER":
|
||||
offset = -thickness / 2
|
||||
elif offset_type == "INTERIOR":
|
||||
offset = -thickness
|
||||
|
||||
unit_system = tool.Drawing.get_unit_system()
|
||||
factor = 1
|
||||
if unit_system == "IMPERIAL":
|
||||
factor = 3.048
|
||||
if unit_system == "METRIC":
|
||||
unit_length = context.scene.unit_settings.length_unit
|
||||
if unit_length == "MILLIMETERS":
|
||||
factor = 1000
|
||||
|
||||
# For the model properties, the offset value should just be converted
|
||||
# However, for the wall preview logic that follows, offset and thickness must change direction
|
||||
model_props.offset = offset * factor
|
||||
thickness *= direction
|
||||
offset *= direction
|
||||
|
||||
height = float(model_props.extrusion_depth)
|
||||
rl = float(model_props.rl1)
|
||||
x_angle = float(model_props.x_angle)
|
||||
angle_distortion = height * tan(x_angle)
|
||||
|
||||
wall_preview_data = {}
|
||||
wall_preview_data["verts"] = []
|
||||
|
||||
# Verts
|
||||
polyline_vertices = []
|
||||
polyline_data = context.scene.BIMPolylineProperties.insertion_polyline
|
||||
polyline_points = polyline_data[0].polyline_points if polyline_data else []
|
||||
if len(polyline_points) < 2:
|
||||
wall_preview_data = []
|
||||
return
|
||||
for point in polyline_points:
|
||||
polyline_vertices.append(Vector((point.x, point.y, point.z)))
|
||||
|
||||
is_closed = False
|
||||
if (
|
||||
polyline_vertices[0].x == polyline_vertices[-1].x
|
||||
and polyline_vertices[0].y == polyline_vertices[-1].y
|
||||
and polyline_vertices[0].z == polyline_vertices[-1].z
|
||||
):
|
||||
is_closed = True
|
||||
polyline_vertices.pop(-1) # Remove the last point. The edges are going to inform that the shape is closed.
|
||||
|
||||
bm_base = create_bmesh_from_vertices(polyline_vertices)
|
||||
base_vertices = tool.Cad.offset_edges(bm_base, offset)
|
||||
offset_base_verts = tool.Cad.offset_edges(bm_base, thickness + offset)
|
||||
top_vertices = tool.Cad.offset_edges(bm_base, angle_distortion + offset)
|
||||
offset_top_verts = tool.Cad.offset_edges(bm_base, angle_distortion + thickness + offset)
|
||||
if is_closed:
|
||||
base_vertices.append(base_vertices[0])
|
||||
offset_base_verts.append(offset_base_verts[0])
|
||||
top_vertices.append(top_vertices[0])
|
||||
offset_top_verts.append(offset_top_verts[0])
|
||||
|
||||
if offset_base_verts is not None:
|
||||
for v in base_vertices:
|
||||
wall_preview_data["verts"].append((v.co.x, v.co.y, v.co.z + rl))
|
||||
|
||||
for v in offset_base_verts[::-1]:
|
||||
wall_preview_data["verts"].append((v.co.x, v.co.y, v.co.z + rl))
|
||||
|
||||
for v in top_vertices:
|
||||
wall_preview_data["verts"].append((v.co.x, v.co.y, v.co.z + rl + height))
|
||||
|
||||
for v in offset_top_verts[::-1]:
|
||||
wall_preview_data["verts"].append((v.co.x, v.co.y, v.co.z + rl + height))
|
||||
|
||||
bm_base.free()
|
||||
|
||||
# Edges and Tris
|
||||
points = []
|
||||
side_edges_1 = []
|
||||
side_edges_2 = []
|
||||
base_edges = []
|
||||
|
||||
for i in range(len(wall_preview_data["verts"])):
|
||||
points.append(Vector(wall_preview_data["verts"][i]))
|
||||
|
||||
n = len(points) // 2
|
||||
bottom_side_1 = [[i, (i + 1) % (n)] for i in range((n - 1) // 2)]
|
||||
bottom_side_2 = [[i, (i + 1) % (n)] for i in range(n // 2, n - 1)]
|
||||
bottom_connections = [[i, n - i - 1] for i in range(n // 2)]
|
||||
bottom_loop = bottom_connections + bottom_side_1 + bottom_side_2
|
||||
side_edges_1.extend(bottom_side_1)
|
||||
side_edges_2.extend(bottom_side_2)
|
||||
base_edges.extend(bottom_loop)
|
||||
|
||||
upper_side_1 = [[i + n for i in edges] for edges in bottom_side_1]
|
||||
upper_side_2 = [[i + n for i in edges] for edges in bottom_side_2]
|
||||
upper_loop = [[i + n for i in edges] for edges in bottom_loop]
|
||||
side_edges_1.extend(upper_side_1)
|
||||
side_edges_2.extend(upper_side_2)
|
||||
base_edges.extend(upper_loop)
|
||||
|
||||
loops = [side_edges_1, side_edges_2, base_edges]
|
||||
|
||||
wall_preview_data["edges"] = []
|
||||
wall_preview_data["tris"] = []
|
||||
for i, group in enumerate(loops):
|
||||
bm = bmesh.new()
|
||||
|
||||
new_verts = [bm.verts.new(v) for v in points]
|
||||
new_edges = [bm.edges.new((new_verts[e[0]], new_verts[e[1]])) for e in group]
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
|
||||
if i == 2:
|
||||
new_faces = bmesh.ops.contextual_create(bm, geom=bm.edges)
|
||||
new_faces = bmesh.ops.bridge_loops(bm, edges=bm.edges, use_pairs=True, use_cyclic=True)
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
edges = [[v.index for v in e.verts] for e in bm.edges]
|
||||
tris = [[l.vert.index for l in loop] for loop in bm.calc_loop_triangles()]
|
||||
wall_preview_data["edges"].extend(edges)
|
||||
wall_preview_data["tris"].extend(tris)
|
||||
|
||||
wall_preview_data["edges"] = list(set(tuple(e) for e in wall_preview_data["edges"]))
|
||||
wall_preview_data["tris"] = list(set(tuple(t) for t in wall_preview_data["tris"]))
|
||||
|
||||
return wall_preview_data
|
||||
|
||||
def get_product_preview_data(cls, context, relating_type):
|
||||
model_props = context.scene.BIMModelProperties
|
||||
if relating_type.is_a("IfcDoorType"):
|
||||
rl = float(model_props.rl1)
|
||||
elif relating_type.is_a("IfcWindowType"):
|
||||
rl = float(model_props.rl2)
|
||||
else:
|
||||
rl = 0
|
||||
snap_prop = context.scene.BIMPolylineProperties.snap_mouse_point[0]
|
||||
mouse_point = Vector((snap_prop.x, snap_prop.y, snap_prop.z))
|
||||
snap_obj = bpy.data.objects.get(snap_prop.snap_object)
|
||||
snap_element = tool.Ifc.get_entity(snap_obj)
|
||||
rot_mat = Quaternion()
|
||||
if snap_element and snap_element.is_a("IfcWall"):
|
||||
rot_mat = snap_obj.matrix_world.to_quaternion()
|
||||
|
||||
obj_type = tool.Ifc.get_object(relating_type)
|
||||
if obj_type.data:
|
||||
data = ItemDecorator.get_obj_data(obj_type)
|
||||
data["verts"] = [tuple(obj_type.matrix_world.inverted() @ Vector(v)) for v in data["verts"]]
|
||||
data["verts"] = [tuple(rot_mat @ (Vector((v[0], v[1], (v[2] + rl)))) + mouse_point) for v in data["verts"]]
|
||||
return data
|
||||
|
||||
def get_profile_preview_data(self, context, relating_type):
|
||||
material = ifcopenshell.util.element.get_material(relating_type)
|
||||
try:
|
||||
profile = material.MaterialProfiles[0].Profile
|
||||
except:
|
||||
return {}
|
||||
|
||||
model_props = context.scene.BIMModelProperties
|
||||
extrusion_depth = model_props.extrusion_depth
|
||||
cardinal_point = model_props.cardinal_point
|
||||
rot_mat = Quaternion()
|
||||
if relating_type.is_a("IfcBeamType"):
|
||||
y_rot = Quaternion((0.0, 1.0, 0.0), radians(90))
|
||||
z_rot = Quaternion((0.0, 0.0, 1.0), radians(90))
|
||||
rot_mat = y_rot @ z_rot
|
||||
# Get profile data
|
||||
settings = ifcopenshell.geom.settings()
|
||||
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
|
||||
shape = ifcopenshell.geom.create_shape(settings, profile)
|
||||
|
||||
verts = shape.verts
|
||||
if not verts:
|
||||
raise RuntimeError("Profile shape has no vertices, it probably is invalid.")
|
||||
|
||||
edges = shape.edges
|
||||
|
||||
grouped_verts = [[verts[i], verts[i + 1], 0] for i in range(0, len(verts), 3)]
|
||||
grouped_edges = [[edges[i], edges[i + 1]] for i in range(0, len(edges), 2)]
|
||||
|
||||
# Create offsets based on cardinal point
|
||||
min_x = min(v[0] for v in grouped_verts)
|
||||
max_x = max(v[0] for v in grouped_verts)
|
||||
min_y = min(v[1] for v in grouped_verts)
|
||||
max_y = max(v[1] for v in grouped_verts)
|
||||
|
||||
x_offset = (max_x - min_x) / 2
|
||||
y_offset = (max_y - min_y) / 2
|
||||
|
||||
match cardinal_point:
|
||||
case "1":
|
||||
grouped_verts = [(v[0] - x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
|
||||
case "2":
|
||||
grouped_verts = [(v[0], v[1] + y_offset, v[2]) for v in grouped_verts]
|
||||
case "3":
|
||||
grouped_verts = [(v[0] + x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
|
||||
case "4":
|
||||
grouped_verts = [(v[0] - x_offset, v[1], v[2]) for v in grouped_verts]
|
||||
case "5":
|
||||
grouped_verts = [(v[0], v[1], v[2]) for v in grouped_verts]
|
||||
case "6":
|
||||
grouped_verts = [(v[0] + x_offset, v[1], v[2]) for v in grouped_verts]
|
||||
case "7":
|
||||
grouped_verts = [(v[0] - x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
|
||||
case "8":
|
||||
grouped_verts = [(v[0], v[1] - y_offset, v[2]) for v in grouped_verts]
|
||||
case "9":
|
||||
grouped_verts = [(v[0] + x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
|
||||
|
||||
# Create extrusion bmesh
|
||||
bm = bmesh.new()
|
||||
|
||||
new_verts = [bm.verts.new(v) for v in grouped_verts]
|
||||
new_edges = [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(grouped_verts) - 1)]
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
|
||||
new_faces = bmesh.ops.contextual_create(bm, geom=bm.edges)
|
||||
|
||||
new_faces = bmesh.ops.extrude_face_region(bm, geom=bm.faces)
|
||||
new_verts = [e for e in new_faces["geom"] if isinstance(e, bmesh.types.BMVert)]
|
||||
new_faces = bmesh.ops.translate(bm, verts=new_verts, vec=(0.0, 0.0, extrusion_depth))
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()]
|
||||
|
||||
# Create bounding box
|
||||
verts = bm.verts
|
||||
i = len(verts)
|
||||
|
||||
min_x = min(v.co.x for v in verts)
|
||||
max_x = max(v.co.x for v in verts)
|
||||
min_y = min(v.co.y for v in verts)
|
||||
max_y = max(v.co.y for v in verts)
|
||||
min_z = min(v.co.z for v in verts)
|
||||
max_z = max(v.co.z for v in verts)
|
||||
|
||||
bbox_verts = [
|
||||
(min_x, min_y, min_z),
|
||||
(max_x, min_y, min_z),
|
||||
(max_x, max_y, min_z),
|
||||
(min_x, max_y, min_z),
|
||||
(min_x, min_y, max_z),
|
||||
(max_x, min_y, max_z),
|
||||
(max_x, max_y, max_z),
|
||||
(min_x, max_y, max_z),
|
||||
]
|
||||
|
||||
bbox_edges = [
|
||||
(0 + i, 3 + i),
|
||||
(3 + i, 7 + i),
|
||||
(7 + i, 4 + i),
|
||||
(4 + i, 0 + i),
|
||||
(0 + i, 1 + i),
|
||||
(3 + i, 2 + i),
|
||||
(7 + i, 6 + i),
|
||||
(4 + i, 5 + i),
|
||||
(1 + i, 2 + i),
|
||||
(2 + i, 6 + i),
|
||||
(6 + i, 5 + i),
|
||||
(5 + i, 1 + i),
|
||||
]
|
||||
|
||||
# Calculate rotation, mouse position, angle and cardinal point
|
||||
# TODO Angle
|
||||
snap_prop = context.scene.BIMPolylineProperties.snap_mouse_point[0]
|
||||
mouse_point = Vector((snap_prop.x, snap_prop.y, snap_prop.z))
|
||||
def get_product_preview_data(self, context):
|
||||
props = context.scene.BIMProductPreviewProperties
|
||||
data = {}
|
||||
|
||||
verts = [tuple(v.co) for v in verts]
|
||||
verts.extend(bbox_verts)
|
||||
verts = [tuple(rot_mat @ Vector(v)) for v in verts]
|
||||
verts = [tuple(Vector(v) + mouse_point) for v in verts]
|
||||
data["verts"] = verts
|
||||
data["edges"] = bbox_edges
|
||||
# data["edges"] = [(edge.verts[0].index, edge.verts[1].index) for edge in bm.edges]
|
||||
data["tris"] = tris
|
||||
|
||||
bm.free()
|
||||
data["verts"] = [(*v.value_3d,) for v in props.verts]
|
||||
data["edges"] = [(int(e.value_2d[0]), int(e.value_2d[1])) for e in props.edges]
|
||||
data["tris"] = [(int(t.value_3d[0]), int(t.value_3d[1]), int(t.value_3d[2])) for t in props.tris]
|
||||
return data
|
||||
|
||||
def draw_product_preview(self, context):
|
||||
@@ -1134,25 +834,7 @@ class ProductDecorator:
|
||||
else:
|
||||
return
|
||||
|
||||
# Wall
|
||||
if self.relating_type.is_a("IfcWallType"):
|
||||
wall_preview_data = self.get_wall_preview_data(context, self.relating_type)
|
||||
if wall_preview_data:
|
||||
self.draw_batch("LINES", wall_preview_data["verts"], decorator_color, wall_preview_data["edges"])
|
||||
self.draw_batch(
|
||||
"TRIS", wall_preview_data["verts"], transparent_color(decorator_color), wall_preview_data["tris"]
|
||||
)
|
||||
|
||||
# Mesh type products
|
||||
product_preview_data = self.get_product_preview_data(context, self.relating_type)
|
||||
if product_preview_data:
|
||||
self.draw_batch("LINES", product_preview_data["verts"], decorator_color, product_preview_data["edges"])
|
||||
self.draw_batch(
|
||||
"TRIS", product_preview_data["verts"], transparent_color(decorator_color), product_preview_data["tris"]
|
||||
)
|
||||
|
||||
# Profile type products
|
||||
product_preview_data = self.get_profile_preview_data(context, self.relating_type)
|
||||
product_preview_data = self.get_product_preview_data(context)
|
||||
if product_preview_data:
|
||||
self.draw_batch("LINES", product_preview_data["verts"], decorator_color, product_preview_data["edges"])
|
||||
self.draw_batch(
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
|
||||
import bpy
|
||||
import bmesh
|
||||
from bmesh.types import BMVert, BMFace
|
||||
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.util.element
|
||||
@@ -42,8 +40,7 @@ import collections
|
||||
V_ = tool.Blender.V_
|
||||
|
||||
|
||||
def update_door_modifier_representation(context: bpy.types.Context) -> None:
|
||||
obj = context.active_object
|
||||
def update_door_modifier_representation(obj: bpy.types.Object) -> None:
|
||||
props = obj.BIMDoorProperties
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
ifc_file = tool.Ifc.get()
|
||||
@@ -170,22 +167,6 @@ def update_door_modifier_representation(context: bpy.types.Context) -> None:
|
||||
tool.Model.update_simple_openings(element)
|
||||
|
||||
|
||||
# TODO: move it out to tools
|
||||
def bm_sort_out_geom(
|
||||
geom_data: list[Union[bmesh.types.BMVert, bmesh.types.BMEdge, bmesh.types.BMFace]]
|
||||
) -> dict[str, Any]:
|
||||
geom_dict = {"verts": [], "edges": [], "faces": []}
|
||||
|
||||
for el in geom_data:
|
||||
if isinstance(el, BMVert):
|
||||
geom_dict["verts"].append(el)
|
||||
elif isinstance(el, BMFace):
|
||||
geom_dict["faces"].append(el)
|
||||
else:
|
||||
geom_dict["edges"].append(el)
|
||||
return geom_dict
|
||||
|
||||
|
||||
def bm_mirror(
|
||||
bm: bmesh.types.BMesh,
|
||||
verts: list[bmesh.types.BMVert],
|
||||
@@ -209,7 +190,7 @@ def bm_mirror(
|
||||
for v in verts:
|
||||
faces.update(v.link_faces)
|
||||
duplicated = bmesh.ops.duplicate(bm, geom=list(faces))
|
||||
verts = bm_sort_out_geom(duplicated["geom"])["verts"]
|
||||
verts = tool.Model.bm_sort_out_geom(duplicated["geom"])["verts"]
|
||||
|
||||
bmesh.ops.transform(bm, verts=verts, matrix=matrix, space=Matrix.Identity(4))
|
||||
return verts
|
||||
@@ -243,7 +224,7 @@ def create_bm_extruded_profile(
|
||||
|
||||
extruded = bmesh.ops.extrude_face_region(bm, geom=new_faces)
|
||||
extrusion_vector = extrusion_vector * magnitude
|
||||
extruded_verts = bm_sort_out_geom(extruded["geom"])["verts"]
|
||||
extruded_verts = tool.Model.bm_sort_out_geom(extruded["geom"])["verts"]
|
||||
bmesh.ops.translate(bm, vec=extrusion_vector, verts=extruded_verts)
|
||||
|
||||
bmesh.ops.translate(bm, vec=position, verts=new_verts + extruded_verts)
|
||||
@@ -305,7 +286,7 @@ def create_bm_door_lining(
|
||||
|
||||
extruded = bmesh.ops.extrude_face_region(bm, geom=new_faces)
|
||||
extrusion_vector = Vector((0, 1, 0)) * depth
|
||||
translate_verts = [v for v in extruded["geom"] if isinstance(v, BMVert)]
|
||||
translate_verts = [v for v in extruded["geom"] if isinstance(v, bmesh.types.BMVert)]
|
||||
bmesh.ops.translate(bm, vec=extrusion_vector, verts=translate_verts)
|
||||
|
||||
bmesh.ops.translate(bm, vec=position, verts=new_verts + translate_verts)
|
||||
@@ -546,11 +527,10 @@ class BIM_OT_add_door(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class AddDoor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_door"
|
||||
bl_label = "Add Door"
|
||||
bl_description = "Add Bonsai parametric door to the active IFC element"
|
||||
bl_description = "Add a Parametric Door to the Selected IFC Door Elements"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
obj = context.active_object
|
||||
def add_door_on_object(self, obj):
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
props = obj.BIMDoorProperties
|
||||
|
||||
@@ -571,18 +551,25 @@ class AddDoor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
pset=pset,
|
||||
properties={"Data": tool.Ifc.get().createIfcText(json.dumps(door_data, default=list))},
|
||||
)
|
||||
update_door_modifier_representation(context)
|
||||
update_door_modifier_representation(obj)
|
||||
|
||||
def _execute(self, context):
|
||||
for obj in tool.Blender.get_selected_objects():
|
||||
if not tool.Blender.Modifier.is_eligible_for_door_modifier(obj):
|
||||
continue
|
||||
self.add_door_on_object(obj)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class CancelEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.cancel_editing_door"
|
||||
bl_label = "Cancel Editing Door"
|
||||
bl_options = {"REGISTER"}
|
||||
bl_label = "Cancel Editing Door on Selected Objects"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
obj = context.active_object
|
||||
def cancel_editing_door_on_object(self, obj):
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not tool.Blender.Modifier.is_door(element):
|
||||
return
|
||||
props = obj.BIMDoorProperties
|
||||
data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Door", "Data"))
|
||||
data.update(data.pop("lining_properties"))
|
||||
@@ -603,17 +590,22 @@ class CancelEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
)
|
||||
|
||||
props.is_editing = False
|
||||
|
||||
def _execute(self, context):
|
||||
for obj in tool.Blender.get_selected_objects():
|
||||
self.cancel_editing_door_on_object(obj)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class FinishEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.finish_editing_door"
|
||||
bl_label = "Finish Editing Door"
|
||||
bl_options = {"REGISTER"}
|
||||
bl_label = "Finish Editing Door on Selected Objects"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
obj = context.active_object
|
||||
def finish_editing_door_on_object(self, obj):
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not tool.Blender.Modifier.is_door(element):
|
||||
return
|
||||
props = obj.BIMDoorProperties
|
||||
|
||||
door_data = props.get_general_kwargs(convert_to_project_units=True)
|
||||
@@ -625,23 +617,28 @@ class FinishEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
props.is_editing = False
|
||||
|
||||
update_door_modifier_representation(context)
|
||||
update_door_modifier_representation(obj)
|
||||
|
||||
pset = tool.Pset.get_element_pset(element, "BBIM_Door")
|
||||
door_data = tool.Ifc.get().createIfcText(json.dumps(door_data, default=list))
|
||||
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Data": door_data})
|
||||
|
||||
def _execute(self, context):
|
||||
for obj in tool.Blender.get_selected_objects():
|
||||
self.finish_editing_door_on_object(obj)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class EnableEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.enable_editing_door"
|
||||
bl_label = "Enable Editing Door"
|
||||
bl_options = {"REGISTER"}
|
||||
bl_label = "Enable Editing Door on Selected Objects"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
obj = context.active_object
|
||||
props = obj.BIMDoorProperties
|
||||
def edit_door_on_obj(self, obj):
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not tool.Blender.Modifier.is_door(element):
|
||||
return
|
||||
props = obj.BIMDoorProperties
|
||||
data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Door", "Data"))
|
||||
data.update(data.pop("lining_properties"))
|
||||
data.update(data.pop("panel_properties"))
|
||||
@@ -649,21 +646,28 @@ class EnableEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
# required since we could load pset from .ifc and BIMDoorProperties won't be set
|
||||
props.set_props_kwargs_from_ifc_data(data)
|
||||
props.is_editing = True
|
||||
|
||||
def _execute(self, context):
|
||||
for obj in tool.Blender.get_selected_objects():
|
||||
self.edit_door_on_obj(obj)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RemoveDoor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.remove_door"
|
||||
bl_label = "Remove Door"
|
||||
bl_options = {"REGISTER"}
|
||||
bl_label = "Remove Door on Selected Objects"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
obj = context.active_object
|
||||
props = obj.BIMDoorProperties
|
||||
def remove_door_on_object(self, obj):
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not tool.Blender.Modifier.is_door(element):
|
||||
return
|
||||
obj.BIMDoorProperties.is_editing = False
|
||||
|
||||
pset = tool.Pset.get_element_pset(element, "BBIM_Door")
|
||||
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset)
|
||||
|
||||
def _execute(self, context):
|
||||
for obj in tool.Blender.get_selected_objects():
|
||||
self.remove_door_on_object(obj)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -25,6 +25,8 @@ import logging
|
||||
import numpy as np
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.api.geometry
|
||||
import ifcopenshell.api.void
|
||||
import ifcopenshell.geom
|
||||
import ifcopenshell.util.shape
|
||||
import ifcopenshell.util.element
|
||||
@@ -35,6 +37,7 @@ import ifcopenshell.util.unit
|
||||
import bonsai.tool as tool
|
||||
import bonsai.core.geometry
|
||||
import bonsai.bim.import_ifc as import_ifc
|
||||
from collections import defaultdict
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
from math import pi, radians
|
||||
from mathutils import Vector, Matrix
|
||||
@@ -43,37 +46,27 @@ from bpy.types import SpaceView3D
|
||||
from bpy.props import FloatProperty
|
||||
from bpy_extras.object_utils import AddObjectHelper, object_data_add
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
from typing import Union, Optional, Any, cast
|
||||
|
||||
|
||||
class AddFilledOpening(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_filled_opening"
|
||||
bl_label = "Add Filled Opening"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
voided_obj: bpy.props.StringProperty()
|
||||
filling_obj: bpy.props.StringProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
FilledOpeningGenerator().generate(bpy.data.objects.get(self.filling_obj), bpy.data.objects.get(self.voided_obj))
|
||||
return {"FINISHED"}
|
||||
from typing import Union, Optional, Any, cast, Sequence
|
||||
|
||||
|
||||
class FilledOpeningGenerator:
|
||||
def generate(
|
||||
self,
|
||||
filling_obj: Union[bpy.types.Object, None],
|
||||
voided_obj: Union[bpy.types.Object, None],
|
||||
filling_obj: bpy.types.Object,
|
||||
voided_obj: bpy.types.Object,
|
||||
target: Optional[Vector] = None,
|
||||
) -> None:
|
||||
) -> Union[None, str]:
|
||||
"""
|
||||
:param target: Target opening position. If ommited, cursor position is used.
|
||||
:return: None if there was no errors, otherwise returns a string with error message.
|
||||
"""
|
||||
props = bpy.context.scene.BIMModelProperties
|
||||
opening_thickness_si = 0.0
|
||||
|
||||
filling = tool.Ifc.get_entity(filling_obj)
|
||||
element = tool.Ifc.get_entity(voided_obj)
|
||||
|
||||
if not voided_obj or not filling_obj:
|
||||
return
|
||||
|
||||
assert filling and element
|
||||
if filling.FillsVoids:
|
||||
ifcopenshell.api.run(
|
||||
"void.remove_opening", tool.Ifc.get(), opening=filling.FillsVoids[0].RelatingOpeningElement
|
||||
@@ -92,7 +85,7 @@ class FilledOpeningGenerator:
|
||||
target = filling_obj.matrix_world.translation.copy()
|
||||
raycast = voided_obj.closest_point_on_mesh(voided_obj.matrix_world.inverted() @ target, distance=0.5)
|
||||
if not raycast[0]:
|
||||
return
|
||||
return "TARGET is too far away from the voided object's mesh."
|
||||
|
||||
# In this prototype, we assume openings are only added to axis-based elements
|
||||
layers = tool.Model.get_material_layer_parameters(element)
|
||||
@@ -119,6 +112,8 @@ class FilledOpeningGenerator:
|
||||
new_matrix.translation.xyz = voided_obj.matrix_world @ local_position_on_voided_obj
|
||||
rotation_matrix = Matrix.Rotation(radians(-90), 4, "X")
|
||||
new_matrix @= rotation_matrix
|
||||
else:
|
||||
assert False, f"Unexpected layer set direction: {layers['layer_set_direction']}"
|
||||
|
||||
filling_obj.matrix_world = new_matrix
|
||||
bpy.context.view_layer.update()
|
||||
@@ -147,6 +142,7 @@ class FilledOpeningGenerator:
|
||||
representation = ifcopenshell.util.representation.get_representation(
|
||||
existing_opening_occurrence, "Model", "Body", "MODEL_VIEW"
|
||||
)
|
||||
assert representation
|
||||
representation = ifcopenshell.util.representation.resolve_representation(representation)
|
||||
else:
|
||||
representation = self.generate_opening_from_filling(
|
||||
@@ -165,7 +161,7 @@ class FilledOpeningGenerator:
|
||||
|
||||
voided_objs = [voided_obj]
|
||||
# Openings affect all subelements of an aggregate
|
||||
for subelement in ifcopenshell.util.element.get_decomposition(element):
|
||||
for subelement in tool.Aggregate.get_parts_recursively(element):
|
||||
subobj = tool.Ifc.get_object(subelement)
|
||||
if subobj:
|
||||
voided_objs.append(subobj)
|
||||
@@ -173,8 +169,10 @@ class FilledOpeningGenerator:
|
||||
for voided_obj in voided_objs:
|
||||
if voided_obj.data:
|
||||
voided_element = tool.Ifc.get_entity(voided_obj)
|
||||
assert voided_element
|
||||
context = tool.Geometry.get_active_representation_context(voided_obj)
|
||||
representation = tool.Geometry.get_representation_by_context(voided_element, context)
|
||||
assert representation
|
||||
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
@@ -223,7 +221,7 @@ class FilledOpeningGenerator:
|
||||
opening_obj = tool.Ifc.get_object(opening)
|
||||
if opening_obj:
|
||||
tool.Ifc.unlink(element=opening)
|
||||
bpy.data.objects.remove(opening_obj)
|
||||
tool.Blender.remove_data_blocks([opening_obj], remove_unused_data=True)
|
||||
|
||||
filling_obj = tool.Ifc.get_object(filling)
|
||||
representation = self.generate_opening_from_filling(filling, filling_obj)
|
||||
@@ -271,11 +269,12 @@ class FilledOpeningGenerator:
|
||||
)
|
||||
filling_obj = tool.Ifc.get_object(filling_type)
|
||||
context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
|
||||
assert context
|
||||
|
||||
if profile:
|
||||
profile = ifcopenshell.util.representation.resolve_representation(profile)
|
||||
|
||||
def get_curve_2d_from_3d(profile):
|
||||
def get_curve_2d_from_3d(profile: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
|
||||
if len(profile.Items) == 1:
|
||||
curve_3d = profile.Items[0]
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
@@ -357,7 +356,7 @@ class FilledOpeningGenerator:
|
||||
return shape_builder.get_representation(context, [extrusion])
|
||||
|
||||
def has_visible_openings(self, element):
|
||||
for opening in [r.RelatedOpeningElement for r in element.HasOpenings]:
|
||||
for opening in [r.RelatedOpeningElement for r in tool.Geometry.get_openings(element)]:
|
||||
if tool.Ifc.get_object(opening):
|
||||
return True
|
||||
return False
|
||||
@@ -446,17 +445,9 @@ class AddPotentialOpening(Operator, AddObjectHelper):
|
||||
bl_label = "Add Opening"
|
||||
bl_description = "Add an Opening object which can be applied on an Element"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
x: FloatProperty(name="X", default=0.5)
|
||||
y: FloatProperty(name="Y", default=0.5)
|
||||
z: FloatProperty(name="Z", default=0.5)
|
||||
|
||||
def draw_settings(context, layout, tool):
|
||||
row = self.layout.row()
|
||||
row.prop(data=self, property="x", label="Size X")
|
||||
row = self.layout.row()
|
||||
row.prop(data=self, property="y")
|
||||
row = self.layout.row()
|
||||
row.prop(data=self, property="z")
|
||||
x: FloatProperty(name="Size X", default=0.5, subtype="DISTANCE")
|
||||
y: FloatProperty(name="Y", default=0.5, subtype="DISTANCE")
|
||||
z: FloatProperty(name="Z", default=0.5, subtype="DISTANCE")
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.BIMModelProperties
|
||||
@@ -496,10 +487,8 @@ class AddPotentialOpening(Operator, AddObjectHelper):
|
||||
if new_matrix:
|
||||
obj.matrix_world = new_matrix
|
||||
|
||||
tool.Model.clear_scene_openings()
|
||||
|
||||
new = props.openings.add()
|
||||
new.obj = obj
|
||||
tool.Model.purge_scene_openings()
|
||||
tool.Root.add_tracked_opening(obj, "OPENING")
|
||||
|
||||
DecorationsHandler.install(context)
|
||||
return {"FINISHED"}
|
||||
@@ -511,7 +500,6 @@ class AddPotentialHalfSpaceSolid(Operator, AddObjectHelper):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.BIMModelProperties
|
||||
bm = bmesh.new()
|
||||
bmesh.ops.create_grid(bm, size=0.5)
|
||||
bm.verts.ensure_lookup_table()
|
||||
@@ -523,10 +511,8 @@ class AddPotentialHalfSpaceSolid(Operator, AddObjectHelper):
|
||||
obj = object_data_add(context, mesh, operator=self)
|
||||
obj.name = "HalfSpaceSolid"
|
||||
|
||||
tool.Model.clear_scene_openings()
|
||||
|
||||
new = props.openings.add()
|
||||
new.obj = obj
|
||||
tool.Model.purge_scene_openings()
|
||||
tool.Root.add_tracked_opening(obj, "BOOLEAN")
|
||||
|
||||
DecorationsHandler.install(context)
|
||||
return {"FINISHED"}
|
||||
@@ -540,280 +526,36 @@ class AddBoolean(Operator, tool.Ifc.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not len(context.selected_objects) == 2:
|
||||
cls.poll_message_set("Exactly 2 objects need to be selected.")
|
||||
if not len(context.selected_objects) >= 2:
|
||||
cls.poll_message_set("At least 2 objects need to be selected.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMModelProperties
|
||||
obj1, obj2 = context.selected_objects
|
||||
element1 = tool.Ifc.get_entity(obj1)
|
||||
element2 = tool.Ifc.get_entity(obj2)
|
||||
if not (bool(element1) ^ bool(element2)):
|
||||
self.report({"INFO"}, "One of the selected objects should be blender object and another IFC object.")
|
||||
return {"FINISHED"}
|
||||
first_obj = tool.Blender.get_active_object()
|
||||
if not first_obj or not tool.Geometry.is_boolean_operand(first_obj):
|
||||
self.report({"INFO"}, "At least two valid objects must be selected to add a boolean.")
|
||||
return {"CANCELLED"}
|
||||
second_objs = [
|
||||
o for o in tool.Blender.get_selected_objects() if o != first_obj and tool.Geometry.is_boolean_operand(o)
|
||||
]
|
||||
if not second_objs:
|
||||
self.report({"INFO"}, "At least two representation items must be selected to add a boolean.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# element1 - IFC object, element2 - blender object.
|
||||
if element2 and not element1:
|
||||
obj1, obj2 = obj2, obj1
|
||||
element1, element2 = element2, element1
|
||||
props = context.scene.BIMBooleanProperties
|
||||
|
||||
representation = tool.Geometry.get_active_representation(obj1)
|
||||
if not representation:
|
||||
self.report({"INFO"}, "No representation found for the selected IFC object.")
|
||||
return {"FINISHED"}
|
||||
first_item = tool.Ifc.get().by_id(first_obj.data.BIMMeshProperties.ifc_definition_id)
|
||||
second_items = [tool.Ifc.get().by_id(o.data.BIMMeshProperties.ifc_definition_id) for o in second_objs]
|
||||
booleans = ifcopenshell.api.geometry.add_boolean(tool.Ifc.get(), first_item, second_items, props.operator)
|
||||
|
||||
if not obj2.data or len(obj2.data.polygons) <= 4: # It takes 4 faces to create a closed solid
|
||||
mesh_data = {"type": "IfcHalfSpaceSolid", "matrix": obj1.matrix_world.inverted() @ obj2.matrix_world}
|
||||
else:
|
||||
mesh_data = {"type": "Mesh", "blender_obj": obj1, "blender_void": obj2}
|
||||
|
||||
booleans = ifcopenshell.api.run(
|
||||
"geometry.add_boolean", tool.Ifc.get(), representation=representation, operator="DIFFERENCE", **mesh_data
|
||||
)
|
||||
|
||||
tool.Model.mark_manual_booleans(element1, booleans)
|
||||
tool.Model.clear_scene_openings()
|
||||
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj1,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
|
||||
bpy.data.objects.remove(obj2)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ShowBooleans(Operator, tool.Ifc.Operator, AddObjectHelper):
|
||||
bl_idname = "bim.show_booleans"
|
||||
bl_label = "Show Booleans"
|
||||
bl_description = "Show booleans for the selected objects.\nCan be used to reset booleans positions"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
obj = context.active_object
|
||||
return (
|
||||
obj is not None
|
||||
and obj.data
|
||||
and hasattr(obj.data, "BIMMeshProperties")
|
||||
and obj.data.BIMMeshProperties.ifc_definition_id
|
||||
)
|
||||
|
||||
def _execute(self, context):
|
||||
selected_objects = tool.Blender.get_selected_objects()
|
||||
boolean_objs = []
|
||||
for obj in selected_objects:
|
||||
if not isinstance(mesh := obj.data, bpy.types.Mesh) or not mesh.BIMMeshProperties.ifc_definition_id:
|
||||
continue
|
||||
boolean_objs.extend(self.show_booleans(obj))
|
||||
|
||||
if boolean_objs:
|
||||
DecorationsHandler.install(context)
|
||||
tool.Blender.set_objects_selection(context, boolean_objs[0], boolean_objs)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
def show_booleans(self, obj: bpy.types.Object) -> list[bpy.types.Object]:
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
|
||||
booleans: list[ifcopenshell.entity_instance] = []
|
||||
for item in representation.Items:
|
||||
booleans.extend(self.get_booleans(item))
|
||||
|
||||
props = bpy.context.scene.BIMModelProperties
|
||||
tool.Model.clear_scene_openings()
|
||||
|
||||
existing_booleans = {
|
||||
boolean_obj.data.BIMMeshProperties.ifc_boolean_id: boolean_obj
|
||||
for opening in props.openings
|
||||
if (boolean_obj := opening.obj).data.BIMMeshProperties.obj == obj
|
||||
}
|
||||
|
||||
booleans_objs: list[bpy.types.Object] = []
|
||||
for boolean in booleans:
|
||||
boolean_obj = None
|
||||
|
||||
if boolean.is_a() == "IfcHalfSpaceSolid":
|
||||
surface = boolean.BaseSurface
|
||||
if surface.is_a("IfcPlane"):
|
||||
boolean_obj = self.create_half_space_solid()
|
||||
position = surface.Position
|
||||
position = Matrix(ifcopenshell.util.placement.get_axis2placement(position).tolist())
|
||||
position.translation *= unit_scale
|
||||
boolean_obj.matrix_world = obj.matrix_world @ position
|
||||
else:
|
||||
self.report(
|
||||
{"INFO"},
|
||||
f"Showing boolean using non-IfcPlane IfcSurface ({surface.is_a()}) is not supported.",
|
||||
)
|
||||
else:
|
||||
settings = ifcopenshell.geom.settings()
|
||||
logger = logging.getLogger("ImportIFC")
|
||||
ifc_import_settings = import_ifc.IfcImportSettings.factory(context, IfcStore.path, logger)
|
||||
shape = ifcopenshell.geom.create_shape(settings, boolean)
|
||||
if shape:
|
||||
ifc_importer = import_ifc.IfcImporter(ifc_import_settings)
|
||||
ifc_importer.file = tool.Ifc.get()
|
||||
mesh = ifc_importer.create_mesh(boolean, shape)
|
||||
else:
|
||||
mesh = None
|
||||
boolean_obj = object_data_add(bpy.context, mesh, operator=self)
|
||||
boolean_obj.name = "BooleanMesh"
|
||||
boolean_obj.matrix_world = obj.matrix_world
|
||||
|
||||
if boolean_obj:
|
||||
boolean_id = boolean.id()
|
||||
# Remove existing boolean as it will be reloaded.
|
||||
if boolean_id in existing_booleans:
|
||||
bpy.data.objects.remove(existing_booleans[boolean_id])
|
||||
boolean_obj.data.BIMMeshProperties.ifc_boolean_id = boolean_id
|
||||
boolean_obj.data.BIMMeshProperties.obj = obj
|
||||
new = props.openings.add()
|
||||
new.obj = boolean_obj
|
||||
booleans_objs.append(boolean_obj)
|
||||
|
||||
return booleans_objs
|
||||
|
||||
def get_booleans(self, item: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
|
||||
results = []
|
||||
if item.is_a("IfcBooleanResult"):
|
||||
results.extend(self.get_booleans(item.FirstOperand))
|
||||
results.append(item.SecondOperand)
|
||||
return results
|
||||
|
||||
def create_half_space_solid(self) -> bpy.types.Object:
|
||||
bm = bmesh.new()
|
||||
bmesh.ops.create_grid(bm, size=0.5)
|
||||
bm.verts.ensure_lookup_table()
|
||||
bm.edges.ensure_lookup_table()
|
||||
bm.faces.ensure_lookup_table()
|
||||
mesh = bpy.data.meshes.new(name="Dumb Opening")
|
||||
bm.to_mesh(mesh)
|
||||
bm.free()
|
||||
obj = object_data_add(bpy.context, mesh, operator=self)
|
||||
obj.name = "HalfSpaceSolid"
|
||||
return obj
|
||||
|
||||
|
||||
class HideBooleans(Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.hide_booleans"
|
||||
bl_label = "Hide Booleans"
|
||||
bl_description = (
|
||||
"Hide boolean objects and apply their changed transforms.\n"
|
||||
"If boolean object is selected, hide it, otherwise hide all boolean objects"
|
||||
)
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
ifc_file = tool.Ifc.get()
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
|
||||
builder = ifcopenshell.util.shape_builder.ShapeBuilder(ifc_file)
|
||||
|
||||
set_active_obj, set_selected_objs = None, None
|
||||
boolean_objs: list[bpy.types.Object]
|
||||
selected_objects = tool.Blender.get_selected_objects()
|
||||
selected_booleans_objs = [obj for obj in selected_objects if tool.Model.is_boolean_obj(obj)]
|
||||
if selected_booleans_objs:
|
||||
boolean_objs = selected_booleans_objs
|
||||
if (active_object := context.active_object) in selected_booleans_objs:
|
||||
active_obj_source = active_object
|
||||
else:
|
||||
active_obj_source = selected_booleans_objs[0]
|
||||
set_active_obj = active_obj_source.data.BIMMeshProperties.obj
|
||||
set_selected_objs = [tool.Model.get_booleaned_obj(obj) for obj in boolean_objs]
|
||||
else:
|
||||
props = bpy.context.scene.BIMModelProperties
|
||||
boolean_objs = [obj for o in props.openings if (obj := o.obj)]
|
||||
|
||||
for obj in boolean_objs:
|
||||
ifc_boolean_id = obj.data.BIMMeshProperties.ifc_boolean_id
|
||||
boolean = tool.Ifc.get_entity_by_id(ifc_boolean_id)
|
||||
|
||||
# Update boolean transform.
|
||||
if boolean:
|
||||
main_obj = cast(bpy.types.Object, obj.data.BIMMeshProperties.obj)
|
||||
if boolean.is_a("IfcHalfSpaceSolid"):
|
||||
surface = boolean.BaseSurface
|
||||
|
||||
# Only IfcPlane is supported currently.
|
||||
position = surface.Position
|
||||
m = Matrix(ifcopenshell.util.placement.get_axis2placement(position).tolist())
|
||||
m.translation *= unit_scale
|
||||
|
||||
# Only update if transform was changed.
|
||||
if not np.allclose(m, obj.matrix_world):
|
||||
new_m = main_obj.matrix_world.inverted() @ obj.matrix_world
|
||||
new_m.normalize()
|
||||
new_m.translation /= unit_scale
|
||||
new_m = np.array(new_m)
|
||||
ifcopenshell.util.element.remove_deep2(ifc_file, position)
|
||||
surface.Position = builder.create_axis2_placement_3d_from_matrix(new_m)
|
||||
tool.Geometry.reload_representation(main_obj)
|
||||
|
||||
bpy.data.objects.remove(obj)
|
||||
tool.Model.clear_scene_openings()
|
||||
|
||||
if set_active_obj and set_selected_objs is not None:
|
||||
tool.Blender.set_objects_selection(context, set_active_obj, set_selected_objs)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RemoveBooleans(Operator, tool.Ifc.Operator, AddObjectHelper):
|
||||
bl_idname = "bim.remove_booleans"
|
||||
bl_label = "Remove Booleans"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
upstream_obj = None
|
||||
bbim_boolean_updates = {}
|
||||
for obj in context.selected_objects:
|
||||
if (
|
||||
not obj.data
|
||||
or not hasattr(obj.data, "BIMMeshProperties")
|
||||
or not obj.data.BIMMeshProperties.ifc_boolean_id
|
||||
):
|
||||
continue
|
||||
try:
|
||||
item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_boolean_id)
|
||||
except:
|
||||
continue
|
||||
|
||||
boolean_id = None
|
||||
for inverse in tool.Ifc.get().get_inverse(item):
|
||||
if inverse.is_a("IfcBooleanResult"):
|
||||
boolean_id = inverse.id()
|
||||
break
|
||||
ifcopenshell.api.run("geometry.remove_boolean", tool.Ifc.get(), item=item)
|
||||
|
||||
if obj.data.BIMMeshProperties.obj:
|
||||
upstream_obj = obj.data.BIMMeshProperties.obj
|
||||
element = tool.Ifc.get_entity(upstream_obj)
|
||||
bbim_boolean_updates.setdefault(element, []).append(boolean_id)
|
||||
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
||||
if body:
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=upstream_obj,
|
||||
representation=body,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
bpy.data.objects.remove(obj)
|
||||
|
||||
for element, boolean_ids in bbim_boolean_updates.items():
|
||||
tool.Model.unmark_manual_booleans(element, boolean_ids)
|
||||
|
||||
tool.Blender.set_active_object(upstream_obj)
|
||||
return {"FINISHED"}
|
||||
rep_obj = bpy.context.scene.BIMGeometryProperties.representation_obj
|
||||
rep_element = tool.Ifc.get_entity(rep_obj)
|
||||
tool.Model.mark_manual_booleans(rep_element, booleans)
|
||||
tool.Geometry.reload_representation(rep_obj)
|
||||
if props.is_editing:
|
||||
bpy.ops.bim.enable_editing_booleans()
|
||||
tool.Root.reload_item_decorator()
|
||||
|
||||
|
||||
class ShowOpenings(Operator, tool.Ifc.Operator):
|
||||
@@ -823,35 +565,115 @@ class ShowOpenings(Operator, tool.Ifc.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
props = bpy.context.scene.BIMModelProperties
|
||||
objs = context.selected_objects
|
||||
|
||||
# add the obj's aggregate, as well, to objs
|
||||
for obj in objs:
|
||||
objs = list(context.selected_objects)
|
||||
# If several parts of an aggregation are selected, the aggregate openings may be queried more than once
|
||||
objects_element_map = set()
|
||||
while objs:
|
||||
obj = objs.pop(0)
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element and getattr(element, "Decomposes", None):
|
||||
if element.Decomposes and (aggregate := element.Decomposes[0].RelatingObject):
|
||||
aggregate_obj = tool.Ifc.get_object(aggregate)
|
||||
objs.append(aggregate_obj)
|
||||
if element:
|
||||
if getattr(element, "Decomposes", None):
|
||||
# Select aggregate recursively
|
||||
if element.Decomposes and (aggregate := element.Decomposes[0].RelatingObject):
|
||||
aggregate_obj = tool.Ifc.get_object(aggregate)
|
||||
objs.append(aggregate_obj)
|
||||
objects_element_map.add((obj, element))
|
||||
|
||||
for obj in objs:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not getattr(element, "HasOpenings", None):
|
||||
continue
|
||||
if tool.Ifc.is_moved(obj):
|
||||
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
||||
openings = tool.Model.load_openings(
|
||||
[
|
||||
r.RelatedOpeningElement
|
||||
for r in element.HasOpenings
|
||||
if not tool.Ifc.get_object(r.RelatedOpeningElement)
|
||||
],
|
||||
)
|
||||
for opening in openings:
|
||||
new = props.openings.add()
|
||||
new.obj = opening
|
||||
opening.display_type = "WIRE"
|
||||
for obj, element in objects_element_map:
|
||||
self.show_object_openings(obj, element)
|
||||
DecorationsHandler.install(bpy.context)
|
||||
bpy.ops.bim.update_openings_focus()
|
||||
return {"FINISHED"}
|
||||
|
||||
def show_object_openings(self, obj, element):
|
||||
openings_elements = [rel.RelatedOpeningElement for rel in tool.Geometry.get_openings(element)]
|
||||
if not openings_elements:
|
||||
return
|
||||
if tool.Ifc.is_moved(obj):
|
||||
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
||||
openings_elements_to_load = [o for o in openings_elements if not tool.Ifc.get_object(o)]
|
||||
openings_objects = tool.Model.load_openings(openings_elements_to_load)
|
||||
for opening in openings_objects:
|
||||
self.on_new_opening_obj(opening)
|
||||
|
||||
def on_new_opening_obj(self, opening_obj: bpy.types.Object) -> None:
|
||||
tool.Root.add_tracked_opening(opening_obj, "OPENING")
|
||||
opening_obj.display_type = "WIRE"
|
||||
|
||||
|
||||
class UpdateOpeningsFocus(Operator):
|
||||
bl_idname = "bim.update_openings_focus"
|
||||
bl_label = "Update Openings Focus"
|
||||
bl_description = "Show objects that are not part of the object or its openings as transparent"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
preferences = tool.Blender.get_addon_preferences()
|
||||
if preferences.opening_focus_opacity == 100:
|
||||
return {"FINISHED"}
|
||||
openings = set()
|
||||
building_objects = set()
|
||||
for opening in context.scene.BIMModelProperties.openings:
|
||||
if opening.obj:
|
||||
openings.add(opening.obj)
|
||||
opening_element = tool.Ifc.get_entity(opening.obj)
|
||||
if opening_element is None: # Most likely an opening created with bim.add_potential_opening
|
||||
continue
|
||||
building_element = opening_element.VoidsElements[0].RelatingBuildingElement
|
||||
building_objects.add(tool.Ifc.get_object(building_element))
|
||||
|
||||
for obj in context.scene.objects:
|
||||
obj.color = [
|
||||
obj.color[0],
|
||||
obj.color[1],
|
||||
obj.color[2],
|
||||
(
|
||||
1
|
||||
if not context.scene.BIMModelProperties.openings
|
||||
or not building_objects
|
||||
or obj in openings
|
||||
or obj in building_objects
|
||||
or obj in context.selected_objects
|
||||
else preferences.opening_focus_opacity / 100
|
||||
),
|
||||
]
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def hide_openings(context: bpy.types.Context, objects: Sequence[bpy.types.Object]) -> None:
|
||||
objects_to_remove = set()
|
||||
for opening_prop in context.scene.BIMModelProperties.openings:
|
||||
opening_obj = opening_prop.obj
|
||||
if not opening_obj:
|
||||
continue
|
||||
opening_element = tool.Ifc.get_entity(opening_obj)
|
||||
if opening_element:
|
||||
if not opening_element.is_a("IfcOpeningElement"):
|
||||
# This opening has been assigned to another ifc class. Remove it from the openings pool. See #3854
|
||||
opening_prop.obj = None
|
||||
continue
|
||||
building_element = opening_element.VoidsElements[0].RelatingBuildingElement
|
||||
if building_element:
|
||||
building_obj = tool.Ifc.get_object(building_element)
|
||||
if building_obj in objects:
|
||||
tool.Ifc.unlink(element=opening_element)
|
||||
objects_to_remove.add(opening_obj)
|
||||
if opening_obj in objects:
|
||||
objects_to_remove.add(opening_obj)
|
||||
|
||||
tool.Blender.remove_data_blocks(objects_to_remove, remove_unused_data=True)
|
||||
tool.Model.purge_scene_openings()
|
||||
bpy.ops.bim.update_openings_focus()
|
||||
|
||||
|
||||
class HideAllOpenings(Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.hide_all_openings"
|
||||
bl_label = "Hide All Openings"
|
||||
bl_description = "Hide every single opening"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
hide_openings(context, context.scene.objects[:])
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -862,23 +684,7 @@ class HideOpenings(Operator, tool.Ifc.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
to_delete = set()
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
if element.is_a("IfcOpeningElement"):
|
||||
element = element.VoidsElements[0].RelatingBuildingElement
|
||||
obj = tool.Ifc.get_object(element)
|
||||
openings = [r.RelatedOpeningElement for r in element.HasOpenings]
|
||||
for opening in openings:
|
||||
opening_obj = tool.Ifc.get_object(opening)
|
||||
if opening_obj:
|
||||
to_delete.add((opening, opening_obj))
|
||||
for opening, opening_obj in to_delete:
|
||||
tool.Ifc.unlink(element=opening)
|
||||
bpy.data.objects.remove(opening_obj)
|
||||
tool.Model.clear_scene_openings()
|
||||
hide_openings(context, context.selected_objects[:])
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -887,51 +693,90 @@ class EditOpenings(Operator, tool.Ifc.Operator):
|
||||
bl_label = "Edit"
|
||||
bl_description = "Edit Openings"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
apply_all: bpy.props.BoolProperty(default=False)
|
||||
|
||||
def _execute(self, context):
|
||||
props = bpy.context.scene.BIMModelProperties
|
||||
building_objs = set()
|
||||
current_openings = bpy.data.collections.get("IfcOpeningElement").objects
|
||||
|
||||
for obj in current_openings:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
if element.is_a("IfcOpeningElement"):
|
||||
element = element.VoidsElements[0].RelatingBuildingElement
|
||||
obj = tool.Ifc.get_object(element)
|
||||
openings = [r.RelatedOpeningElement for r in element.HasOpenings]
|
||||
for opening in openings:
|
||||
opening_obj = tool.Ifc.get_object(opening)
|
||||
building_objs.add(obj)
|
||||
|
||||
similar_openings = bonsai.core.geometry.get_similar_openings(tool.Ifc, opening)
|
||||
similar_openings_building_objs = bonsai.core.geometry.get_similar_openings_building_objs(
|
||||
tool.Ifc, similar_openings
|
||||
)
|
||||
building_objs.update(similar_openings_building_objs)
|
||||
|
||||
if opening_obj:
|
||||
if tool.Ifc.is_edited(opening_obj):
|
||||
tool.Geometry.run_geometry_update_representation(obj=opening_obj)
|
||||
bonsai.core.geometry.edit_similar_opening_placement(tool.Geometry, opening, similar_openings)
|
||||
elif tool.Ifc.is_moved(opening_obj):
|
||||
bonsai.core.geometry.edit_object_placement(
|
||||
tool.Ifc, tool.Geometry, tool.Surveyor, obj=opening_obj
|
||||
)
|
||||
bonsai.core.geometry.edit_similar_opening_placement(tool.Geometry, opening, similar_openings)
|
||||
|
||||
building_objs.update(
|
||||
self.get_all_building_objects_of_similar_openings(opening)
|
||||
) # NB this has nothing to do with clone similar_opening
|
||||
tool.Ifc.unlink(element=opening)
|
||||
if bpy.context.scene.BIMGeometryProperties.representation_obj == opening_obj:
|
||||
bpy.context.scene.BIMGeometryProperties.representation_obj = None
|
||||
bpy.data.objects.remove(opening_obj)
|
||||
building_objs, opening_elements = self.get_buildings_and_openings(context)
|
||||
self.edit_openings(building_objs, opening_elements)
|
||||
|
||||
tool.Model.purge_scene_openings()
|
||||
tool.Model.reload_body_representation(building_objs)
|
||||
bpy.ops.bim.update_openings_focus()
|
||||
return {"FINISHED"}
|
||||
|
||||
def get_buildings_and_openings(
|
||||
self, context: bpy.types.Context
|
||||
) -> tuple[set[bpy.types.Object], set[ifcopenshell.entity_instance]]:
|
||||
props = context.scene.BIMModelProperties
|
||||
building_objs: set[bpy.types.Object] = set()
|
||||
opening_elements: set[ifcopenshell.entity_instance] = set()
|
||||
objects_to_remove = set()
|
||||
if self.apply_all:
|
||||
for opening_prop in props.openings:
|
||||
opening_obj = opening_prop.obj
|
||||
if opening_obj is None:
|
||||
continue
|
||||
opening_element = tool.Ifc.get_entity(opening_obj)
|
||||
if opening_element is None:
|
||||
objects_to_remove.add(opening_obj)
|
||||
continue
|
||||
opening_elements.add(opening_element)
|
||||
building_element = opening_element.VoidsElements[0].RelatingBuildingElement
|
||||
building_objs.add(tool.Ifc.get_object(building_element))
|
||||
else:
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element.is_a("IfcOpeningElement"):
|
||||
opening_element = element
|
||||
opening_elements.add(opening_element)
|
||||
if opening_element.VoidsElements:
|
||||
building_element = opening_element.VoidsElements[0].RelatingBuildingElement
|
||||
building_obj = tool.Ifc.get_object(building_element)
|
||||
if building_obj:
|
||||
building_objs.add(building_obj)
|
||||
else:
|
||||
for relation in tool.Geometry.get_openings(element):
|
||||
opening_element = relation.RelatedOpeningElement
|
||||
if tool.Ifc.get_object(opening_element):
|
||||
opening_elements.add(opening_element)
|
||||
building_objs.add(obj)
|
||||
tool.Blender.remove_data_blocks(objects_to_remove, remove_unused_data=True)
|
||||
return building_objs, opening_elements
|
||||
|
||||
def edit_openings(
|
||||
self, building_objs: set[bpy.types.Object], opening_elements: set[ifcopenshell.entity_instance]
|
||||
) -> None:
|
||||
objects_to_remove: set[bpy.types.Object] = set()
|
||||
for opening_element in opening_elements:
|
||||
opening_obj = tool.Ifc.get_object(opening_element)
|
||||
|
||||
similar_openings = bonsai.core.geometry.get_similar_openings(tool.Ifc, opening_element)
|
||||
similar_openings_building_objs = bonsai.core.geometry.get_similar_openings_building_objs(
|
||||
tool.Ifc, similar_openings
|
||||
)
|
||||
building_objs.update(similar_openings_building_objs)
|
||||
|
||||
if opening_obj:
|
||||
if tool.Ifc.is_edited(opening_obj):
|
||||
tool.Geometry.run_geometry_update_representation(obj=opening_obj)
|
||||
bonsai.core.geometry.edit_similar_opening_placement(
|
||||
tool.Geometry, opening_element, similar_openings
|
||||
)
|
||||
elif tool.Ifc.is_moved(opening_obj):
|
||||
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=opening_obj)
|
||||
bonsai.core.geometry.edit_similar_opening_placement(
|
||||
tool.Geometry, opening_element, similar_openings
|
||||
)
|
||||
|
||||
building_objs.update(
|
||||
self.get_all_building_objects_of_similar_openings(opening_element)
|
||||
) # NB this has nothing to do with clone similar_opening
|
||||
tool.Ifc.unlink(element=opening_element)
|
||||
if bpy.context.scene.BIMGeometryProperties.representation_obj == opening_obj:
|
||||
bpy.context.scene.BIMGeometryProperties.representation_obj = None
|
||||
objects_to_remove.add(opening_obj)
|
||||
tool.Blender.remove_data_blocks(objects_to_remove, remove_unused_data=True)
|
||||
|
||||
def get_all_building_objects_of_similar_openings(self, opening):
|
||||
if not opening.is_a("IfcOpeningElement") or not opening.HasFillings:
|
||||
return []
|
||||
@@ -952,20 +797,28 @@ class EditOpenings(Operator, tool.Ifc.Operator):
|
||||
class CloneOpening(Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.clone_opening"
|
||||
bl_label = "Clone Opening"
|
||||
bl_description = "Clone the selected Opening and assign to the selected Element"
|
||||
bl_description = "Clone the active Opening object and assign to the selected Element"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
objects = bpy.context.selected_objects
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if len(context.selected_objects) != 2:
|
||||
cls.poll_message_set("Exactly 2 objects must be selected.")
|
||||
return False
|
||||
return True
|
||||
|
||||
for obj in objects:
|
||||
entity = tool.Ifc.get_entity(obj)
|
||||
if entity.is_a() == "IfcWall":
|
||||
wall = entity
|
||||
continue
|
||||
if entity.is_a() == "IfcOpeningElement":
|
||||
opening = entity
|
||||
continue
|
||||
def _execute(self, context):
|
||||
# NOTE: Operator displayed in UI only with IfcOpeningElement being active.
|
||||
ifc_file = tool.Ifc.get()
|
||||
objects = bpy.context.selected_objects
|
||||
opening_obj = context.active_object
|
||||
assert opening_obj
|
||||
opening = tool.Ifc.get_entity(opening_obj)
|
||||
assert opening and opening.is_a("IfcOpeningElement")
|
||||
|
||||
voided_obj = next(o for o in objects if o != opening_obj)
|
||||
voided_element = tool.Ifc.get_entity(voided_obj)
|
||||
assert voided_element
|
||||
|
||||
opening_placement = opening.ObjectPlacement
|
||||
opening_representation = opening.Representation
|
||||
@@ -973,8 +826,127 @@ class CloneOpening(Operator, tool.Ifc.Operator):
|
||||
new_opening = ifcopenshell.api.run("root.create_entity", tool.Ifc.get(), ifc_class="IfcOpeningElement")
|
||||
new_opening.Representation = opening_representation
|
||||
|
||||
ifcopenshell.api.run("void.add_opening", tool.Ifc.get(), opening=new_opening, element=wall)
|
||||
ifcopenshell.api.void.add_opening(ifc_file, opening=new_opening, element=voided_element)
|
||||
new_opening.ObjectPlacement = opening_placement
|
||||
|
||||
# Update affected representations.
|
||||
elements_to_update = tool.Aggregate.get_parts_recursively(voided_element)
|
||||
for element in elements_to_update:
|
||||
obj = tool.Ifc.get_object(element)
|
||||
if not isinstance(obj, bpy.types.Object) or not isinstance(obj.data, bpy.types.Mesh):
|
||||
continue
|
||||
representation = tool.Geometry.get_active_representation(obj)
|
||||
assert representation
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class PurgeUnusedOpenings(Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.purge_unused_openings"
|
||||
bl_label = "Purge Unused Openings"
|
||||
bl_description = "Purge Openings that do not intersect with their related building element"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return any(
|
||||
[tool.Geometry.has_openings(element)]
|
||||
for element in [tool.Ifc.get_entity(obj) for obj in context.selected_objects]
|
||||
if element
|
||||
)
|
||||
|
||||
def _execute(self, context):
|
||||
bpy.ops.bim.show_openings()
|
||||
objects = context.selected_objects[:]
|
||||
[o.select_set(False) for o in objects]
|
||||
active_object = context.active_object
|
||||
purged = 0
|
||||
for obj in objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not tool.Geometry.has_openings(element):
|
||||
continue
|
||||
obj_bvh_tree = tool.Geometry.get_bvh_tree(obj)
|
||||
for opening_rel in tool.Geometry.get_openings(element):
|
||||
opening_elt = opening_rel.RelatedOpeningElement
|
||||
opening_obj = tool.Ifc.get_object(opening_elt)
|
||||
opening_bvh_tree = tool.Geometry.get_bvh_tree(opening_obj)
|
||||
if not opening_bvh_tree.overlap(obj_bvh_tree):
|
||||
opening_obj.select_set(True)
|
||||
purged += 1
|
||||
if context.selected_objects:
|
||||
bpy.ops.bim.override_object_delete(is_batch=False)
|
||||
bpy.ops.bim.edit_openings(apply_all=True)
|
||||
[o.select_set(True) for o in objects]
|
||||
context.view_layer.objects.active = active_object
|
||||
self.report({"INFO"}, f"{purged} unused openings were purged.")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RemoveBoolean(Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.remove_boolean"
|
||||
bl_label = "Remove Boolean"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Removes the actively selected boolean"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
props = context.scene.BIMBooleanProperties
|
||||
return props.active_boolean
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMBooleanProperties
|
||||
ifcopenshell.api.geometry.remove_boolean(
|
||||
tool.Ifc.get(), tool.Ifc.get().by_id(props.active_boolean.ifc_definition_id)
|
||||
)
|
||||
bpy.ops.bim.enable_editing_booleans()
|
||||
rep_obj = bpy.context.scene.BIMGeometryProperties.representation_obj
|
||||
tool.Geometry.reload_representation(rep_obj)
|
||||
tool.Root.reload_item_decorator()
|
||||
|
||||
|
||||
class SelectBoolean(Operator):
|
||||
bl_idname = "bim.select_boolean"
|
||||
bl_label = "Remove Boolean"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Selects operands of the active boolean\nSHIFT-CLICK to select all operands recursively"
|
||||
is_recursive: bpy.props.BoolProperty(name="Is Recursive", default=False, options={"SKIP_SAVE"})
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
props = context.scene.BIMBooleanProperties
|
||||
return props.active_boolean
|
||||
|
||||
def invoke(self, context, event):
|
||||
if event.type == "LEFTMOUSE" and event.shift:
|
||||
self.is_recursive = True
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.BIMBooleanProperties
|
||||
queue = [tool.Ifc.get().by_id(props.active_boolean.ifc_definition_id)]
|
||||
items = {i.ifc_definition_id: i.obj for i in context.scene.BIMGeometryProperties.item_objs}
|
||||
while queue:
|
||||
item = queue.pop()
|
||||
if item.is_a("IfcBooleanResult"):
|
||||
if self.is_recursive:
|
||||
queue.append(item.FirstOperand)
|
||||
queue.append(item.SecondOperand)
|
||||
else:
|
||||
if obj := items.get(item.FirstOperand.id()):
|
||||
tool.Blender.select_object(obj)
|
||||
if obj := items.get(item.SecondOperand.id()):
|
||||
tool.Blender.select_object(obj)
|
||||
elif obj := items.get(item.id()):
|
||||
tool.Blender.select_object(obj)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -1004,6 +976,8 @@ class DecorationsHandler:
|
||||
batch.draw(shader)
|
||||
|
||||
def __call__(self, context):
|
||||
if not context.scene.BIMModelProperties.openings:
|
||||
return
|
||||
self.addon_prefs = tool.Blender.get_addon_preferences()
|
||||
selected_elements_color = self.addon_prefs.decorator_color_selected
|
||||
unselected_elements_color = self.addon_prefs.decorator_color_unselected
|
||||
@@ -1019,6 +993,12 @@ class DecorationsHandler:
|
||||
|
||||
for opening in context.scene.BIMModelProperties.openings:
|
||||
obj = opening.obj
|
||||
if context.scene.BIMGeometryProperties.representation_obj == obj:
|
||||
# We are editing the representation of the opening :
|
||||
for item in context.scene.BIMGeometryProperties.item_objs:
|
||||
if item.obj.mode == "EDIT":
|
||||
obj = item.obj
|
||||
break
|
||||
if not obj:
|
||||
continue
|
||||
|
||||
@@ -1069,9 +1049,14 @@ class DecorationsHandler:
|
||||
bm.from_mesh(obj.data)
|
||||
|
||||
verts = [tuple(obj.matrix_world @ v.co) for v in bm.verts]
|
||||
edges = [tuple([v.index for v in e.verts]) for e in bm.edges]
|
||||
if ios_edges_attribute := obj.data.attributes.get("ios_edges"):
|
||||
edges = [e for i, e in enumerate(bm.edges) if ios_edges_attribute.data[i].value]
|
||||
else:
|
||||
edges = bm.edges
|
||||
edges_indices = [tuple([v.index for v in e.verts]) for e in edges]
|
||||
|
||||
color = selected_elements_color if obj in context.selected_objects else special_elements_color
|
||||
self.draw_batch("LINES", verts, color, edges)
|
||||
self.draw_batch("LINES", verts, color, edges_indices)
|
||||
|
||||
obj.data.calc_loop_triangles()
|
||||
tris = [tuple(t.vertices) for t in obj.data.loop_triangles]
|
||||
|
||||
@@ -35,14 +35,440 @@ import bonsai.core.geometry
|
||||
import bonsai.core.model as core
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
from math import pi, sin, cos, degrees
|
||||
from mathutils import Vector, Matrix
|
||||
from math import pi, sin, cos, degrees, tan
|
||||
from mathutils import Vector, Matrix, Quaternion
|
||||
from bonsai.bim.module.model.opening import FilledOpeningGenerator
|
||||
from bonsai.bim.module.model.decorator import PolylineDecorator
|
||||
from bonsai.bim.module.geometry.decorator import ItemDecorator
|
||||
from typing import Optional, Union, Literal
|
||||
from lark import Lark, Transformer
|
||||
|
||||
|
||||
def get_wall_preview_data(context, relating_type):
|
||||
def create_bmesh_from_vertices(vertices):
|
||||
bm = bmesh.new()
|
||||
|
||||
new_verts = [bm.verts.new(v) for v in polyline_vertices]
|
||||
if is_closed:
|
||||
new_edges = [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
|
||||
new_edges.append(
|
||||
bm.edges.new((new_verts[-1], new_verts[0]))
|
||||
) # Add an edge between the last an first point to make it closed.
|
||||
else:
|
||||
new_edges = [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
return bm
|
||||
|
||||
# Get properties from object type
|
||||
layers = tool.Model.get_material_layer_parameters(relating_type)
|
||||
if not layers["thickness"]:
|
||||
return
|
||||
thickness = layers["thickness"]
|
||||
model_props = context.scene.BIMModelProperties
|
||||
direction_sense = model_props.direction_sense
|
||||
direction = 1
|
||||
if direction_sense == "NEGATIVE":
|
||||
direction = -1
|
||||
offset_type = model_props.offset_type
|
||||
offset = 0
|
||||
if offset_type == "CENTER":
|
||||
offset = -thickness / 2
|
||||
elif offset_type == "INTERIOR":
|
||||
offset = -thickness
|
||||
|
||||
unit_system = tool.Drawing.get_unit_system()
|
||||
factor = 1
|
||||
if unit_system == "IMPERIAL":
|
||||
factor = 3.28084
|
||||
if unit_system == "METRIC":
|
||||
unit_length = context.scene.unit_settings.length_unit
|
||||
if unit_length == "MILLIMETERS":
|
||||
factor = 1000
|
||||
|
||||
# For the model properties, the offset value should just be converted
|
||||
# However, for the wall preview logic that follows, offset and thickness must change direction
|
||||
model_props.offset = offset * factor
|
||||
thickness *= direction
|
||||
offset *= direction
|
||||
|
||||
height = float(model_props.extrusion_depth)
|
||||
rl = float(model_props.rl1)
|
||||
x_angle = float(model_props.x_angle)
|
||||
angle_distortion = height * tan(x_angle)
|
||||
|
||||
data = {}
|
||||
data["verts"] = []
|
||||
|
||||
# Verts
|
||||
polyline_vertices = []
|
||||
polyline_data = context.scene.BIMPolylineProperties.insertion_polyline
|
||||
polyline_points = polyline_data[0].polyline_points if polyline_data else []
|
||||
if len(polyline_points) < 2:
|
||||
data = []
|
||||
return
|
||||
for point in polyline_points:
|
||||
polyline_vertices.append(Vector((point.x, point.y, point.z)))
|
||||
|
||||
is_closed = False
|
||||
if (
|
||||
polyline_vertices[0].x == polyline_vertices[-1].x
|
||||
and polyline_vertices[0].y == polyline_vertices[-1].y
|
||||
and polyline_vertices[0].z == polyline_vertices[-1].z
|
||||
):
|
||||
is_closed = True
|
||||
polyline_vertices.pop(-1) # Remove the last point. The edges are going to inform that the shape is closed.
|
||||
|
||||
bm_base = create_bmesh_from_vertices(polyline_vertices)
|
||||
base_vertices = tool.Cad.offset_edges(bm_base, offset)
|
||||
offset_base_verts = tool.Cad.offset_edges(bm_base, thickness + offset)
|
||||
top_vertices = tool.Cad.offset_edges(bm_base, angle_distortion + offset)
|
||||
offset_top_verts = tool.Cad.offset_edges(bm_base, angle_distortion + thickness + offset)
|
||||
if is_closed:
|
||||
base_vertices.append(base_vertices[0])
|
||||
offset_base_verts.append(offset_base_verts[0])
|
||||
top_vertices.append(top_vertices[0])
|
||||
offset_top_verts.append(offset_top_verts[0])
|
||||
|
||||
if offset_base_verts is not None:
|
||||
for v in base_vertices:
|
||||
data["verts"].append((v.co.x, v.co.y, v.co.z + rl))
|
||||
|
||||
for v in offset_base_verts[::-1]:
|
||||
data["verts"].append((v.co.x, v.co.y, v.co.z + rl))
|
||||
|
||||
for v in top_vertices:
|
||||
data["verts"].append((v.co.x, v.co.y, v.co.z + rl + height))
|
||||
|
||||
for v in offset_top_verts[::-1]:
|
||||
data["verts"].append((v.co.x, v.co.y, v.co.z + rl + height))
|
||||
|
||||
bm_base.free()
|
||||
|
||||
# Edges and Tris
|
||||
points = []
|
||||
side_edges_1 = []
|
||||
side_edges_2 = []
|
||||
base_edges = []
|
||||
|
||||
for i in range(len(data["verts"])):
|
||||
points.append(Vector(data["verts"][i]))
|
||||
|
||||
n = len(points) // 2
|
||||
bottom_side_1 = [[i, (i + 1) % (n)] for i in range((n - 1) // 2)]
|
||||
bottom_side_2 = [[i, (i + 1) % (n)] for i in range(n // 2, n - 1)]
|
||||
bottom_connections = [[i, n - i - 1] for i in range(n // 2)]
|
||||
bottom_loop = bottom_connections + bottom_side_1 + bottom_side_2
|
||||
side_edges_1.extend(bottom_side_1)
|
||||
side_edges_2.extend(bottom_side_2)
|
||||
base_edges.extend(bottom_loop)
|
||||
|
||||
upper_side_1 = [[i + n for i in edges] for edges in bottom_side_1]
|
||||
upper_side_2 = [[i + n for i in edges] for edges in bottom_side_2]
|
||||
upper_loop = [[i + n for i in edges] for edges in bottom_loop]
|
||||
side_edges_1.extend(upper_side_1)
|
||||
side_edges_2.extend(upper_side_2)
|
||||
base_edges.extend(upper_loop)
|
||||
|
||||
loops = [side_edges_1, side_edges_2, base_edges]
|
||||
|
||||
data["edges"] = []
|
||||
data["tris"] = []
|
||||
for i, group in enumerate(loops):
|
||||
bm = bmesh.new()
|
||||
|
||||
new_verts = [bm.verts.new(v) for v in points]
|
||||
new_edges = [bm.edges.new((new_verts[e[0]], new_verts[e[1]])) for e in group]
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
|
||||
if i == 2:
|
||||
new_faces = bmesh.ops.contextual_create(bm, geom=bm.edges)
|
||||
new_faces = bmesh.ops.bridge_loops(bm, edges=bm.edges, use_pairs=True, use_cyclic=True)
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
edges = [[v.index for v in e.verts] for e in bm.edges]
|
||||
tris = [[l.vert.index for l in loop] for loop in bm.calc_loop_triangles()]
|
||||
data["edges"].extend(edges)
|
||||
data["tris"].extend(tris)
|
||||
|
||||
data["edges"] = list(set(tuple(e) for e in data["edges"]))
|
||||
data["tris"] = list(set(tuple(t) for t in data["tris"]))
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_vertical_profile_preview_data(context, relating_type):
|
||||
material = ifcopenshell.util.element.get_material(relating_type)
|
||||
try:
|
||||
profile = material.MaterialProfiles[0].Profile
|
||||
except:
|
||||
return {}
|
||||
|
||||
model_props = context.scene.BIMModelProperties
|
||||
extrusion_depth = model_props.extrusion_depth
|
||||
cardinal_point = model_props.cardinal_point
|
||||
rot_mat = Quaternion()
|
||||
if relating_type.is_a("IfcBeamType"):
|
||||
y_rot = Quaternion((0.0, 1.0, 0.0), radians(90))
|
||||
z_rot = Quaternion((0.0, 0.0, 1.0), radians(90))
|
||||
rot_mat = y_rot @ z_rot
|
||||
# Get profile data
|
||||
settings = ifcopenshell.geom.settings()
|
||||
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
|
||||
shape = ifcopenshell.geom.create_shape(settings, profile)
|
||||
|
||||
verts = shape.verts
|
||||
if not verts:
|
||||
raise RuntimeError("Profile shape has no vertices, it probably is invalid.")
|
||||
|
||||
edges = shape.edges
|
||||
|
||||
grouped_verts = [[verts[i], verts[i + 1], 0] for i in range(0, len(verts), 3)]
|
||||
grouped_edges = [[edges[i], edges[i + 1]] for i in range(0, len(edges), 2)]
|
||||
|
||||
# Create offsets based on cardinal point
|
||||
min_x = min(v[0] for v in grouped_verts)
|
||||
max_x = max(v[0] for v in grouped_verts)
|
||||
min_y = min(v[1] for v in grouped_verts)
|
||||
max_y = max(v[1] for v in grouped_verts)
|
||||
|
||||
x_offset = (max_x - min_x) / 2
|
||||
y_offset = (max_y - min_y) / 2
|
||||
|
||||
match cardinal_point:
|
||||
case "1":
|
||||
grouped_verts = [(v[0] - x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
|
||||
case "2":
|
||||
grouped_verts = [(v[0], v[1] + y_offset, v[2]) for v in grouped_verts]
|
||||
case "3":
|
||||
grouped_verts = [(v[0] + x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
|
||||
case "4":
|
||||
grouped_verts = [(v[0] - x_offset, v[1], v[2]) for v in grouped_verts]
|
||||
case "5":
|
||||
grouped_verts = [(v[0], v[1], v[2]) for v in grouped_verts]
|
||||
case "6":
|
||||
grouped_verts = [(v[0] + x_offset, v[1], v[2]) for v in grouped_verts]
|
||||
case "7":
|
||||
grouped_verts = [(v[0] - x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
|
||||
case "8":
|
||||
grouped_verts = [(v[0], v[1] - y_offset, v[2]) for v in grouped_verts]
|
||||
case "9":
|
||||
grouped_verts = [(v[0] + x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
|
||||
|
||||
# Create extrusion bmesh
|
||||
bm = bmesh.new()
|
||||
|
||||
grouped_verts.append(grouped_verts[0]) # Close profile
|
||||
new_verts = [bm.verts.new(v) for v in grouped_verts]
|
||||
new_edges = [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(grouped_verts) - 1)]
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
|
||||
|
||||
new_faces = bmesh.ops.contextual_create(bm, geom=bm.edges)
|
||||
|
||||
new_faces = bmesh.ops.extrude_face_region(bm, geom=bm.faces, use_dissolve_ortho_edges=True)
|
||||
new_verts = [e for e in new_faces["geom"] if isinstance(e, bmesh.types.BMVert)]
|
||||
new_faces = bmesh.ops.translate(bm, verts=new_verts, vec=(0.0, 0.0, extrusion_depth))
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()]
|
||||
|
||||
# Calculate rotation, mouse position, angle and cardinal point
|
||||
snap_prop = context.scene.BIMPolylineProperties.snap_mouse_point[0]
|
||||
mouse_point = Vector((snap_prop.x, snap_prop.y, snap_prop.z))
|
||||
data = {}
|
||||
|
||||
verts = [tuple(v.co) for v in bm.verts]
|
||||
verts = [tuple(rot_mat @ Vector(v)) for v in verts]
|
||||
verts = [tuple(Vector(v) + mouse_point) for v in verts]
|
||||
min_z = min(v.co.z for v in bm.verts)
|
||||
max_z = max(v.co.z for v in bm.verts)
|
||||
# Add axis verts
|
||||
verts.append(tuple(mouse_point))
|
||||
verts.append(tuple(mouse_point + Vector((0, 0, max_z))))
|
||||
# Add only profile edges
|
||||
edges = []
|
||||
for edge in bm.edges:
|
||||
if (edge.verts[0].co.z == min_z and edge.verts[1].co.z == min_z) or (
|
||||
edge.verts[0].co.z == max_z and edge.verts[1].co.z == max_z
|
||||
):
|
||||
edges.append(edge)
|
||||
# Add axis edge
|
||||
edges = [(edge.verts[0].index, edge.verts[1].index) for edge in edges]
|
||||
edges.append((len(verts) - 1, len(verts) - 2))
|
||||
data["verts"] = verts
|
||||
data["edges"] = edges
|
||||
data["tris"] = tris
|
||||
|
||||
bm.free()
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_horizontal_profile_preview_data(context, relating_type):
|
||||
material = ifcopenshell.util.element.get_material(relating_type)
|
||||
try:
|
||||
profile_curve = material.MaterialProfiles[0].Profile
|
||||
except:
|
||||
return {}
|
||||
|
||||
model_props = context.scene.BIMModelProperties
|
||||
cardinal_point = model_props.cardinal_point
|
||||
|
||||
polyline_verts = []
|
||||
polyline_data = context.scene.BIMPolylineProperties.insertion_polyline
|
||||
polyline_points = polyline_data[0].polyline_points if polyline_data else []
|
||||
if len(polyline_points) < 2:
|
||||
return
|
||||
for point in polyline_points:
|
||||
polyline_verts.append(Vector((point.x, point.y, point.z)))
|
||||
polyline_edges = [(i, i + 1) for i in range(len(polyline_verts) - 1)]
|
||||
|
||||
# Get profile shape
|
||||
settings = ifcopenshell.geom.settings()
|
||||
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
|
||||
shape = ifcopenshell.geom.create_shape(settings, profile_curve)
|
||||
|
||||
verts = shape.verts
|
||||
if not verts:
|
||||
raise RuntimeError("Profile shape has no vertices, it probably is invalid.")
|
||||
|
||||
edges = shape.edges
|
||||
|
||||
grouped_verts = [[verts[i], verts[i + 1], 0] for i in range(0, len(verts), 3)]
|
||||
grouped_edges = [[edges[i], edges[i + 1]] for i in range(0, len(edges), 2)]
|
||||
|
||||
# Create offsets based on cardinal point
|
||||
min_x = min(v[0] for v in grouped_verts)
|
||||
max_x = max(v[0] for v in grouped_verts)
|
||||
min_y = min(v[1] for v in grouped_verts)
|
||||
max_y = max(v[1] for v in grouped_verts)
|
||||
|
||||
x_offset = (max_x - min_x) / 2
|
||||
y_offset = (max_y - min_y) / 2
|
||||
|
||||
match cardinal_point:
|
||||
case "1":
|
||||
grouped_verts = [(v[0] - x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
|
||||
case "2":
|
||||
grouped_verts = [(v[0], v[1] + y_offset, v[2]) for v in grouped_verts]
|
||||
case "3":
|
||||
grouped_verts = [(v[0] + x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
|
||||
case "4":
|
||||
grouped_verts = [(v[0] - x_offset, v[1], v[2]) for v in grouped_verts]
|
||||
case "5":
|
||||
grouped_verts = [(v[0], v[1], v[2]) for v in grouped_verts]
|
||||
case "6":
|
||||
grouped_verts = [(v[0] + x_offset, v[1], v[2]) for v in grouped_verts]
|
||||
case "7":
|
||||
grouped_verts = [(v[0] - x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
|
||||
case "8":
|
||||
grouped_verts = [(v[0], v[1] - y_offset, v[2]) for v in grouped_verts]
|
||||
case "9":
|
||||
grouped_verts = [(v[0] + x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
|
||||
|
||||
# Create profile curve
|
||||
scale_mat = Matrix.Scale(-1, 4, (1.0, 0.0, 0.0))
|
||||
grouped_verts = [scale_mat @ Vector(v) for v in grouped_verts]
|
||||
profile_curve = bpy.data.curves.new("Profile", type="CURVE")
|
||||
profile_curve.dimensions = "2D"
|
||||
profile_curve.splines.new("POLY")
|
||||
profile_curve.splines[0].points.add(len(grouped_verts))
|
||||
|
||||
for i, point in enumerate(profile_curve.splines[0].points):
|
||||
if i == len(grouped_verts): # Close curve
|
||||
point.co = Vector((*grouped_verts[0], 0))
|
||||
continue
|
||||
point.co = Vector((*grouped_verts[i], 0))
|
||||
profile_obj = bpy.data.objects.new("Profile", profile_curve)
|
||||
|
||||
# Create path curve with profile object as bevel
|
||||
path_curve = bpy.data.curves.new("Polyline", type="CURVE")
|
||||
path_curve.dimensions = "2D"
|
||||
path_curve.splines.new("POLY")
|
||||
path_curve.splines[0].points.add(len(polyline_verts) - 1)
|
||||
for i, point in enumerate(path_curve.splines[0].points):
|
||||
point.co = Vector((*polyline_verts[i], 0))
|
||||
path_curve.splines[0].use_smooth = False
|
||||
path_curve.bevel_mode = "OBJECT"
|
||||
path_curve.bevel_object = profile_obj
|
||||
|
||||
# Convert path curve to mesh
|
||||
# This operation throws a warning when done during gpu drawing, so it was removed from the decorator file to be handled here
|
||||
path_obj = bpy.data.objects.new("Preview", path_curve)
|
||||
context.scene.collection.objects.link(path_obj)
|
||||
bpy.context.view_layer.objects.active = path_obj
|
||||
dg = context.evaluated_depsgraph_get()
|
||||
path_obj = path_obj.evaluated_get(dg)
|
||||
me = path_obj.to_mesh()
|
||||
|
||||
# Create bmesh from path mesh
|
||||
bm = bmesh.new()
|
||||
new_verts = [bm.verts.new(v.co) for v in me.vertices]
|
||||
index = [[v for v in edge.vertices] for edge in me.edges]
|
||||
new_edges = [bm.edges.new((new_verts[i[0]], new_verts[i[1]])) for i in index]
|
||||
for face in me.polygons:
|
||||
verts = [new_verts[i] for i in face.vertices]
|
||||
bm.faces.new(verts)
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()]
|
||||
|
||||
bpy.data.objects.remove(bpy.data.objects[path_obj.name], do_unlink=True)
|
||||
bpy.data.objects.remove(bpy.data.objects[profile_obj.name], do_unlink=True)
|
||||
try:
|
||||
bpy.data.curves.remove(profile_obj.data, do_unlink=True)
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
bpy.data.curves.remove(path_obj.data, do_unlink=True)
|
||||
except:
|
||||
pass
|
||||
|
||||
data = {}
|
||||
data["verts"] = [tuple(v.co) for v in bm.verts]
|
||||
data["edges"] = [(edge.verts[0].index, edge.verts[1].index) for edge in bm.edges]
|
||||
data["tris"] = tris
|
||||
|
||||
bm.free()
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_product_preview_data(context, relating_type):
|
||||
model_props = context.scene.BIMModelProperties
|
||||
if relating_type.is_a("IfcDoorType"):
|
||||
rl = float(model_props.rl1)
|
||||
elif relating_type.is_a("IfcWindowType"):
|
||||
rl = float(model_props.rl2)
|
||||
else:
|
||||
rl = 0
|
||||
snap_prop = context.scene.BIMPolylineProperties.snap_mouse_point[0]
|
||||
default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z
|
||||
mouse_point = Vector((snap_prop.x, snap_prop.y, default_container_elevation))
|
||||
snap_obj = bpy.data.objects.get(snap_prop.snap_object)
|
||||
snap_element = tool.Ifc.get_entity(snap_obj)
|
||||
rot_mat = Quaternion()
|
||||
if snap_element and snap_element.is_a("IfcWall"):
|
||||
rot_mat = snap_obj.matrix_world.to_quaternion()
|
||||
|
||||
obj_type = tool.Ifc.get_object(relating_type)
|
||||
if obj_type.data:
|
||||
data = ItemDecorator.get_obj_data(obj_type)
|
||||
data["verts"] = [tuple(obj_type.matrix_world.inverted() @ Vector(v)) for v in data["verts"]]
|
||||
data["verts"] = [tuple(rot_mat @ (Vector((v[0], v[1], (v[2] + rl)))) + mouse_point) for v in data["verts"]]
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class PolylineOperator:
|
||||
# TODO Fill doc strings
|
||||
""" """
|
||||
@@ -387,6 +813,34 @@ class PolylineOperator:
|
||||
tool.Polyline.remove_last_polyline_point()
|
||||
tool.Blender.update_viewport()
|
||||
|
||||
def get_product_preview_data(self, context: bpy.types.Context, relating_type: ifcopenshell.entity_isntance):
|
||||
if tool.Model.get_usage_type(relating_type) == "PROFILE" and relating_type.is_a() not in {"IfcColumnType"}:
|
||||
data = get_horizontal_profile_preview_data(context, relating_type)
|
||||
elif tool.Model.get_usage_type(relating_type) == "PROFILE" and relating_type.is_a() in {"IfcColumnType"}:
|
||||
data = get_vertical_profile_preview_data(context, relating_type)
|
||||
elif tool.Model.get_usage_type(relating_type) == "LAYER2":
|
||||
data = get_wall_preview_data(context, relating_type)
|
||||
else:
|
||||
data = get_product_preview_data(context, relating_type)
|
||||
|
||||
# Update properties so it can be used by the decorator
|
||||
if not data:
|
||||
return
|
||||
props = context.scene.BIMProductPreviewProperties
|
||||
props.verts.clear()
|
||||
props.edges.clear()
|
||||
props.tris.clear()
|
||||
|
||||
for vert in data["verts"]:
|
||||
v = props.verts.add()
|
||||
v.value_3d = vert
|
||||
for edge in data["edges"]:
|
||||
e = props.edges.add()
|
||||
e.value_2d = edge
|
||||
for tri in data["tris"]:
|
||||
t = props.tris.add()
|
||||
t.value_3d = tri
|
||||
|
||||
def modal(self, context: bpy.types.Context, event: bpy.types.Event) -> Union[set[str], None]:
|
||||
PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0])
|
||||
tool.Blender.update_viewport()
|
||||
|
||||
@@ -191,6 +191,8 @@ class AddOccurrence(bpy.types.Operator, PolylineOperator):
|
||||
if event.value == "RELEASE" and event.type == "LEFTMOUSE":
|
||||
self.create_occurrence(context, event)
|
||||
|
||||
self.get_product_preview_data(context, self.relating_type)
|
||||
|
||||
cancel = self.handle_cancelation(context, event)
|
||||
if cancel is not None:
|
||||
ProductDecorator.uninstall()
|
||||
@@ -417,7 +419,7 @@ class AddConstrTypeInstance(bpy.types.Operator, tool.Ifc.Operator):
|
||||
):
|
||||
if instance_class in ["IfcWindow", "IfcDoor"]:
|
||||
# TODO For now we are hardcoding windows and doors as a prototype
|
||||
bpy.ops.bim.add_filled_opening(voided_obj=building_obj.name, filling_obj=obj.name)
|
||||
tool.Model.add_filled_opening(building_obj, obj)
|
||||
else:
|
||||
if self.container_obj:
|
||||
if props.rl_mode == "BOTTOM":
|
||||
@@ -489,7 +491,6 @@ class SetActiveType(bpy.types.Operator, tool.Ifc.Operator):
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMModelProperties
|
||||
props.relating_type_id = str(self.relating_type)
|
||||
context.window.screen = context.window.screen # Closes the type manager popup
|
||||
|
||||
|
||||
class AlignProduct(bpy.types.Operator):
|
||||
@@ -564,10 +565,11 @@ class LoadTypeThumbnails(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return
|
||||
|
||||
props = bpy.context.scene.BIMModelProperties
|
||||
processing = set()
|
||||
# Only process at most one paginated class at a time.
|
||||
# Large projects have hundreds of types which can lead to unnecessary lag.
|
||||
queue = sorted(tool.Ifc.get().by_type(self.ifc_class), key=lambda e: e.Name or "Unnamed")
|
||||
if not AuthoringData.is_loaded:
|
||||
AuthoringData.load()
|
||||
queue = AuthoringData.data["type_elements_filtered"]
|
||||
if self.limit:
|
||||
queue = queue[self.offset : self.offset + self.limit]
|
||||
else:
|
||||
@@ -576,6 +578,12 @@ class LoadTypeThumbnails(bpy.types.Operator, tool.Ifc.Operator):
|
||||
offset = 0
|
||||
queue = queue[offset : offset + 9]
|
||||
|
||||
# The active type may be in another page than the active one :
|
||||
if relating_type_id_current := AuthoringData.data["relating_type_id_current"]:
|
||||
active_element = tool.Ifc.get_entity_by_id(int(relating_type_id_current))
|
||||
if active_element and active_element not in queue:
|
||||
queue.append(active_element)
|
||||
|
||||
while queue:
|
||||
# if bpy.app.is_job_running("RENDER_PREVIEW") does not seem to reflect asset preview generation
|
||||
element = queue.pop()
|
||||
|
||||
@@ -32,11 +32,13 @@ import bonsai.core.type
|
||||
import bonsai.core.geometry
|
||||
import bonsai.core.material
|
||||
import bonsai.core.root
|
||||
from math import pi, degrees, inf
|
||||
from math import pi, degrees, inf, atan2
|
||||
from mathutils import Vector, Matrix, Quaternion
|
||||
from bonsai.bim.module.geometry.helper import Helper
|
||||
from bonsai.bim.module.model.wall import DumbWallRecalculator
|
||||
from bonsai.bim.module.model.decorator import ProfileDecorator
|
||||
from bonsai.bim.module.model.decorator import ProfileDecorator, PolylineDecorator, ProductDecorator
|
||||
from bonsai.bim.module.model.polyline import PolylineOperator
|
||||
from typing import Union, Any, Optional
|
||||
|
||||
|
||||
class DumbProfileGenerator:
|
||||
@@ -44,7 +46,7 @@ class DumbProfileGenerator:
|
||||
self.relating_type = relating_type
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
|
||||
def generate(self):
|
||||
def generate(self, insertion_type="CURSOR"):
|
||||
self.file = tool.Ifc.get()
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
material = ifcopenshell.util.element.get_material(self.relating_type)
|
||||
@@ -67,7 +69,28 @@ class DumbProfileGenerator:
|
||||
self.rotation = 0
|
||||
self.location = Vector((0, 0, 0))
|
||||
self.cardinal_point = int(bpy.context.scene.BIMModelProperties.cardinal_point)
|
||||
return self.derive_from_cursor()
|
||||
if insertion_type == "POLYLINE":
|
||||
return self.derive_from_polyline()
|
||||
elif insertion_type == "CURSOR":
|
||||
return self.derive_from_cursor()
|
||||
|
||||
def derive_from_polyline(self) -> tuple[list[Union[dict[str, Any], None]], bool]:
|
||||
polyline_data = bpy.context.scene.BIMPolylineProperties.insertion_polyline
|
||||
polyline_points = polyline_data[0].polyline_points if polyline_data else []
|
||||
is_polyline_closed = False
|
||||
if len(polyline_points) > 3:
|
||||
first_vec = Vector((polyline_points[0].x, polyline_points[0].y, polyline_points[0].z))
|
||||
last_vec = Vector((polyline_points[-1].x, polyline_points[-1].y, polyline_points[-1].z))
|
||||
if first_vec == last_vec:
|
||||
is_polyline_closed = True
|
||||
|
||||
profiles = []
|
||||
for i in range(len(polyline_points) - 1):
|
||||
vec1 = Vector((polyline_points[i].x, polyline_points[i].y, polyline_points[i].z))
|
||||
vec2 = Vector((polyline_points[i + 1].x, polyline_points[i + 1].y, polyline_points[i + 1].z))
|
||||
coords = (vec1, vec2)
|
||||
profiles.append(self.create_profile_from_2_points(coords))
|
||||
return profiles, is_polyline_closed
|
||||
|
||||
def derive_from_cursor(self):
|
||||
self.location = bpy.context.scene.cursor.location
|
||||
@@ -82,10 +105,13 @@ class DumbProfileGenerator:
|
||||
obj = bpy.data.objects.new(tool.Model.generate_occurrence_name(self.relating_type, ifc_class), mesh)
|
||||
|
||||
matrix_world = Matrix()
|
||||
if self.relating_type.is_a() in ["IfcBeamType", "IfcMemberType"] or self.relating_type.is_a(
|
||||
if self.relating_type.is_a() in ["IfcBeamType", "IfcCoveringType", "IfcMemberType"] or self.relating_type.is_a(
|
||||
"IfcFlowSegmentType"
|
||||
):
|
||||
matrix_world = Matrix.Rotation(pi / 2, 4, "Z") @ Matrix.Rotation(pi / 2, 4, "X") @ matrix_world
|
||||
|
||||
matrix_world = Matrix.Rotation(self.rotation, 4, "Z") @ matrix_world
|
||||
|
||||
matrix_world.translation = self.location
|
||||
if self.container_obj:
|
||||
matrix_world.translation.z = self.container_obj.location.z
|
||||
@@ -146,6 +172,25 @@ class DumbProfileGenerator:
|
||||
|
||||
return obj
|
||||
|
||||
def create_profile_from_2_points(self, coords, should_round=False) -> Union[dict[str, Any], None]:
|
||||
direction = coords[1] - coords[0]
|
||||
length = direction.length
|
||||
if round(length, 4) < 0.1:
|
||||
return
|
||||
data = {"coords": coords}
|
||||
|
||||
self.depth = length
|
||||
self.rotation = atan2(direction[1], direction[0])
|
||||
if should_round:
|
||||
# Round to nearest 50mm (yes, metric for now)
|
||||
self.length = 0.05 * round(length / 0.05)
|
||||
# Round to nearest 5 degrees
|
||||
nearest_degree = (math.pi / 180) * 5
|
||||
self.rotation = nearest_degree * round(self.rotation / nearest_degree)
|
||||
self.location = coords[0]
|
||||
data["obj"] = self.create_profile()
|
||||
return data
|
||||
|
||||
|
||||
class DumbProfileRegenerator:
|
||||
def regenerate_from_profile_def(self, profile):
|
||||
@@ -272,7 +317,7 @@ class DumbProfileJoiner:
|
||||
self.axis_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Axis", "GRAPH_VIEW")
|
||||
self.body_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
|
||||
|
||||
def unjoin(self, profile1):
|
||||
def unjoin(self, profile1: bpy.types.Object) -> None:
|
||||
element1 = tool.Ifc.get_entity(profile1)
|
||||
if not element1:
|
||||
return
|
||||
@@ -285,7 +330,7 @@ class DumbProfileJoiner:
|
||||
body = copy.deepcopy(axis1)
|
||||
self.recreate_profile(element1, profile1, axis, body)
|
||||
|
||||
def join_E(self, profile1, target, connection=None):
|
||||
def join_E(self, profile1: bpy.types.Object, target: Vector, connection: Optional[str] = None) -> None:
|
||||
"""`connection` = `ATEND` / `ATSTART` to explicitly define the reference point for the join.
|
||||
|
||||
For example if profile 1m long and `target` is at (0, 0, 0.1) and `connection` = `None`
|
||||
@@ -309,7 +354,7 @@ class DumbProfileJoiner:
|
||||
body[1 if connection == "ATEND" else 0] = intersect
|
||||
self.recreate_profile(element1, profile1, axis, body)
|
||||
|
||||
def set_depth(self, profile1, si_length):
|
||||
def set_depth(self, profile1: bpy.types.Object, si_length: float) -> None:
|
||||
element1 = tool.Ifc.get_entity(profile1)
|
||||
if not element1:
|
||||
return
|
||||
@@ -324,7 +369,7 @@ class DumbProfileJoiner:
|
||||
body[1] = end
|
||||
self.recreate_profile(element1, profile1, axis, body)
|
||||
|
||||
def join_T(self, profile1, profile2):
|
||||
def join_T(self, profile1: bpy.types.Object, profile2: bpy.types.Object) -> None:
|
||||
element1 = tool.Ifc.get_entity(profile1)
|
||||
element2 = tool.Ifc.get_entity(profile2)
|
||||
axis1 = self.get_profile_axis(profile1)
|
||||
@@ -348,7 +393,7 @@ class DumbProfileJoiner:
|
||||
|
||||
self.recreate_profile(element1, profile1, axis1, axis1)
|
||||
|
||||
def join_V(self, profile1, profile2):
|
||||
def join_V(self, profile1: bpy.types.Object, profile2: bpy.types.Object) -> None:
|
||||
element1 = tool.Ifc.get_entity(profile1)
|
||||
element2 = tool.Ifc.get_entity(profile2)
|
||||
axis1 = self.get_profile_axis(profile1)
|
||||
@@ -374,7 +419,7 @@ class DumbProfileJoiner:
|
||||
self.recreate_profile(element1, profile1, axis1, axis1)
|
||||
self.recreate_profile(element2, profile2, axis2, axis2)
|
||||
|
||||
def join_L(self, profile1, profile2):
|
||||
def join_L(self, profile1: bpy.types.Object, profile2: bpy.types.Object) -> None:
|
||||
element1 = tool.Ifc.get_entity(profile1)
|
||||
element2 = tool.Ifc.get_entity(profile2)
|
||||
axis1 = self.get_profile_axis(profile1)
|
||||
@@ -400,7 +445,7 @@ class DumbProfileJoiner:
|
||||
self.recreate_profile(element1, profile1, axis1, axis1)
|
||||
self.recreate_profile(element2, profile2, axis2, axis2)
|
||||
|
||||
def recreate_profile(self, element, obj, axis=None, body=None):
|
||||
def recreate_profile(self, element: ifcopenshell.entity_instance, obj: bpy.types.Object, axis=None, body=None):
|
||||
if axis is None or body is None:
|
||||
axis = body = self.get_profile_axis(obj)
|
||||
self.axis = copy.deepcopy(axis)
|
||||
@@ -791,10 +836,10 @@ class DumbProfileJoiner:
|
||||
z_axis = obj.matrix_world.to_quaternion() @ Vector((-1, 0, 0))
|
||||
return self.create_matrix(p, x_axis, y_axis, z_axis)
|
||||
|
||||
def create_matrix(self, p, x, y, z):
|
||||
def create_matrix(self, p: Vector, x: Vector, y: Vector, z: Vector) -> Matrix:
|
||||
return Matrix([x, y, z, p]).to_4x4().transposed()
|
||||
|
||||
def get_profile_axis(self, obj):
|
||||
def get_profile_axis(self, obj: bpy.types.Object) -> list[Vector]:
|
||||
z_values = [v[2] for v in obj.bound_box]
|
||||
return [
|
||||
(obj.matrix_world @ Vector((0.0, 0.0, min(z_values)))),
|
||||
@@ -1064,3 +1109,114 @@ class EditExtrusionAxis(bpy.types.Operator, tool.Ifc.Operator):
|
||||
joiner = DumbProfileJoiner()
|
||||
joiner.set_depth(obj, depth)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DrawPolylineProfile(bpy.types.Operator, PolylineOperator):
|
||||
bl_idname = "bim.draw_polyline_profile"
|
||||
bl_label = "Draw Polyline Profile"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.space_data.type == "VIEW_3D"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.relating_type = None
|
||||
props = bpy.context.scene.BIMModelProperties
|
||||
relating_type_id = props.relating_type_id
|
||||
if relating_type_id:
|
||||
self.relating_type = tool.Ifc.get().by_id(int(relating_type_id))
|
||||
|
||||
def create_profiles_from_polyline(self, context: bpy.types.Context) -> Union[set[str], None]:
|
||||
if not self.relating_type:
|
||||
return {"FINISHED"}
|
||||
|
||||
model_props = context.scene.BIMModelProperties
|
||||
direction_sense = model_props.direction_sense
|
||||
offset = model_props.offset
|
||||
|
||||
profiles, is_polyline_closed = DumbProfileGenerator(self.relating_type).generate("POLYLINE")
|
||||
if profiles:
|
||||
if is_polyline_closed:
|
||||
for profile1, profile2 in zip(profiles, profiles[1:] + [profiles[0]]):
|
||||
DumbProfileJoiner().join_V(profile2["obj"], profile1["obj"])
|
||||
else:
|
||||
for profile1, profile2 in zip(profiles[:-1], profiles[1:]):
|
||||
DumbProfileJoiner().join_V(profile2["obj"], profile1["obj"])
|
||||
|
||||
def modal(self, context, event):
|
||||
if not self.relating_type:
|
||||
self.report({"WARNING"}, "You need to select a profile type.")
|
||||
PolylineDecorator.uninstall()
|
||||
tool.Blender.update_viewport()
|
||||
return {"FINISHED"}
|
||||
|
||||
PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0])
|
||||
tool.Blender.update_viewport()
|
||||
|
||||
self.handle_lock_axis(context, event) # Must come before "PASS_TRHOUGH"
|
||||
|
||||
if event.type in {"MIDDLEMOUSE", "WHEELUPMOUSE", "WHEELDOWNMOUSE"}:
|
||||
self.handle_mouse_move(context, event)
|
||||
return {"PASS_THROUGH"}
|
||||
|
||||
# Wall axis settings
|
||||
if event.value == "RELEASE" and event.type == "F":
|
||||
direction_sense = context.scene.BIMModelProperties.direction_sense
|
||||
context.scene.BIMModelProperties.direction_sense = (
|
||||
"NEGATIVE" if direction_sense == "POSITIVE" else "POSITIVE"
|
||||
)
|
||||
|
||||
if event.value == "RELEASE" and event.type == "O":
|
||||
offset_type = context.scene.BIMModelProperties.offset_type
|
||||
items = ["EXTERIOR", "CENTER", "INTERIOR"]
|
||||
index = items.index(offset_type)
|
||||
size = len(items)
|
||||
context.scene.BIMModelProperties.offset_type = items[((index + 1) % size)]
|
||||
|
||||
props = bpy.context.scene.BIMModelProperties
|
||||
wall_config = f"""Direction: {props.direction_sense}
|
||||
Offset Type: {props.offset_type}
|
||||
Offset Value: {props.offset}
|
||||
"""
|
||||
self.handle_instructions(context, wall_config)
|
||||
|
||||
self.handle_mouse_move(context, event, should_round=True)
|
||||
|
||||
self.choose_axis(event)
|
||||
|
||||
self.handle_snap_selection(context, event)
|
||||
|
||||
if (
|
||||
not self.tool_state.is_input_on
|
||||
and event.value == "RELEASE"
|
||||
and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}
|
||||
):
|
||||
self.create_profiles_from_polyline(context)
|
||||
context.workspace.status_text_set(text=None)
|
||||
ProductDecorator.uninstall()
|
||||
PolylineDecorator.uninstall()
|
||||
tool.Polyline.clear_polyline()
|
||||
tool.Blender.update_viewport()
|
||||
return {"FINISHED"}
|
||||
|
||||
self.handle_keyboard_input(context, event)
|
||||
|
||||
self.handle_inserting_polyline(context, event)
|
||||
|
||||
self.get_product_preview_data(context, self.relating_type)
|
||||
|
||||
cancel = self.handle_cancelation(context, event)
|
||||
if cancel is not None:
|
||||
ProductDecorator.uninstall()
|
||||
return cancel
|
||||
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
super().invoke(context, event)
|
||||
ProductDecorator.install(context)
|
||||
self.tool_state.use_default_container = True
|
||||
self.tool_state.plane_method = "XY"
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
@@ -25,6 +25,7 @@ from bonsai.bim.prop import ObjProperty
|
||||
from bonsai.bim.module.model.data import AuthoringData
|
||||
from bpy.types import PropertyGroup, NodeTree
|
||||
from math import pi, radians
|
||||
from bonsai.bim.module.model.decorator import WallAxisDecorator, SlabDirectionDecorator
|
||||
|
||||
|
||||
def get_ifc_class(self, context):
|
||||
@@ -42,6 +43,9 @@ def get_boundary_class(self, context):
|
||||
def get_relating_type_id(self, context):
|
||||
if not AuthoringData.is_loaded:
|
||||
AuthoringData.load()
|
||||
else:
|
||||
AuthoringData.data["type_elements"] = AuthoringData.type_elements()
|
||||
AuthoringData.data["relating_type_id"] = AuthoringData.relating_type_id()
|
||||
return AuthoringData.data["relating_type_id"]
|
||||
|
||||
|
||||
@@ -64,10 +68,14 @@ def update_relating_type_id(self, context):
|
||||
AuthoringData.data["relating_type_name"] = AuthoringData.relating_type_name()
|
||||
AuthoringData.data["type_thumbnail"] = AuthoringData.type_thumbnail()
|
||||
AuthoringData.data["predefined_type"] = AuthoringData.predefined_type()
|
||||
self.type_page = [e[0] for e in AuthoringData.data["relating_type_id"]].index(self.relating_type_id) // 9 + 1
|
||||
|
||||
|
||||
def update_type_page(self, context):
|
||||
AuthoringData.data["paginated_relating_types"] = AuthoringData.paginated_relating_types()
|
||||
bpy.ops.bim.load_type_thumbnails(ifc_class=self.ifc_class, offset=9 * (self.type_page - 1), limit=9)
|
||||
self["type_page"] = min(self["type_page"], AuthoringData.data["total_pages"])
|
||||
self["type_page"] = max(self["type_page"], 1)
|
||||
|
||||
|
||||
def update_relating_array_from_object(self, context):
|
||||
@@ -82,11 +90,46 @@ def is_object_array_applicable(self, obj):
|
||||
return ifcopenshell.util.element.get_pset(element, "BBIM_Array")
|
||||
|
||||
|
||||
def update_wall_axis_decorator(self, context):
|
||||
if self.show_wall_axis:
|
||||
WallAxisDecorator.install(bpy.context)
|
||||
else:
|
||||
WallAxisDecorator.uninstall()
|
||||
|
||||
|
||||
def update_slab_direction_decorator(self, context):
|
||||
if self.show_slab_direction:
|
||||
SlabDirectionDecorator.install(bpy.context)
|
||||
else:
|
||||
SlabDirectionDecorator.uninstall()
|
||||
|
||||
|
||||
def update_search_name(self, context):
|
||||
AuthoringData.load()
|
||||
# Total number of pages may decrease when using the search bar :
|
||||
if self.type_page > AuthoringData.data["total_pages"]:
|
||||
self.type_page = max(1, AuthoringData.data["total_pages"])
|
||||
bpy.ops.bim.load_type_thumbnails(ifc_class=self.ifc_class)
|
||||
|
||||
|
||||
def update_x_angle(self, context):
|
||||
angle_deg = math.degrees(self.x_angle)
|
||||
if tool.Cad.is_x(angle_deg, -90, 0.5) or tool.Cad.is_x(angle_deg, 90, 0.5):
|
||||
self.x_angle = 0
|
||||
|
||||
|
||||
class BIMModelProperties(PropertyGroup):
|
||||
ifc_class: bpy.props.EnumProperty(items=get_ifc_class, name="Construction Class", update=update_ifc_class)
|
||||
relating_type_id: bpy.props.EnumProperty(
|
||||
items=get_relating_type_id, name="Relating Type", update=update_relating_type_id
|
||||
)
|
||||
search_name: bpy.props.StringProperty(
|
||||
name="Search Name",
|
||||
default="",
|
||||
description="Use this property to filter the list of available types",
|
||||
update=update_search_name,
|
||||
options={"SKIP_SAVE", "TEXTEDIT_UPDATE"},
|
||||
)
|
||||
menu_relating_type_id: bpy.props.IntProperty()
|
||||
icon_id: bpy.props.IntProperty()
|
||||
updating: bpy.props.BoolProperty(default=False)
|
||||
@@ -146,8 +189,10 @@ class BIMModelProperties(PropertyGroup):
|
||||
rl2: bpy.props.FloatProperty(name="RL", default=1, subtype="DISTANCE", description="Z offset for windows")
|
||||
# Used for plan calculation points such as in room generation
|
||||
rl3: bpy.props.FloatProperty(name="RL", default=1, subtype="DISTANCE", description="Z offset for space calculation")
|
||||
x_angle: bpy.props.FloatProperty(name="X Angle", default=0, subtype="ANGLE", min=-pi / 180 * 89, max=pi / 180 * 89)
|
||||
type_page: bpy.props.IntProperty(name="Type Page", default=1, update=update_type_page)
|
||||
type_page: bpy.props.IntProperty(name="Type Page", default=1, min=1, update=update_type_page)
|
||||
x_angle: bpy.props.FloatProperty(
|
||||
name="X Angle", default=0, subtype="ANGLE", min=math.radians(-180), max=math.radians(180), update=update_x_angle
|
||||
)
|
||||
type_name: bpy.props.StringProperty(name="Name", default="TYPEX")
|
||||
boundary_class: bpy.props.EnumProperty(items=get_boundary_class, name="Boundary Class")
|
||||
direction_sense: bpy.props.EnumProperty(
|
||||
@@ -162,6 +207,16 @@ class BIMModelProperties(PropertyGroup):
|
||||
description="It's a convention that affects the offset to reference line",
|
||||
)
|
||||
offset: bpy.props.FloatProperty(name="Offset", default=0.0, description="Material usage offset from reference line")
|
||||
show_wall_axis: bpy.props.BoolProperty(
|
||||
name="Show Wall Axis",
|
||||
default=False,
|
||||
update=update_wall_axis_decorator,
|
||||
)
|
||||
show_slab_direction: bpy.props.BoolProperty(
|
||||
name="Show Slab Direction",
|
||||
default=False,
|
||||
update=update_slab_direction_decorator,
|
||||
)
|
||||
|
||||
|
||||
class BIMArrayProperties(PropertyGroup):
|
||||
@@ -195,6 +250,24 @@ class BIMArrayProperties(PropertyGroup):
|
||||
)
|
||||
|
||||
|
||||
def update_total_length_target(self, context):
|
||||
self["tread_run"] = self.total_length_target / (self.number_of_treads + 1)
|
||||
|
||||
|
||||
def update_tread_run(self, context):
|
||||
if self.total_length_lock:
|
||||
self["number_of_treads"] = int((self.total_length_target / self.tread_run) - 1)
|
||||
else:
|
||||
self["total_length_target"] = (self.number_of_treads + 1) * self.tread_run
|
||||
|
||||
|
||||
def update_number_of_treads(self, context):
|
||||
if self.total_length_lock:
|
||||
self["tread_run"] = self.total_length_target / (self.number_of_treads + 1)
|
||||
else:
|
||||
self["total_length_target"] = (self.number_of_treads + 1) * self.tread_run
|
||||
|
||||
|
||||
class BIMStairProperties(PropertyGroup):
|
||||
def validate_nosing_value(self, context):
|
||||
if self.stair_type != "WOOD/STEEL" and self.nosing_length < 0:
|
||||
@@ -210,9 +283,26 @@ class BIMStairProperties(PropertyGroup):
|
||||
is_editing: bpy.props.BoolProperty(default=False)
|
||||
width: bpy.props.FloatProperty(name="Width", default=1.2, soft_min=0.01, subtype="DISTANCE")
|
||||
height: bpy.props.FloatProperty(name="Height", default=1.0, soft_min=0.01, subtype="DISTANCE")
|
||||
number_of_treads: bpy.props.IntProperty(name="Number of Treads", default=6, soft_min=1)
|
||||
number_of_treads: bpy.props.IntProperty(
|
||||
name="Number of Treads", default=6, soft_min=1, update=update_number_of_treads
|
||||
)
|
||||
total_length_target: bpy.props.FloatProperty(
|
||||
name="Total Length Target",
|
||||
default=3.0,
|
||||
soft_min=0.01,
|
||||
subtype="DISTANCE",
|
||||
update=update_total_length_target,
|
||||
description="Total Length Target, might not be exactly respected depending on the parameters",
|
||||
)
|
||||
total_length_lock: bpy.props.BoolProperty(
|
||||
default=False,
|
||||
name="Lock Total Length",
|
||||
description="Lock Total Length when changing number of treads or tread run",
|
||||
)
|
||||
tread_depth: bpy.props.FloatProperty(name="Tread Depth", default=0.25, soft_min=0.01, subtype="DISTANCE")
|
||||
tread_run: bpy.props.FloatProperty(name="Tread Run", default=0.3, soft_min=0.01, subtype="DISTANCE")
|
||||
tread_run: bpy.props.FloatProperty(
|
||||
name="Tread Run", default=0.3, soft_min=0.01, subtype="DISTANCE", update=update_tread_run
|
||||
)
|
||||
base_slab_depth: bpy.props.FloatProperty(name="Base Slab Depth", default=0.25, soft_min=0, subtype="DISTANCE")
|
||||
top_slab_depth: bpy.props.FloatProperty(name="Top Slab Depth", default=0.25, soft_min=0, subtype="DISTANCE")
|
||||
has_top_nib: bpy.props.BoolProperty(name="Has Top Nib", default=True)
|
||||
@@ -739,7 +829,9 @@ class BIMRoofProperties(PropertyGroup):
|
||||
soft_max=to_percentage(radians(60.0)),
|
||||
)
|
||||
roof_thickness: bpy.props.FloatProperty(name="Roof Thickness", default=0.1, subtype="DISTANCE")
|
||||
rafter_edge_angle: bpy.props.FloatProperty(name="Rafter Edge Angle", min=0, max=pi, default=pi / 2, subtype="ANGLE")
|
||||
rafter_edge_angle: bpy.props.FloatProperty(
|
||||
name="Rafter Edge Angle", min=0, max=pi / 2, default=pi / 2, subtype="ANGLE"
|
||||
)
|
||||
|
||||
def get_general_kwargs(self, generation_method=None, convert_to_project_units=False):
|
||||
if generation_method is None:
|
||||
@@ -795,3 +887,14 @@ class BIMPolylineProperties(PropertyGroup):
|
||||
snap_mouse_ref: bpy.props.CollectionProperty(type=SnapMousePoint)
|
||||
insertion_polyline: bpy.props.CollectionProperty(type=Polyline)
|
||||
measurement_polyline: bpy.props.CollectionProperty(type=Polyline)
|
||||
|
||||
|
||||
class ProductPreviewItem(PropertyGroup):
|
||||
value_3d: bpy.props.FloatVectorProperty()
|
||||
value_2d: bpy.props.FloatVectorProperty(size=2)
|
||||
|
||||
|
||||
class BIMProductPreviewProperties(PropertyGroup):
|
||||
verts: bpy.props.CollectionProperty(type=ProductPreviewItem)
|
||||
edges: bpy.props.CollectionProperty(type=ProductPreviewItem)
|
||||
tris: bpy.props.CollectionProperty(type=ProductPreviewItem)
|
||||
|
||||
@@ -26,7 +26,6 @@ import ifcopenshell.util.unit
|
||||
import bonsai.core.root
|
||||
import bonsai.core.geometry
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.module.model.door import bm_sort_out_geom
|
||||
from bonsai.bim.module.model.data import RailingData, refresh
|
||||
from bonsai.bim.module.model.decorator import ProfileDecorator
|
||||
|
||||
@@ -47,7 +46,7 @@ def bm_split_edge_at_offset(edge: bmesh.types.BMEdge, offset: float) -> dict[str
|
||||
|
||||
split_output_0 = bmesh.utils.edge_split(edge, v0, offset / edge_len)
|
||||
split_output_1 = bmesh.utils.edge_split(edge, v1, offset / (edge_len - offset))
|
||||
new_geometry = bm_sort_out_geom(split_output_0 + split_output_1)
|
||||
new_geometry = tool.Model.bm_sort_out_geom(split_output_0 + split_output_1)
|
||||
return new_geometry
|
||||
|
||||
|
||||
@@ -175,11 +174,11 @@ def update_railing_modifier_bmesh(context: bpy.types.Context) -> None:
|
||||
ortho_vector = edge_dir.cross(V_(0, 0, 1))
|
||||
|
||||
extruded_geom = bmesh.ops.extrude_edge_only(bm, edges=[main_edge])["geom"]
|
||||
extruded_verts = bm_sort_out_geom(extruded_geom)["verts"]
|
||||
extruded_verts = tool.Model.bm_sort_out_geom(extruded_geom)["verts"]
|
||||
bmesh.ops.translate(bm, vec=ortho_vector * (-thickness / 2), verts=extruded_verts)
|
||||
|
||||
extruded_geom = bmesh.ops.extrude_edge_only(bm, edges=[main_edge])["geom"]
|
||||
extruded_verts = bm_sort_out_geom(extruded_geom)["verts"]
|
||||
extruded_verts = tool.Model.bm_sort_out_geom(extruded_geom)["verts"]
|
||||
bmesh.ops.translate(bm, vec=ortho_vector * (thickness / 2), verts=extruded_verts)
|
||||
|
||||
# dissolve middle edge
|
||||
@@ -187,7 +186,7 @@ def update_railing_modifier_bmesh(context: bpy.types.Context) -> None:
|
||||
|
||||
# height
|
||||
extruded_geom = bmesh.ops.extrude_face_region(bm, geom=bm.faces)["geom"]
|
||||
extruded_verts = bm_sort_out_geom(extruded_geom)["verts"]
|
||||
extruded_verts = tool.Model.bm_sort_out_geom(extruded_geom)["verts"]
|
||||
extrusion_vector = Vector((0, 0, 1)) * height
|
||||
bmesh.ops.translate(bm, vec=extrusion_vector, verts=extruded_verts)
|
||||
|
||||
@@ -354,6 +353,44 @@ class AddRailing(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tool.Model.add_body_representation(obj)
|
||||
|
||||
|
||||
class CopyRailingParameters(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.copy_railing_parameters"
|
||||
bl_label = "Copy Railing Parameters from Active to Selected"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not context.active_object or len(context.selected_objects) < 1:
|
||||
cls.poll_message_set("At least 2 objects must be selected.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
source_obj = context.active_object
|
||||
source_props = source_obj.BIMRailingProperties
|
||||
railing_data = source_props.get_general_kwargs(convert_to_project_units=True)
|
||||
|
||||
for target_obj in context.selected_objects:
|
||||
if target_obj == source_obj:
|
||||
continue
|
||||
context.view_layer.objects.active = target_obj
|
||||
RailingData.load()
|
||||
if not "path_data" in RailingData.data:
|
||||
continue
|
||||
railing_data["path_data"] = RailingData.data["path_data"]
|
||||
target_element = tool.Ifc.get_entity(target_obj)
|
||||
target_props = target_obj.BIMRailingProperties
|
||||
|
||||
target_props.set_props_kwargs_from_ifc_data(railing_data)
|
||||
update_bbim_railing_pset(target_element, railing_data)
|
||||
refresh()
|
||||
update_railing_modifier_bmesh(context)
|
||||
update_railing_modifier_ifc_data(context)
|
||||
|
||||
context.view_layer.objects.active = source_obj
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class EnableEditingRailing(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.enable_editing_railing"
|
||||
bl_label = "Enable Editing Railing"
|
||||
|
||||
@@ -25,18 +25,15 @@ import ifcopenshell.util.representation
|
||||
import ifcopenshell.util.unit
|
||||
import bonsai.core.root
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.helper import convert_property_group_from_si
|
||||
from bonsai.bim.module.model.door import bm_sort_out_geom
|
||||
from bonsai.bim.module.model.data import RoofData, refresh
|
||||
from bonsai.bim.module.model.decorator import ProfileDecorator
|
||||
|
||||
import json
|
||||
from math import tan, pi, radians
|
||||
from mathutils import Vector, Matrix
|
||||
from math import cos, tan, pi, radians
|
||||
from mathutils import Vector, Matrix, Quaternion
|
||||
import mathutils.geometry
|
||||
from bpypolyskel import bpypolyskel
|
||||
import shapely
|
||||
from pprint import pprint
|
||||
from typing import Literal, Union, Any
|
||||
|
||||
# reference:
|
||||
@@ -90,7 +87,7 @@ class GenerateHippedRoof(bpy.types.Operator, tool.Ifc.Operator):
|
||||
self.report(op_status, error_message)
|
||||
return {"CANCELLED"}
|
||||
|
||||
generate_hiped_roof_bmesh(bm, self.mode, self.height, self.angle)
|
||||
generate_hipped_roof_bmesh(bm, self.mode, self.height, self.angle)
|
||||
tool.Blender.apply_bmesh(obj.data, bm)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -108,7 +105,7 @@ def is_valid_roof_footprint(bm: bmesh.types.BMesh) -> tuple[set[str], str]:
|
||||
return ({"FINISHED"}, "")
|
||||
|
||||
|
||||
def generate_hiped_roof_bmesh(
|
||||
def generate_hipped_roof_bmesh(
|
||||
bm: bmesh.types.BMesh,
|
||||
mode: Literal["ANGLE", "HEIGHT"] = "ANGLE",
|
||||
height: float = 1.0,
|
||||
@@ -126,29 +123,23 @@ def generate_hiped_roof_bmesh(
|
||||
|
||||
If roof bmesh needed only to supply into decorator then there is no reason to mutate it.
|
||||
"""
|
||||
|
||||
if not mutate_current_bmesh:
|
||||
bm = bm.copy()
|
||||
|
||||
# CLEAN UP
|
||||
bm_mesh_clean_up(bm)
|
||||
|
||||
boundary_lines = []
|
||||
|
||||
original_geometry_data = dict()
|
||||
rafter_edge_angle = pi / 2 - rafter_edge_angle # It's easier to work with this angle.
|
||||
angled_edges = []
|
||||
angle_layer = bm.edges.layers.float.get("BBIM_gable_roof_angles")
|
||||
separate_verts_layer = bm.edges.layers.int.get("BBIM_gable_roof_separate_verts")
|
||||
if angle_layer:
|
||||
original_geometry_data["edges"] = [
|
||||
(set(bm_get_indices(e.verts)), e[angle_layer], e[separate_verts_layer]) for e in bm.edges
|
||||
angled_edges = [
|
||||
(set([v.co.copy().freeze() for v in e.verts]), e[angle_layer]) for e in bm.edges if e[angle_layer]
|
||||
]
|
||||
else:
|
||||
original_geometry_data["edges"] = [(set(bm_get_indices(e.verts)), None, None) for e in bm.edges]
|
||||
|
||||
original_geometry_data["verts"] = {v.index: v.co.copy() for v in bm.verts}
|
||||
footprint_z = bm.verts[:][0].co.z
|
||||
|
||||
def calculate_hiped_roof():
|
||||
def calculate_hipped_roof():
|
||||
boundary_lines = []
|
||||
for edge in bm.edges:
|
||||
boundary_lines.append(shapely.LineString([v.co for v in edge.verts]))
|
||||
|
||||
@@ -188,202 +179,224 @@ def generate_hiped_roof_bmesh(
|
||||
nonlocal height, angle
|
||||
if mode == "HEIGHT":
|
||||
height = height
|
||||
angle = 0.0
|
||||
tan_angle = 0.0
|
||||
else:
|
||||
angle = tan(angle)
|
||||
tan_angle = tan(angle)
|
||||
height = 0.0
|
||||
|
||||
# I think bpypolyskel upstream has a bug which can cause sloped ridges
|
||||
# where it is not necessary to have sloped ridges specifically for
|
||||
# dormers. This seems to work, but I'm not confident because apparently
|
||||
# you need a PhD to understand this. See bug #5319.
|
||||
if not hasattr(bpypolyskel._SLAV, "unpatched_handle_dormer_event"):
|
||||
|
||||
def patched_handle_dormer_event(self, event):
|
||||
result = self.unpatched_handle_dormer_event(event)
|
||||
if result[0]:
|
||||
result[0][1] = bpypolyskel.Subtree(result[0][1].source, result[0][0].height, result[0][1].sinks)
|
||||
return result
|
||||
|
||||
bpypolyskel._SLAV.unpatched_handle_dormer_event = bpypolyskel._SLAV.handle_dormer_event
|
||||
bpypolyskel._SLAV.handle_dormer_event = patched_handle_dormer_event
|
||||
|
||||
faces = bpypolyskel.polygonize(
|
||||
verts, start_exterior_index, total_exterior_verts, inner_loops, height, angle, faces, unit_vectors
|
||||
verts, start_exterior_index, total_exterior_verts, inner_loops, height, tan_angle, faces, unit_vectors
|
||||
)
|
||||
|
||||
edges = []
|
||||
return verts, edges, faces
|
||||
|
||||
verts, edges, faces = calculate_hiped_roof()
|
||||
verts, edges, faces = calculate_hipped_roof()
|
||||
bm.clear()
|
||||
|
||||
new_verts = [bm.verts.new(v) for v in verts]
|
||||
new_edges = [bm.edges.new([new_verts[vi] for vi in edge]) for edge in edges]
|
||||
new_faces = [bm.faces.new([new_verts[vi] for vi in face]) for face in faces]
|
||||
|
||||
def find_identical_new_vert(co: Vector) -> Union[bmesh.types.BMVert, None]:
|
||||
for v in bm.verts:
|
||||
if tool.Cad.is_x((co - v.co).length, 0):
|
||||
return v
|
||||
if mode == "HEIGHT": # Calculate the angle we ended up with.
|
||||
new_faces[0].normal_update()
|
||||
angle = new_faces[0].normal.angle(Vector((0, 0, 1)))
|
||||
|
||||
def find_other_polygon_verts(edge: bmesh.types.BMEdge) -> list[bmesh.types.BMVert]:
|
||||
polygon = edge.link_faces[0]
|
||||
return [v for v in polygon.verts if v not in edge.verts]
|
||||
# bpypolyskel performs a straight skeleton with equal weights. Ideally, we
|
||||
# need support for weighted straight skeletons (CGAL has this function) to
|
||||
# calculate skeletons of varying angles. Doing anything else, such as
|
||||
# creating new vertices, splitting edges, or sliding vertices is extremely
|
||||
# error prone and only works in simple scenarios.
|
||||
|
||||
def change_angle(projected_vert_co: Vector, edge_verts: list[bmesh.types.BMVert], new_angle: float) -> Vector:
|
||||
A, B = [v.co for v in edge_verts]
|
||||
P = projected_vert_co
|
||||
|
||||
AP = P - A
|
||||
AB = B - A
|
||||
AB_dir = AB.normalized()
|
||||
proj_length = AP.dot(AB_dir)
|
||||
C = A + AB_dir * proj_length
|
||||
Pp = P * Vector([1, 1, 0]) + Vector([0, 0, C.z])
|
||||
|
||||
Pp = P * Vector([1, 1, 0]) + Vector([0, 0, C.z])
|
||||
angle_tan = tan(new_angle)
|
||||
dist = (P.z - Pp.z) / angle_tan
|
||||
PPnew = C + (Pp - C).normalized() * dist
|
||||
Pnew = PPnew * Vector([1, 1, 0]) + Vector([0, 0, P.z])
|
||||
return Pnew
|
||||
|
||||
footprint_edges = []
|
||||
footprint_verts = set()
|
||||
verts_to_change = {}
|
||||
verts_to_rip = []
|
||||
bottom_chords_to_remove = []
|
||||
|
||||
def is_footprint_vert(v: bmesh.types.BMVert) -> bool:
|
||||
return tool.Cad.is_x(v.co.z - footprint_z, 0)
|
||||
# In the meantime, we can support the simplest, but luckily also most
|
||||
# common scenario of a gable roof, or hipped roof where the varied angle
|
||||
# only occurs on a triangle face.
|
||||
|
||||
def is_footprint_edge(edge: bmesh.types.BMEdge) -> bool:
|
||||
return all(is_footprint_vert(v) for v in edge.verts)
|
||||
return all(tool.Cad.is_x(v.co.z - footprint_z, 0) for v in edge.verts)
|
||||
|
||||
# find footprint edges
|
||||
footprint_edges = set()
|
||||
gable_edges = set()
|
||||
edges_to_delete = set()
|
||||
face_angles = {}
|
||||
|
||||
# Post process skeleton to handle gables and custom angles.
|
||||
for edge in bm.edges:
|
||||
if is_footprint_edge(edge):
|
||||
footprint_edges.append(edge)
|
||||
footprint_verts.update(edge.verts)
|
||||
|
||||
old_verts_remap = {}
|
||||
for old_vert in original_geometry_data["verts"]:
|
||||
old_vert_co = original_geometry_data["verts"][old_vert]
|
||||
old_verts_remap[old_vert] = find_identical_new_vert(old_vert_co)
|
||||
|
||||
# iterate over edges from original geometry
|
||||
# if their angle was redefined by user - apply the changes to the related vertices
|
||||
# to match the requested angle
|
||||
|
||||
process_later = []
|
||||
for old_edge_verts, defined_angle, separate_verts in original_geometry_data["edges"]:
|
||||
if not defined_angle:
|
||||
if not is_footprint_edge(edge):
|
||||
continue
|
||||
|
||||
edge_verts_remaped = set(old_verts_remap[old_vert] for old_vert in old_edge_verts)
|
||||
|
||||
identical_edge = None
|
||||
for edge in footprint_edges:
|
||||
if set(edge.verts) == edge_verts_remaped:
|
||||
identical_edge = edge
|
||||
break
|
||||
assert identical_edge
|
||||
|
||||
if separate_verts:
|
||||
verts_to_move = find_other_polygon_verts(identical_edge)
|
||||
for v in verts_to_move:
|
||||
vert_co = v.co
|
||||
new_vert_co = change_angle(vert_co, edge_verts_remaped, defined_angle)
|
||||
verts_to_rip.append([v, new_vert_co, identical_edge])
|
||||
else:
|
||||
process_later.append([identical_edge, edge_verts_remaped, defined_angle])
|
||||
|
||||
if defined_angle >= pi / 2:
|
||||
bottom_chords_to_remove.append(identical_edge)
|
||||
|
||||
def separate_vert(
|
||||
bm: bmesh.types.BMesh, vert: bmesh.types.BMVert, edge: bmesh.types.BMEdge, new_co: Vector
|
||||
) -> bmesh.types.BMVert:
|
||||
face = next(f for f in vert.link_faces if edge in f.edges)
|
||||
new_v = bmesh.utils.face_vert_separate(face, vert)
|
||||
new_v.co = new_co
|
||||
new_edge = bm.edges.new((vert, new_v))
|
||||
|
||||
for cur_edge in face.edges:
|
||||
if cur_edge == edge:
|
||||
continue
|
||||
bmesh.ops.contextual_create(bm, geom=[new_edge, cur_edge])
|
||||
return new_v
|
||||
|
||||
# correct angle for verts that require verts separation
|
||||
# store separated verts for angle correction later
|
||||
related_verts = {}
|
||||
for v, new_co, edge in verts_to_rip:
|
||||
new_v = separate_vert(bm, v, edge, new_co)
|
||||
related_verts.setdefault(v, []).append(new_v)
|
||||
|
||||
# verts angle correction.
|
||||
# we're taking into account new verts created after verts separation
|
||||
# required for asymmetrical gable roof
|
||||
for identical_edge, edge_verts_remaped, defined_angle in process_later:
|
||||
verts_to_move = find_other_polygon_verts(identical_edge)
|
||||
for v in verts_to_move[:]:
|
||||
verts_to_move.extend(related_verts.get(v, []))
|
||||
|
||||
for v in verts_to_move:
|
||||
vert_co = verts_to_change.get(v, v.co)
|
||||
new_vert_co = change_angle(vert_co, edge_verts_remaped, defined_angle)
|
||||
verts_to_change[v] = new_vert_co
|
||||
|
||||
# apply all changes once at the end
|
||||
for v in verts_to_change:
|
||||
v.co = verts_to_change[v]
|
||||
|
||||
bmesh.ops.delete(bm, geom=bottom_chords_to_remove, context="EDGES")
|
||||
|
||||
# add roof thickness
|
||||
extrusion_geom = bm_sort_out_geom(bmesh.ops.extrude_face_region(bm, geom=bm.faces)["geom"])
|
||||
extruded_edges = extrusion_geom["edges"]
|
||||
extruded_verts = extrusion_geom["verts"]
|
||||
rafter_edge_angle = pi / 2 - rafter_edge_angle
|
||||
default_offset_dir = Vector([0, 0, 1]) * roof_thickness
|
||||
footprint_verts = set()
|
||||
|
||||
if not tool.Cad.is_x(rafter_edge_angle, 0):
|
||||
footprint_edges = []
|
||||
for edge in extruded_edges:
|
||||
if is_footprint_edge(edge):
|
||||
footprint_edges.append(edge)
|
||||
footprint_verts.update(edge.verts)
|
||||
|
||||
def get_top_vert(v):
|
||||
non_footprint_verts = []
|
||||
for edge in v.link_edges:
|
||||
v1 = edge.other_vert(v)
|
||||
if not is_footprint_vert(v1):
|
||||
non_footprint_verts.append(v1)
|
||||
|
||||
if len(non_footprint_verts) == 1:
|
||||
return non_footprint_verts[0]
|
||||
|
||||
return min(non_footprint_verts, key=lambda v1: (v1.co - v.co).length)
|
||||
|
||||
top_verts = dict()
|
||||
for v in footprint_verts:
|
||||
top_verts[v] = get_top_vert(v)
|
||||
|
||||
# TODO: rafter_edge_angle might differ
|
||||
# if footprint edges are not connected by 90 degrees
|
||||
verts_offsets = dict()
|
||||
for edge in footprint_edges:
|
||||
v0, v1 = edge.verts
|
||||
edge_dir = (v0.co - v1.co).normalized()
|
||||
offset_dir = Matrix.Rotation(rafter_edge_angle, 4, edge_dir) @ default_offset_dir
|
||||
for v in edge.verts:
|
||||
previous_offset = verts_offsets.get(v, None)
|
||||
if previous_offset:
|
||||
previous_offset.xy += offset_dir.xy
|
||||
footprint_edges.add(edge)
|
||||
if len(edge.link_faces) != 1 or len(edge.link_faces[0].verts) != 3:
|
||||
continue # Too complicated for us to figure out without a weighted skeleton
|
||||
face_verts = edge.link_faces[0].verts
|
||||
verts = set([v.co.copy().freeze() for v in edge.verts])
|
||||
for angled_edge, edge_angle in angled_edges:
|
||||
if angled_edge == verts:
|
||||
face_angles[edge.link_faces[0]] = edge_angle
|
||||
ridge_vert = [v for v in face_verts if v.co.copy().freeze() not in verts][0]
|
||||
if len(ridge_vert.link_edges) == 3:
|
||||
ridge_edge = [e for e in ridge_vert.link_edges if e not in edge.link_faces[0].edges][0]
|
||||
other_ridge_vert = ridge_edge.other_vert(ridge_vert)
|
||||
else:
|
||||
verts_offsets[v] = offset_dir
|
||||
# We cannot actually get this correct without a weighted
|
||||
# skeleton, but for now let's assume we duplicate the ridge
|
||||
# vert and slide the vertex towards the midpoint of the
|
||||
# footprint edge.
|
||||
edge_midpoint = edge.verts[0].co.lerp(edge.verts[1].co, 0.5)
|
||||
edge_midpoint.z = ridge_vert.co.z
|
||||
poke = bmesh.ops.poke(bm, faces=[edge.link_faces[0]])
|
||||
other_ridge_vert = poke["verts"][0]
|
||||
other_ridge_vert.co = ridge_vert.co + (edge_midpoint - ridge_vert.co).normalized() * 0.01
|
||||
ridge_vert, other_ridge_vert = other_ridge_vert, ridge_vert
|
||||
|
||||
for v in verts_offsets:
|
||||
vert_offset = verts_offsets[v]
|
||||
top = top_verts[v].co
|
||||
bot = v.co + default_offset_dir
|
||||
base = v.co
|
||||
offsetted = v.co + vert_offset
|
||||
# all this math is needed to maintain the same roof thickness
|
||||
# for different rafter edge angles
|
||||
v.co = mathutils.geometry.intersect_line_line(bot, top, base, offsetted)[0]
|
||||
face_center = (face_verts[0].co + face_verts[1].co + face_verts[2].co) / 3
|
||||
x_axis = (edge.verts[1].co - edge.verts[0].co).normalized()
|
||||
z_axis = Vector((0, 0, -1))
|
||||
y_axis = z_axis.cross(x_axis)
|
||||
positive = edge.verts[0].co + (y_axis * 0.01)
|
||||
negative = edge.verts[0].co - (y_axis * 0.01)
|
||||
if (face_center - positive).length < (face_center - negative).length:
|
||||
y_axis = -y_axis
|
||||
x_axis = y_axis.cross(z_axis) # Recalculate X axis to make rotation sign consistent
|
||||
rotation_quaternion = Quaternion(x_axis, edge_angle)
|
||||
plane_no = rotation_quaternion @ z_axis
|
||||
|
||||
if intersect := tool.Cad.intersect_edge_plane(
|
||||
other_ridge_vert.co, ridge_vert.co, edge.verts[0].co, plane_no
|
||||
):
|
||||
edge_percent = tool.Cad.edge_percent(intersect, (other_ridge_vert.co, ridge_vert.co))
|
||||
if edge_percent <= 0:
|
||||
ridge_vert.co = other_ridge_vert.co
|
||||
else:
|
||||
ridge_vert.co = intersect
|
||||
|
||||
if tool.Cad.is_x(edge_angle, pi / 2, tolerance=radians(1)):
|
||||
footprint_edges.remove(edge)
|
||||
edges_to_delete.add(edge)
|
||||
gable_edges.update(
|
||||
[e for e in ridge_vert.link_edges if e.other_vert(ridge_vert) != other_ridge_vert]
|
||||
)
|
||||
break
|
||||
|
||||
bmesh.ops.delete(bm, geom=list(edges_to_delete), context="EDGES")
|
||||
|
||||
# Determine cutting planes for rafter_edge_angle
|
||||
# The skeleton is always extruded in the -Z. A cutting plane is applied at
|
||||
# the footprint or gable edge if a rafter_edge_angle is specified.
|
||||
cutting_planes = {}
|
||||
bm.faces.ensure_lookup_table()
|
||||
|
||||
if not tool.Cad.is_x(rafter_edge_angle, 0, tolerance=radians(1)):
|
||||
for e in footprint_edges:
|
||||
face = e.link_faces[0]
|
||||
planes = []
|
||||
verts = [v.co for v in face.verts]
|
||||
face_center = sum(verts[1:], start=verts[0].copy()) / len(verts)
|
||||
v1, v2 = [v.co.copy() for v in e.verts]
|
||||
if not tool.Cad.is_x(v1.z, footprint_z):
|
||||
v1, v2 = v2, v1
|
||||
x_axis = (v2 - v1).normalized()
|
||||
z_axis = Vector((0, 0, -1))
|
||||
y_axis = z_axis.cross(x_axis)
|
||||
positive = v1 + (y_axis * 0.01)
|
||||
negative = v1 - (y_axis * 0.01)
|
||||
plane_no = y_axis
|
||||
if (face_center - positive).length < (face_center - negative).length:
|
||||
plane_no = -y_axis
|
||||
x_axis = plane_no.cross(z_axis) # Recalculate X axis to make rotation sign consistent
|
||||
rotation_quaternion = Quaternion(x_axis, rafter_edge_angle)
|
||||
plane_no = rotation_quaternion @ plane_no
|
||||
planes.append((v1, plane_no))
|
||||
cutting_planes[face] = planes
|
||||
|
||||
# Gable edges are special - they are not cut at the rafter_edge_angle.
|
||||
# We copy the neighbouring cutting planes to make sure they mitre
|
||||
# correctly.
|
||||
for e in gable_edges:
|
||||
face = e.link_faces[0]
|
||||
planes = []
|
||||
neighbouring_faces = set()
|
||||
neighbouring_faces.update(e.verts[0].link_faces)
|
||||
neighbouring_faces.update(e.verts[1].link_faces)
|
||||
for f in neighbouring_faces:
|
||||
planes.extend(cutting_planes.get(f, []))
|
||||
cutting_planes[face] = planes
|
||||
|
||||
# Extrude skeleton using roof thickness
|
||||
for top_face in [f for f in bm.faces]:
|
||||
face = top_face.copy()
|
||||
face.tag = True # Tags keep track of which geometry is part of our current skeleton fragment
|
||||
|
||||
if face.normal.z > 0:
|
||||
face.normal_flip()
|
||||
|
||||
# This means that different fragments of the roof may not exactly join together.
|
||||
# It's technically correct, but may look weird if the architect doesn't know what they're doing.
|
||||
face_angle = face_angles.get(top_face, angle)
|
||||
# Alternatively maybe we can offer a "distort" mode which doesn't preserve thickness at custom angles:
|
||||
# face_angle = angle
|
||||
extrusion_height = roof_thickness / cos(face_angle)
|
||||
extrusion_vector = Vector((0, 0, -1)) * extrusion_height
|
||||
extrusion = tool.Model.bm_sort_out_geom(bmesh.ops.extrude_face_region(bm, geom=[face])["geom"])
|
||||
bmesh.ops.translate(bm, vec=extrusion_vector, verts=extrusion["verts"])
|
||||
|
||||
for plane_co, plane_no in cutting_planes.get(top_face, []):
|
||||
geom = set()
|
||||
for f in bm.faces:
|
||||
if f.tag:
|
||||
geom.add(f)
|
||||
for e in f.edges:
|
||||
geom.add(e)
|
||||
for v in f.verts:
|
||||
geom.add(v)
|
||||
|
||||
result = bmesh.ops.bisect_plane(
|
||||
bm, geom=list(geom), plane_co=plane_co, plane_no=plane_no, clear_outer=True, clear_inner=False
|
||||
)
|
||||
for g in result["geom"]:
|
||||
if isinstance(g, bmesh.types.BMFace):
|
||||
g.tag = True
|
||||
bm.faces.ensure_lookup_table()
|
||||
result = bmesh.ops.triangle_fill(
|
||||
bm, use_dissolve=True, edges=[g for g in result["geom_cut"] if isinstance(g, bmesh.types.BMEdge)]
|
||||
)
|
||||
for g in result["geom"]:
|
||||
if isinstance(g, bmesh.types.BMFace):
|
||||
g.tag = True
|
||||
verts = set()
|
||||
[verts.update(f.verts) for f in bm.faces if f.tag]
|
||||
bmesh.ops.remove_doubles(bm, verts=list(verts), dist=1e-4)
|
||||
|
||||
for f in bm.faces:
|
||||
f.tag = False
|
||||
|
||||
verts = [v for v in extruded_verts if v not in footprint_verts]
|
||||
bmesh.ops.translate(bm, vec=default_offset_dir, verts=verts)
|
||||
bmesh.ops.recalc_face_normals(bm, faces=bm.faces[:])
|
||||
|
||||
# Merge fragments and remove internal faces.
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-4)
|
||||
faces_to_delete = set()
|
||||
for face in [f for f in bm.faces]:
|
||||
is_internal = True
|
||||
for e in face.edges:
|
||||
if len(e.link_faces) < 3:
|
||||
is_internal = False
|
||||
if is_internal:
|
||||
faces_to_delete.add(face)
|
||||
bmesh.ops.delete(bm, geom=list(faces_to_delete), context="FACES")
|
||||
return bm
|
||||
|
||||
|
||||
@@ -414,9 +427,6 @@ def update_roof_modifier_ifc_data(context: bpy.types.Context) -> None:
|
||||
if props.roof_type == "HIP/GABLE ROOF":
|
||||
element.PredefinedType = "GABLE_ROOF" if roof_is_gabled() else "HIP_ROOF"
|
||||
|
||||
# occurrences attributes
|
||||
# occurrences = tool.Ifc.get_all_element_occurrences(element)
|
||||
|
||||
tool.Model.add_body_representation(obj)
|
||||
|
||||
|
||||
@@ -438,32 +448,27 @@ def update_roof_modifier_bmesh(obj: bpy.types.Object) -> None:
|
||||
# NOTE: using Data since bmesh update will hapen very often
|
||||
if not RoofData.is_loaded:
|
||||
RoofData.load()
|
||||
path_data = RoofData.data["pset_data"]["data_dict"]["path_data"]
|
||||
path_data = RoofData.data["path_data"]
|
||||
angle_layer_data = path_data.get("gable_roof_angles", None)
|
||||
separate_verts_data = path_data.get("gable_roof_separate_verts", None)
|
||||
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
# need to make sure we support edit mode
|
||||
# since users will probably be in edit mode when they'll be changing roof path
|
||||
bm = tool.Blender.get_bmesh_for_mesh(obj.data, clean=True)
|
||||
angle_layer = bm.edges.layers.float.new("BBIM_gable_roof_angles")
|
||||
separate_verts_layer = bm.edges.layers.int.new("BBIM_gable_roof_separate_verts")
|
||||
|
||||
# generating roof path
|
||||
new_verts = [bm.verts.new(Vector(v) * si_conversion) for v in path_data["verts"]]
|
||||
new_edges = []
|
||||
for i in range(len(path_data["edges"])):
|
||||
e = path_data["edges"][i]
|
||||
edge = bm.edges.new((new_verts[e[0]], new_verts[e[1]]))
|
||||
edge[angle_layer] = angle_layer_data[i] if angle_layer_data else 0
|
||||
edge[separate_verts_layer] = separate_verts_data[i] if separate_verts_data else 0
|
||||
new_edges.append(edge)
|
||||
|
||||
if props.is_editing_path:
|
||||
tool.Blender.apply_bmesh(obj.data, bm)
|
||||
return
|
||||
|
||||
generate_hiped_roof_bmesh(
|
||||
generate_hipped_roof_bmesh(
|
||||
bm,
|
||||
props.generation_method,
|
||||
props.height,
|
||||
@@ -488,15 +493,12 @@ def get_path_data(obj: bpy.types.Object) -> Union[dict[str, Any], None]:
|
||||
bm_mesh_clean_up(bm)
|
||||
|
||||
angle_layer = bm.edges.layers.float.get("BBIM_gable_roof_angles")
|
||||
separate_verts_layer = bm.edges.layers.int.get("BBIM_gable_roof_separate_verts")
|
||||
|
||||
path_data = dict()
|
||||
path_data["edges"] = [bm_get_indices(e.verts) for e in bm.edges]
|
||||
path_data["verts"] = [v.co / si_conversion for v in bm.verts]
|
||||
if angle_layer:
|
||||
path_data["gable_roof_angles"] = [e[angle_layer] for e in bm.edges]
|
||||
if separate_verts_layer:
|
||||
path_data["gable_roof_separate_verts"] = [e[separate_verts_layer] for e in bm.edges]
|
||||
|
||||
if not path_data["edges"] or not path_data["verts"]:
|
||||
return None
|
||||
@@ -693,7 +695,7 @@ class EnableEditingRoofPath(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
main_bm.edges.layers.int.new("BBIM_preview")
|
||||
|
||||
second_bm = generate_hiped_roof_bmesh(
|
||||
second_bm = generate_hipped_roof_bmesh(
|
||||
bm,
|
||||
props.generation_method,
|
||||
props.height,
|
||||
@@ -702,7 +704,6 @@ class EnableEditingRoofPath(bpy.types.Operator, tool.Ifc.Operator):
|
||||
props.rafter_edge_angle,
|
||||
mutate_current_bmesh=False,
|
||||
)
|
||||
bmesh.ops.translate(second_bm, verts=second_bm.verts, vec=Vector((0, 0, 1)))
|
||||
|
||||
tool.Blender.bmesh_join(main_bm, second_bm, callback=mark_preview_edges)
|
||||
return main_bm
|
||||
@@ -739,6 +740,41 @@ class CancelEditingRoofPath(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return cancel_editing_roof_path(context)
|
||||
|
||||
|
||||
class CopyRoofParameters(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.copy_roof_parameters"
|
||||
bl_label = "Copy Roof Parameters from Active to Selected"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.active_object and len(context.selected_objects) > 1
|
||||
|
||||
def _execute(self, context):
|
||||
source_obj = context.active_object
|
||||
source_props = source_obj.BIMRoofProperties
|
||||
data = source_props.get_general_kwargs(convert_to_project_units=True)
|
||||
|
||||
for target_obj in context.selected_objects:
|
||||
if target_obj == source_obj:
|
||||
continue
|
||||
context.view_layer.objects.active = target_obj
|
||||
RoofData.load()
|
||||
if not "path_data" in RoofData.data:
|
||||
continue
|
||||
data["path_data"] = RoofData.data["path_data"]
|
||||
target_element = tool.Ifc.get_entity(target_obj)
|
||||
target_props = target_obj.BIMRoofProperties
|
||||
|
||||
target_props.set_props_kwargs_from_ifc_data(data)
|
||||
update_bbim_roof_pset(target_element, data)
|
||||
refresh()
|
||||
update_roof_modifier_bmesh(target_obj)
|
||||
update_roof_modifier_ifc_data(context)
|
||||
|
||||
context.view_layer.objects.active = source_obj
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class FinishEditingRoofPath(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.finish_editing_roof_path"
|
||||
bl_label = "Finish Editing Roof Path"
|
||||
@@ -792,7 +828,6 @@ class SetGableRoofEdgeAngle(bpy.types.Operator):
|
||||
bl_label = "Set Gable Roof Edge Angle"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
angle: bpy.props.FloatProperty(name="Angle", default=90)
|
||||
separate_verts: bpy.props.BoolProperty(name="Separate Verts", default=True)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@@ -815,17 +850,12 @@ class SetGableRoofEdgeAngle(bpy.types.Operator):
|
||||
if "BBIM_gable_roof_angles" not in me.attributes:
|
||||
me.attributes.new("BBIM_gable_roof_angles", type="FLOAT", domain="EDGE")
|
||||
|
||||
if "BBIM_gable_roof_separate_verts" not in me.attributes:
|
||||
me.attributes.new("BBIM_gable_roof_separate_verts", type="INT", domain="EDGE")
|
||||
|
||||
angles_layer = bm.edges.layers.float["BBIM_gable_roof_angles"]
|
||||
separate_verts_layer = bm.edges.layers.int["BBIM_gable_roof_separate_verts"]
|
||||
|
||||
for e in bm.edges:
|
||||
if not e.select:
|
||||
continue
|
||||
e[angles_layer] = self.angle
|
||||
e[separate_verts_layer] = self.separate_verts
|
||||
|
||||
tool.Blender.apply_bmesh(me, bm)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -188,6 +188,8 @@ class DumbSlabGenerator:
|
||||
|
||||
pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="EPset_Parametric")
|
||||
ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Engine": "Bonsai.DumbLayer3"})
|
||||
material = ifcopenshell.util.element.get_material(element)
|
||||
material.LayerSetDirection = "AXIS3"
|
||||
obj.select_set(True)
|
||||
return obj
|
||||
|
||||
|
||||
@@ -84,10 +84,9 @@ class LaunchTypeManager(bpy.types.Operator):
|
||||
bl_idname = "bim.launch_type_manager"
|
||||
bl_label = "Launch Type Manager"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Display all available Construction Types to add new instances"
|
||||
bl_description = "Browse, Edit and Manage Types"
|
||||
|
||||
def execute(self, context):
|
||||
bpy.ops.bim.hotkey("INVOKE_DEFAULT", hotkey="S_A")
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
@@ -101,40 +100,47 @@ class LaunchTypeManager(bpy.types.Operator):
|
||||
# will be None if project has no types
|
||||
if ifc_class is not None:
|
||||
bpy.ops.bim.load_type_thumbnails(ifc_class=ifc_class, offset=0, limit=9)
|
||||
return context.window_manager.invoke_props_dialog(
|
||||
self, width=550, title="Type Manager", confirm_text="Add Type"
|
||||
)
|
||||
return context.window_manager.invoke_props_dialog(self, width=550, title="Type Manager", confirm_text="Close")
|
||||
|
||||
def draw(self, context):
|
||||
props = context.scene.BIMModelProperties
|
||||
row = self.layout.row(align=True)
|
||||
text = f"{AuthoringData.data['total_types']} {AuthoringData.data['ifc_element_type'] or 'Types'}"
|
||||
if AuthoringData.data["total_types"] > 1:
|
||||
text = f"{AuthoringData.data['total_types']} {AuthoringData.data['ifc_element_type']}s"
|
||||
else:
|
||||
text = f"{AuthoringData.data['total_types']} {AuthoringData.data['ifc_element_type']}"
|
||||
text += "s"
|
||||
row.label(text=text, icon="FILE_VOLUME")
|
||||
# prop_with_search(row, props, "ifc_class", text="", should_click_ok_to_validate=True)
|
||||
row.menu("BIM_MT_type_manager_menu", text="", icon="PREFERENCES")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.launch_add_element", text=f"Create New {AuthoringData.data['ifc_element_type']}", icon="ADD")
|
||||
op = row.operator(
|
||||
"bim.add_element", text=f"Create New {AuthoringData.data['ifc_element_type'] or 'Type'}", icon="ADD"
|
||||
)
|
||||
op.is_specific_tool = bool(AuthoringData.data["ifc_element_type"])
|
||||
op.ifc_product = "IfcElementType"
|
||||
op.ifc_class = AuthoringData.data["ifc_element_type"] or props.ifc_class or ""
|
||||
|
||||
if AuthoringData.data["total_types"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "search_name", icon="FILTER", text="")
|
||||
|
||||
columns = self.layout.column_flow(columns=3)
|
||||
row = columns.row(align=True)
|
||||
row.alignment = "CENTER"
|
||||
### In case you want something here in the future
|
||||
if AuthoringData.data["total_pages"] > 0:
|
||||
row = columns.row(align=True)
|
||||
row.alignment = "RIGHT"
|
||||
row2 = row.row(align=True)
|
||||
row2.label(text="Page")
|
||||
row2.prop(props, "type_page", text="", emboss=False)
|
||||
row2.label(text=f"/{AuthoringData.data['total_pages']} ")
|
||||
|
||||
###
|
||||
prev_page_op = row.row(align=True)
|
||||
op = prev_page_op.operator("bim.change_type_page", icon="TRIA_LEFT", text="")
|
||||
op.page = AuthoringData.data["prev_page"] or 0
|
||||
prev_page_op.enabled = op.page > 0
|
||||
|
||||
row = columns.row(align=True)
|
||||
row.alignment = "RIGHT"
|
||||
if AuthoringData.data["total_pages"] > 1:
|
||||
row.label(text=f"Page {props.type_page}/{AuthoringData.data['total_pages']} ")
|
||||
if AuthoringData.data["prev_page"]:
|
||||
op = row.operator("bim.change_type_page", icon="TRIA_LEFT", text="")
|
||||
op.page = AuthoringData.data["prev_page"]
|
||||
if AuthoringData.data["next_page"]:
|
||||
op = row.operator("bim.change_type_page", icon="TRIA_RIGHT", text="")
|
||||
op.page = AuthoringData.data["next_page"]
|
||||
next_page_op = row.row(align=True)
|
||||
op = next_page_op.operator("bim.change_type_page", icon="TRIA_RIGHT", text="")
|
||||
op.page = AuthoringData.data["next_page"] or 0
|
||||
next_page_op.enabled = op.page > 0
|
||||
|
||||
flow = self.layout.grid_flow(row_major=True, columns=3, even_columns=True, even_rows=True, align=True)
|
||||
|
||||
@@ -142,11 +148,11 @@ class LaunchTypeManager(bpy.types.Operator):
|
||||
outer_col = flow.column()
|
||||
box = outer_col.box()
|
||||
|
||||
row = box.row(align=True)
|
||||
row = box.row()
|
||||
op = row.operator("bim.set_active_type", text=relating_type["name"], icon="BLANK1", emboss=False)
|
||||
op.relating_type = relating_type["id"]
|
||||
|
||||
op = row.operator("bim.launch_type_menu", icon="DOWNARROW_HLT", text="", emboss=False)
|
||||
op = row.operator("bim.launch_type_menu", icon="PREFERENCES", text="", emboss=True)
|
||||
op.relating_type_id = relating_type["id"]
|
||||
|
||||
row = box.row()
|
||||
@@ -163,7 +169,15 @@ class LaunchTypeManager(bpy.types.Operator):
|
||||
row2.operator("bim.set_active_type", text="", emboss=False).relating_type = relating_type["id"]
|
||||
row2.operator("bim.set_active_type", text="", emboss=False).relating_type = relating_type["id"]
|
||||
row2.operator("bim.set_active_type", text="", emboss=False).relating_type = relating_type["id"]
|
||||
row2.operator("bim.set_active_type", text="", emboss=False).relating_type = relating_type["id"]
|
||||
is_current_relating_type = str(relating_type["id"]) == str(
|
||||
AuthoringData.data["relating_type_id_current"]
|
||||
)
|
||||
if is_current_relating_type:
|
||||
active_row = row2.row()
|
||||
active_row.alignment = "CENTER"
|
||||
active_row.label(text="Active", icon="CHECKMARK")
|
||||
else:
|
||||
row2.operator("bim.set_active_type", text="", emboss=False).relating_type = relating_type["id"]
|
||||
else:
|
||||
row = box.row()
|
||||
op = box.operator("bim.load_type_thumbnails", text="", icon="FILE_REFRESH", emboss=False)
|
||||
@@ -302,6 +316,15 @@ class BIM_PT_stair(bpy.types.Panel):
|
||||
self.layout.prop(props, prop_name, text="")
|
||||
else:
|
||||
self.layout.prop(props, prop_name)
|
||||
if prop_name == "height": # Weak but we just want to insert this inside props drawing
|
||||
row_length = self.layout.row(align=True)
|
||||
row_length.prop(props, "total_length_target")
|
||||
row_length.prop(
|
||||
props,
|
||||
"total_length_lock",
|
||||
text="",
|
||||
icon="LOCKED" if props.total_length_lock else "UNLOCKED",
|
||||
)
|
||||
regenerate_stair_mesh(obj)
|
||||
else:
|
||||
calculated_params = StairData.data["calculated_params"]
|
||||
@@ -548,7 +571,7 @@ class BIM_PT_door(bpy.types.Panel):
|
||||
else:
|
||||
row = self.layout.row()
|
||||
row.label(text="No Door Found")
|
||||
row.operator("bim.add_door", icon="ADD", text="").obj = ""
|
||||
row.operator("bim.add_door", icon="ADD", text="")
|
||||
|
||||
|
||||
class BIM_PT_railing(bpy.types.Panel):
|
||||
@@ -596,6 +619,7 @@ class BIM_PT_railing(bpy.types.Panel):
|
||||
|
||||
else:
|
||||
row.operator("bim.enable_editing_railing", icon="GREASEPENCIL", text="")
|
||||
row.operator("bim.copy_railing_parameters", icon="COPYDOWN", text="")
|
||||
row.operator("bim.enable_editing_railing_path", icon="ANIM", text="")
|
||||
# TODO: good for preview but probably should move to .is_editing == True
|
||||
# since it's writing to ifc
|
||||
@@ -648,13 +672,12 @@ class BIM_PT_roof(bpy.types.Panel):
|
||||
self.layout.prop(props, prop)
|
||||
|
||||
update_roof_modifier_bmesh(obj)
|
||||
|
||||
elif props.is_editing_path:
|
||||
row.operator("bim.finish_editing_roof_path", icon="CHECKMARK", text="")
|
||||
row.operator("bim.cancel_editing_roof_path", icon="CANCEL", text="")
|
||||
|
||||
else:
|
||||
row.operator("bim.enable_editing_roof", icon="GREASEPENCIL", text="")
|
||||
row.operator("bim.copy_roof_parameters", icon="COPYDOWN", text="")
|
||||
row.operator("bim.enable_editing_roof_path", icon="ANIM", text="")
|
||||
row.operator("bim.remove_roof", icon="X", text="")
|
||||
|
||||
|
||||
@@ -421,6 +421,8 @@ class DrawPolylineWall(bpy.types.Operator, PolylineOperator):
|
||||
|
||||
self.handle_inserting_polyline(context, event)
|
||||
|
||||
self.get_product_preview_data(context, self.relating_type)
|
||||
|
||||
cancel = self.handle_cancelation(context, event)
|
||||
if cancel is not None:
|
||||
ProductDecorator.uninstall()
|
||||
@@ -616,6 +618,8 @@ class DumbWallGenerator:
|
||||
polyline_points = extrusion.SweptArea.OuterCurve.Points.CoordList
|
||||
polyline_points = [[(v * self.unit_scale) for v in p] for p in polyline_points]
|
||||
polyline_points = [slab_obj.matrix_world @ Vector((p[0], p[1], elevation)) for p in polyline_points]
|
||||
if not tool.Cad.is_counter_clockwise_order(polyline_points[0], polyline_points[1], polyline_points[2]):
|
||||
polyline_points = polyline_points[::-1]
|
||||
is_polyline_closed = True
|
||||
walls = []
|
||||
for i in range(len(polyline_points) - 1):
|
||||
@@ -803,6 +807,8 @@ class DumbWallGenerator:
|
||||
)
|
||||
pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="EPset_Parametric")
|
||||
ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Engine": "Bonsai.DumbLayer2"})
|
||||
material = ifcopenshell.util.element.get_material(element)
|
||||
material.LayerSetDirection = "AXIS2"
|
||||
obj.select_set(True)
|
||||
return obj
|
||||
|
||||
|
||||
@@ -32,24 +32,10 @@ from bonsai.bim.module.model.prop import get_ifc_class
|
||||
from typing import Optional, Union
|
||||
|
||||
|
||||
# TODO duplicate code in cad/workspace and model/workspace
|
||||
def check_display_mode():
|
||||
global display_mode
|
||||
try:
|
||||
theme = bpy.context.preferences.themes["Default"]
|
||||
text_color = theme.user_interface.wcol_menu_item.text
|
||||
if sum(text_color) < 2.6:
|
||||
display_mode = "lm"
|
||||
else:
|
||||
display_mode = "dm"
|
||||
except:
|
||||
display_mode = "dm"
|
||||
|
||||
|
||||
def load_custom_icons():
|
||||
global custom_icon_previews
|
||||
global custom_icon_previews, display_mode
|
||||
if display_mode is None:
|
||||
check_display_mode()
|
||||
display_mode = tool.Blender.detect_icon_color_mode("user_interface.wcol_tool.text")
|
||||
|
||||
icons_dir = os.path.join(os.path.dirname(__file__), "..", "..", "data", "icons")
|
||||
custom_icon_previews = bpy.utils.previews.new()
|
||||
@@ -118,10 +104,6 @@ class BimTool(WorkSpaceTool):
|
||||
EditObjectUI.draw(context, layout, ifc_element_type=cls.ifc_element_type)
|
||||
else:
|
||||
CreateObjectUI.draw(context, layout, ifc_element_type=cls.ifc_element_type)
|
||||
# Show some UI for spatial elements that are unselectable by default.
|
||||
if active_ifc_object:
|
||||
EditObjectUI.layout = layout # Prevent .draw_modes from using old layout and crash.
|
||||
EditObjectUI.draw_modes(context)
|
||||
|
||||
|
||||
class WallTool(BimTool):
|
||||
@@ -235,8 +217,18 @@ class CableTool(BimTool):
|
||||
|
||||
|
||||
def add_layout_hotkey_operator(
|
||||
layout: bpy.types.UILayout, text: str, hotkey: str, description: Union[str, None], ui_context: str = ""
|
||||
layout: bpy.types.UILayout,
|
||||
text: str,
|
||||
hotkey: str,
|
||||
description: Union[str, None],
|
||||
ui_context: str = "",
|
||||
*,
|
||||
operator: str = "bim.hotkey",
|
||||
) -> bpy.types.OperatorProperties:
|
||||
"""
|
||||
:param operator: Operator to display in UI. Displaying the specific operator in UI can be useful
|
||||
to provide poll error messages.
|
||||
"""
|
||||
parts = hotkey.split("_") if hotkey else []
|
||||
modifier, key = (parts + ["", ""])[:2]
|
||||
|
||||
@@ -245,7 +237,7 @@ def add_layout_hotkey_operator(
|
||||
modifier_icon, modifier_str = MODIFIERS.get(modifier, ("NONE", ""))
|
||||
|
||||
row = layout.row(align=True)
|
||||
op = row.operator("bim.hotkey", text=op_text, icon_value=custom_icon)
|
||||
op = row.operator(operator, text=op_text, icon_value=custom_icon)
|
||||
|
||||
if ui_context != "TOOL_HEADER":
|
||||
row.label(text="", icon=modifier_icon)
|
||||
@@ -254,11 +246,12 @@ def add_layout_hotkey_operator(
|
||||
hotkey_description = f"Hotkey: {modifier_str} {key}".strip()
|
||||
description = "\n\n".join(filter(None, [description, hotkey_description]))
|
||||
|
||||
op.hotkey = hotkey
|
||||
if ui_context == "TOOL_HEADER":
|
||||
op.description = text + "\n" + description
|
||||
else:
|
||||
op.description = description
|
||||
if operator == "bim.hotkey":
|
||||
op.hotkey = hotkey
|
||||
if ui_context == "TOOL_HEADER":
|
||||
op.description = text + "\n" + description
|
||||
else:
|
||||
op.description = description
|
||||
return op
|
||||
|
||||
|
||||
@@ -306,30 +299,41 @@ class BIM_MT_add_representation_item(Menu):
|
||||
ItemData.load()
|
||||
|
||||
if ItemData.data["representation_type"] in ("Tessellation", "Brep", "AdvancedBrep"):
|
||||
self.layout.operator("bim.add_meshlike_item", icon="MESH_PLANE", text="Mesh Plane").shape = "PLANE"
|
||||
self.layout.operator("bim.add_meshlike_item", icon="MESH_CUBE", text="Mesh Cube").shape = "CUBE"
|
||||
self.layout.operator("bim.add_meshlike_item", icon="MESH_CIRCLE", text="Mesh Circle").shape = "CIRCLE"
|
||||
self.layout.operator("bim.add_meshlike_item", icon="MESH_UVSPHERE", text="Mesh UV Sphere").shape = (
|
||||
"UVSPHERE"
|
||||
)
|
||||
self.layout.operator("bim.add_meshlike_item", icon="MESH_ICOSPHERE", text="Mesh Icosphere").shape = (
|
||||
"ICOSPHERE"
|
||||
)
|
||||
self.layout.operator("bim.add_meshlike_item", icon="MESH_CYLINDER", text="Mesh Cylinder").shape = "CYLINDER"
|
||||
|
||||
if ItemData.data["representation_type"] in ("SolidModel", "SweptSolid"):
|
||||
self.layout.operator(
|
||||
"bim.add_swept_area_solid_item", icon="MESH_CUBE", text="Extruded Area Solid Cube"
|
||||
).shape = "CUBE"
|
||||
self.layout.operator(
|
||||
"bim.add_swept_area_solid_item", icon="MESH_CYLINDER", text="Extruded Area Solid Cylinder"
|
||||
).shape = "CYLINDER"
|
||||
|
||||
if ItemData.data["representation_type"] in ("Annotation2D"):
|
||||
self.draw_meshlike()
|
||||
self.layout.separator()
|
||||
self.layout.operator("bim.add_half_space_solid_item", icon="ORIENTATION_NORMAL", text="Half Space Solid")
|
||||
elif ItemData.data["representation_type"] in ("SolidModel", "SweptSolid"):
|
||||
self.draw_swept_area()
|
||||
self.layout.separator()
|
||||
self.layout.operator("bim.add_half_space_solid_item", icon="ORIENTATION_NORMAL", text="Half Space Solid")
|
||||
elif ItemData.data["representation_type"] in ("CSG"):
|
||||
# Surprisingly, once something becomes a CSG, you can combine lots of things due to IfcBooleanOperand
|
||||
self.draw_swept_area()
|
||||
self.layout.separator()
|
||||
self.draw_meshlike()
|
||||
self.layout.separator()
|
||||
self.layout.operator("bim.add_half_space_solid_item", icon="ORIENTATION_NORMAL", text="Half Space Solid")
|
||||
elif ItemData.data["representation_type"] in ("Annotation2D"):
|
||||
self.layout.operator("bim.add_curvelike_item", icon="IPO_CONSTANT", text="Polycurve").shape = "LINE"
|
||||
self.layout.operator("bim.add_curvelike_item", icon="MESH_CIRCLE", text="Circle").shape = "CIRCLE"
|
||||
self.layout.operator("bim.add_curvelike_item", icon="MESH_CIRCLE", text="Ellipse").shape = "ELLIPSE"
|
||||
|
||||
def draw_meshlike(self):
|
||||
self.layout.operator("bim.add_meshlike_item", icon="MESH_PLANE", text="Mesh Plane").shape = "PLANE"
|
||||
self.layout.operator("bim.add_meshlike_item", icon="MESH_CUBE", text="Mesh Cube").shape = "CUBE"
|
||||
self.layout.operator("bim.add_meshlike_item", icon="MESH_CIRCLE", text="Mesh Circle").shape = "CIRCLE"
|
||||
self.layout.operator("bim.add_meshlike_item", icon="MESH_UVSPHERE", text="Mesh UV Sphere").shape = "UVSPHERE"
|
||||
self.layout.operator("bim.add_meshlike_item", icon="MESH_ICOSPHERE", text="Mesh Icosphere").shape = "ICOSPHERE"
|
||||
self.layout.operator("bim.add_meshlike_item", icon="MESH_CYLINDER", text="Mesh Cylinder").shape = "CYLINDER"
|
||||
|
||||
def draw_swept_area(self):
|
||||
self.layout.operator(
|
||||
"bim.add_swept_area_solid_item", icon="MESH_CUBE", text="Extruded Area Solid Cube"
|
||||
).shape = "CUBE"
|
||||
self.layout.operator(
|
||||
"bim.add_swept_area_solid_item", icon="MESH_CYLINDER", text="Extruded Area Solid Cylinder"
|
||||
).shape = "CYLINDER"
|
||||
|
||||
|
||||
class CreateObjectUI:
|
||||
layout: bpy.types.UILayout
|
||||
@@ -392,7 +396,7 @@ class CreateObjectUI:
|
||||
row1.operator(
|
||||
"bim.launch_type_manager",
|
||||
icon="ERROR",
|
||||
text=f"No {AuthoringData.data['ifc_element_type']}s Found",
|
||||
text=f"No {AuthoringData.data['ifc_element_type'] or 'Type'}s Found",
|
||||
emboss=False,
|
||||
)
|
||||
row1.operator(
|
||||
@@ -410,10 +414,12 @@ class CreateObjectUI:
|
||||
|
||||
row = box.row(align=True)
|
||||
row.alignment = "CENTER"
|
||||
row.operator(
|
||||
"bim.launch_add_element",
|
||||
text=f"Create New {AuthoringData.data['ifc_element_type']}",
|
||||
op = row.operator(
|
||||
"bim.add_element", text=f"Create New {AuthoringData.data['ifc_element_type'] or 'Type'}", icon="ADD"
|
||||
)
|
||||
op.is_specific_tool = bool(AuthoringData.data["ifc_element_type"])
|
||||
op.ifc_product = "IfcElementType"
|
||||
op.ifc_class = AuthoringData.data["ifc_element_type"] or props.ifc_class or ""
|
||||
|
||||
row = box.row(align=True)
|
||||
|
||||
@@ -440,8 +446,9 @@ class CreateObjectUI:
|
||||
row = cls.layout.row(align=True)
|
||||
if AuthoringData.data["relating_type_id"]:
|
||||
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
|
||||
op = row.operator("bim.hotkey", text="Add", icon_value=custom_icon_previews["ADD"].icon_id)
|
||||
op.hotkey = "S_A"
|
||||
if context.space_data.type == "VIEW_3D": # Wall polyline tool works only in 3D Space
|
||||
op = row.operator("bim.hotkey", text="Add", icon_value=custom_icon_previews["ADD"].icon_id)
|
||||
op.hotkey = "S_A"
|
||||
else:
|
||||
row.label(text="No Construction Type", icon="FILE_3D")
|
||||
|
||||
@@ -504,62 +511,58 @@ class CreateObjectUI:
|
||||
row = cls.layout.row(align=True)
|
||||
if not AuthoringData.data["ifc_element_type"]:
|
||||
prop_with_search(row, cls.props, "ifc_class", text="Type Class" if ui_context != "TOOL_HEADER" else "")
|
||||
if AuthoringData.data["ifc_classes"]:
|
||||
ifc_class = AuthoringData.data["ifc_class_current"]
|
||||
if ifc_class:
|
||||
box = cls.layout.box()
|
||||
row = box.row(align=True)
|
||||
if AuthoringData.data["type_thumbnail"] and ui_context == "TOOL_HEADER":
|
||||
row.template_icon(icon_value=AuthoringData.data["type_thumbnail"])
|
||||
row.operator("bim.launch_type_manager", text=AuthoringData.data["relating_type_name"], emboss=False)
|
||||
else:
|
||||
row.operator(
|
||||
"bim.launch_type_manager",
|
||||
icon="BLANK1",
|
||||
text=AuthoringData.data["relating_type_name"],
|
||||
emboss=False,
|
||||
)
|
||||
if not AuthoringData.data["ifc_classes"]:
|
||||
return
|
||||
if not (ifc_class := AuthoringData.data["ifc_class_current"]):
|
||||
return
|
||||
|
||||
row.operator(
|
||||
"bim.launch_type_manager",
|
||||
icon=tool.Blender.TYPE_MANAGER_ICON,
|
||||
text="",
|
||||
emboss=False,
|
||||
)
|
||||
box = cls.layout.box()
|
||||
|
||||
if ui_context != "TOOL_HEADER":
|
||||
row = box.row(align=True)
|
||||
row.alignment = "CENTER"
|
||||
row.operator(
|
||||
"bim.launch_type_manager",
|
||||
text=AuthoringData.data["relating_type_description"],
|
||||
emboss=False,
|
||||
)
|
||||
row = box.row(align=True)
|
||||
thumbnail: int = AuthoringData.data["type_thumbnail"]
|
||||
row.template_icon(icon_value=thumbnail)
|
||||
row.operator("bim.launch_type_manager", text=AuthoringData.data["relating_type_name"], emboss=False)
|
||||
row.operator(
|
||||
"bim.launch_type_manager",
|
||||
icon=tool.Blender.TYPE_MANAGER_ICON,
|
||||
text="",
|
||||
emboss=False,
|
||||
)
|
||||
|
||||
if AuthoringData.data["type_thumbnail"]:
|
||||
row1 = box.row()
|
||||
row1.ui_units_y = 0.01
|
||||
row1.template_icon(icon_value=AuthoringData.data["type_thumbnail"], scale=4)
|
||||
row2 = box.column(align=True)
|
||||
row2.ui_units_y = 4
|
||||
for _ in range(4):
|
||||
row2.operator("bim.launch_type_manager", text="", emboss=False)
|
||||
else:
|
||||
op = box.operator(
|
||||
"bim.load_type_thumbnails",
|
||||
text="",
|
||||
icon="FILE_REFRESH",
|
||||
emboss=False,
|
||||
)
|
||||
op.ifc_class = ifc_class
|
||||
if ui_context == "TOOL_HEADER":
|
||||
return
|
||||
row = box.row(align=True)
|
||||
row.alignment = "CENTER"
|
||||
row.operator(
|
||||
"bim.launch_type_manager",
|
||||
text=AuthoringData.data["relating_type_description"],
|
||||
emboss=False,
|
||||
)
|
||||
|
||||
row = box.row(align=True)
|
||||
row.alignment = "CENTER"
|
||||
row.operator(
|
||||
"bim.launch_type_manager",
|
||||
text=AuthoringData.data["predefined_type"],
|
||||
emboss=False,
|
||||
)
|
||||
if thumbnail != 0:
|
||||
row1 = box.row()
|
||||
row1.ui_units_y = 0.01
|
||||
row1.template_icon(icon_value=thumbnail, scale=4)
|
||||
row2 = box.column(align=True)
|
||||
row2.ui_units_y = 4
|
||||
for _ in range(4):
|
||||
row2.operator("bim.launch_type_manager", text="", emboss=False)
|
||||
else:
|
||||
op = box.operator(
|
||||
"bim.load_type_thumbnails",
|
||||
text="",
|
||||
icon="FILE_REFRESH",
|
||||
emboss=False,
|
||||
)
|
||||
op.ifc_class = ifc_class
|
||||
|
||||
row = box.row(align=True)
|
||||
row.alignment = "CENTER"
|
||||
row.operator(
|
||||
"bim.launch_type_manager",
|
||||
text=AuthoringData.data["predefined_type"],
|
||||
emboss=False,
|
||||
)
|
||||
|
||||
|
||||
class EditObjectUI:
|
||||
@@ -589,6 +592,17 @@ class EditObjectUI:
|
||||
AuthoringData.load(ifc_element_type)
|
||||
|
||||
if context.region.type == "TOOL_HEADER":
|
||||
if context.scene.BIMAggregateProperties.in_aggregate_mode:
|
||||
layout.label(text=f"Aggregate Mode", icon="EMPTY_AXIS")
|
||||
row = cls.layout.row(align=True)
|
||||
op = row.operator("bim.disable_aggregate_mode", text="", icon="X")
|
||||
op = row.operator("bim.toggle_aggregate_mode_local_view", text="", icon="ZOOM_SELECTED")
|
||||
if context.scene.BIMNestProperties.in_nest_mode:
|
||||
layout.label(text=f"Nest Mode", icon="EMPTY_AXIS")
|
||||
row = cls.layout.row(align=True)
|
||||
op = row.operator("bim.disable_nest_mode", text="", icon="X")
|
||||
op = row.operator("bim.toggle_nest_mode_local_view", text="", icon="ZOOM_SELECTED")
|
||||
|
||||
text = format_ifc_camel_case(AuthoringData.data["active_class"])
|
||||
layout.label(text=f"{text} Edit Tools:", icon="RESTRICT_SELECT_OFF")
|
||||
cls.draw_parameter_adjustments(context)
|
||||
@@ -698,10 +712,6 @@ class EditObjectUI:
|
||||
)
|
||||
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
|
||||
add_layout_hotkey_operator(row, "Rotate 90", "S_R", "Rotate the selected Element by 90 degrees", ui_context)
|
||||
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
|
||||
add_layout_hotkey_operator(
|
||||
row, "Flip", "S_F", "Flip Element about its local axes, keep the position", ui_context
|
||||
)
|
||||
|
||||
elif AuthoringData.data["active_material_usage"] == "LAYER3":
|
||||
if "LAYER2" in AuthoringData.data["selected_material_usages"]:
|
||||
@@ -711,8 +721,6 @@ class EditObjectUI:
|
||||
elif AuthoringData.data["active_material_usage"] == "PROFILE":
|
||||
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
|
||||
add_layout_hotkey_operator(row, "Extend", "S_E", "", ui_context)
|
||||
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
|
||||
add_layout_hotkey_operator(row, "Flip", "S_F", bpy.ops.bim.flip_object.__doc__, ui_context)
|
||||
|
||||
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
|
||||
if AuthoringData.data["active_class"] in (
|
||||
@@ -758,6 +766,9 @@ class EditObjectUI:
|
||||
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
|
||||
add_layout_hotkey_operator(row, "Rotate 90", "S_R", bpy.ops.bim.rotate_90.__doc__, ui_context)
|
||||
|
||||
if AuthoringData.data["is_flippable_element"]:
|
||||
cls.draw_flip(ui_context, row)
|
||||
|
||||
if PortData.data["total_ports"] > 0:
|
||||
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
|
||||
row.operator(
|
||||
@@ -774,24 +785,11 @@ class EditObjectUI:
|
||||
def draw_regen_operations(cls, row):
|
||||
custom_icon = custom_icon_previews.get("REGEN", custom_icon_previews["IFC"]).icon_id
|
||||
|
||||
if AuthoringData.data["active_material_usage"] == "LAYER2":
|
||||
if AuthoringData.data["is_regenable_element"]:
|
||||
op = row.operator("bim.hotkey", text="", icon_value=custom_icon)
|
||||
description = f"{bpy.ops.bim.recalculate_wall.__doc__}\n\nHotkey: S G"
|
||||
description = "Recalculate Element Geometry\nHotkey: S G"
|
||||
op.hotkey = "S_G"
|
||||
op.description = description.strip()
|
||||
elif AuthoringData.data["active_material_usage"] == "PROFILE":
|
||||
if AuthoringData.data["active_class"] in (
|
||||
"IfcCableCarrierSegment",
|
||||
"IfcCableSegment",
|
||||
"IfcDuctSegment",
|
||||
"IfcPipeSegment",
|
||||
):
|
||||
pass
|
||||
else:
|
||||
op = row.operator("bim.hotkey", text="", icon_value=custom_icon)
|
||||
description = f"{bpy.ops.bim.recalculate_profile.__doc__}\n\nHotkey: S G"
|
||||
op.hotkey = "S_G"
|
||||
op.description = description.strip()
|
||||
|
||||
if PortData.data["total_ports"] > 0:
|
||||
op = row.operator("bim.hotkey", text="", icon_value=custom_icon)
|
||||
@@ -809,8 +807,9 @@ class EditObjectUI:
|
||||
op_icon = custom_icon_previews["APPLY_VOID"].icon_id
|
||||
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
|
||||
row.operator("bim.add_opening", text=op_text, icon_value=op_icon)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_O")
|
||||
if ui_context != "TOOL_HEADER":
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_O")
|
||||
else:
|
||||
op_text = "Add Void" if ui_context != "TOOL_HEADER" else ""
|
||||
op_icon = custom_icon_previews["ADD_VOID"].icon_id
|
||||
@@ -832,12 +831,8 @@ class EditObjectUI:
|
||||
op_text = "" if IS_TOOL_HEADER else "Edit Openings"
|
||||
row.operator("bim.edit_openings", icon="CHECKMARK", text=op_text)
|
||||
row.operator("bim.hide_openings", icon="CANCEL", text="")
|
||||
if len(context.selected_objects) == 2:
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_L")
|
||||
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
|
||||
row.operator("bim.clone_opening", text="Clone Opening")
|
||||
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
|
||||
add_layout_hotkey_operator(row, "Clone Opening", "S_L", "", ui_context, operator="bim.clone_opening")
|
||||
|
||||
@classmethod
|
||||
def draw_align(cls, context):
|
||||
@@ -905,6 +900,11 @@ class EditObjectUI:
|
||||
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
|
||||
add_layout_hotkey_operator(row, "Decomposition", "A_D", "Select decomposition", ui_context)
|
||||
|
||||
@classmethod
|
||||
def draw_flip(cls, ui_context, layout) -> None:
|
||||
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else layout
|
||||
add_layout_hotkey_operator(row, "Flip", "S_F", bpy.ops.bim.flip_object.__doc__, ui_context)
|
||||
|
||||
|
||||
class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.hotkey"
|
||||
@@ -960,47 +960,43 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
row.prop(self, "z")
|
||||
|
||||
def hotkey_S_A(self):
|
||||
props = bpy.context.scene.BIMModelProperties
|
||||
relating_type_id = AuthoringData.data["relating_type_id_current"]
|
||||
if relating_type_id is None:
|
||||
self.report({"ERROR"}, "No relating type selected")
|
||||
return
|
||||
props.relating_type_id = relating_type_id
|
||||
if bpy.context.scene.BIMGeometryProperties.mode == "ITEM":
|
||||
bpy.ops.wm.call_menu(name="BIM_MT_add_representation_item")
|
||||
else:
|
||||
# Slab from walls
|
||||
walls = False
|
||||
for obj in bpy.context.selected_objects:
|
||||
walls = tool.Ifc.get_entity(obj).is_a("IfcWall")
|
||||
if (
|
||||
walls
|
||||
and relating_type_id
|
||||
and tool.Model.get_usage_type(tool.Ifc.get().by_id(int(relating_type_id))) == "LAYER3"
|
||||
):
|
||||
bpy.ops.bim.draw_slab_from_wall("INVOKE_DEFAULT")
|
||||
return {"FINISHED"}
|
||||
# Walls from slab
|
||||
slab = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
if (
|
||||
slab
|
||||
and slab.is_a("IfcSlab")
|
||||
and relating_type_id
|
||||
and tool.Model.get_usage_type(tool.Ifc.get().by_id(int(relating_type_id))) == "LAYER2"
|
||||
):
|
||||
bpy.ops.bim.draw_walls_from_slab("INVOKE_DEFAULT")
|
||||
return {"FINISHED"}
|
||||
return
|
||||
|
||||
for obj in tool.Blender.get_selected_objects():
|
||||
obj.select_set(False)
|
||||
if relating_type_id and tool.Model.get_usage_type(tool.Ifc.get().by_id(int(relating_type_id))) == "LAYER2":
|
||||
bpy.ops.bim.draw_polyline_wall("INVOKE_DEFAULT")
|
||||
elif (
|
||||
relating_type_id and tool.Model.get_usage_type(tool.Ifc.get().by_id(int(relating_type_id))) == "LAYER3"
|
||||
):
|
||||
bpy.ops.bim.draw_polyline_slab("INVOKE_DEFAULT")
|
||||
else:
|
||||
bpy.ops.bim.add_occurrence("INVOKE_DEFAULT")
|
||||
props = bpy.context.scene.BIMModelProperties
|
||||
relating_type_class = AuthoringData.data["ifc_class_current"]
|
||||
if not (relating_type_id := tool.Blender.get_enum_safe(props, "relating_type_id")):
|
||||
self.report({"ERROR"}, "No relating type selected")
|
||||
return
|
||||
|
||||
relating_type = tool.Ifc.get().by_id(int(relating_type_id))
|
||||
|
||||
has_only_walls_selected = tool.Blender.get_selected_objects(include_active=False) and all(
|
||||
(e := tool.Ifc.get_entity(o)) and e.is_a("IfcWall")
|
||||
for o in tool.Blender.get_selected_objects(include_active=False)
|
||||
)
|
||||
|
||||
if tool.Model.get_usage_type(relating_type) == "LAYER3" and has_only_walls_selected:
|
||||
return bpy.ops.bim.draw_slab_from_wall("INVOKE_DEFAULT")
|
||||
elif (
|
||||
(active_obj := tool.Blender.get_active_object(is_selected=True))
|
||||
and (active_element := tool.Ifc.get_entity(active_obj))
|
||||
and active_element.is_a("IfcSlab")
|
||||
and tool.Model.get_usage_type(relating_type) == "LAYER2"
|
||||
):
|
||||
return bpy.ops.bim.draw_walls_from_slab("INVOKE_DEFAULT")
|
||||
|
||||
for obj in tool.Blender.get_selected_objects():
|
||||
obj.select_set(False)
|
||||
|
||||
if tool.Model.get_usage_type(relating_type) == "LAYER2":
|
||||
return bpy.ops.bim.draw_polyline_wall("INVOKE_DEFAULT")
|
||||
elif tool.Model.get_usage_type(relating_type) == "LAYER3":
|
||||
return bpy.ops.bim.draw_polyline_slab("INVOKE_DEFAULT")
|
||||
elif tool.Model.get_usage_type(relating_type) == "PROFILE" and relating_type_class != "IfcColumnType":
|
||||
return bpy.ops.bim.draw_polyline_profile("INVOKE_DEFAULT")
|
||||
return bpy.ops.bim.add_occurrence("INVOKE_DEFAULT")
|
||||
|
||||
def hotkey_S_Q(self):
|
||||
if not bpy.context.selected_objects:
|
||||
@@ -1028,7 +1024,7 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bpy.ops.bim.align_product(align_type="CENTERLINE")
|
||||
|
||||
def hotkey_S_E(self):
|
||||
if not bpy.context.selected_objects:
|
||||
if not bpy.context.selected_objects or not (active_object := bpy.context.active_object):
|
||||
return
|
||||
|
||||
# NOTE: placing it before the other operations because railing can also be SweptSolid
|
||||
@@ -1048,7 +1044,7 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
elif tool.Model.is_parametric_window_active() or tool.Model.is_parametric_door_active():
|
||||
return
|
||||
|
||||
selected_usages = {}
|
||||
selected_usages: dict[str, list[bpy.types.Object]] = {}
|
||||
for obj in bpy.context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
@@ -1066,6 +1062,10 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
selected_usages.setdefault(usage, []).append(obj)
|
||||
|
||||
if len(bpy.context.selected_objects) == 1:
|
||||
# Active object was probably unselected because it doesn't have a usage.
|
||||
if bpy.context.selected_objects[0] != active_object:
|
||||
return
|
||||
|
||||
if self.active_material_usage == "LAYER3":
|
||||
# Edit LAYER3 profile
|
||||
if bpy.context.active_object and bpy.context.active_object.mode == "OBJECT":
|
||||
@@ -1258,9 +1258,8 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bpy.ops.bim.enable_editing_extrusion_axis()
|
||||
|
||||
def hotkey_A_O(self):
|
||||
opening_collection = bpy.data.collections.get("IfcOpeningElement")
|
||||
if opening_collection and opening_collection.objects:
|
||||
bpy.ops.bim.edit_openings()
|
||||
if bpy.context.scene.BIMModelProperties.openings:
|
||||
bpy.ops.bim.edit_openings(apply_all=True)
|
||||
else:
|
||||
bpy.ops.bim.show_openings()
|
||||
|
||||
|
||||
@@ -26,14 +26,20 @@ classes = (
|
||||
operator.BIM_OT_select_components,
|
||||
operator.BIM_OT_select_nest,
|
||||
operator.BIM_OT_nest_unassign_object,
|
||||
operator.BIM_OT_disable_nest_mode,
|
||||
operator.BIM_OT_toggle_nest_mode_local_view,
|
||||
prop.BIMObjectNestProperties,
|
||||
prop.Objects,
|
||||
prop.BIMNestProperties,
|
||||
ui.BIM_PT_nest,
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
bpy.types.Object.BIMObjectNestProperties = bpy.props.PointerProperty(type=prop.BIMObjectNestProperties)
|
||||
bpy.types.Scene.BIMNestProperties = bpy.props.PointerProperty(type=prop.BIMNestProperties)
|
||||
|
||||
|
||||
def unregister():
|
||||
del bpy.types.Object.BIMObjectNestProperties
|
||||
del bpy.types.Scene.BIMNestProperties
|
||||
|
||||
@@ -0,0 +1,336 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2025 Bruno Perdigão <contact@brunopo.com>
|
||||
#
|
||||
# This file is part of Bonsai.
|
||||
#
|
||||
# Bonsai is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Bonsai is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import blf
|
||||
import bpy
|
||||
import gpu
|
||||
import ifcopenshell
|
||||
import bonsai.tool as tool
|
||||
from bpy.types import SpaceView3D
|
||||
from bpy_extras import view3d_utils
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
from mathutils import Vector
|
||||
from bonsai.bim.module.geometry.decorator import ItemDecorator
|
||||
|
||||
|
||||
def transparent_color(color, alpha=0.1):
|
||||
color = [i for i in color]
|
||||
color[3] = alpha
|
||||
return color
|
||||
|
||||
|
||||
def create_bounding_box(objs):
|
||||
# Initialize the bounding box coordinates
|
||||
min_x, min_y, min_z = float("inf"), float("inf"), float("inf")
|
||||
max_x, max_y, max_z = float("-inf"), float("-inf"), float("-inf")
|
||||
|
||||
# Iterate over the selected objects
|
||||
for obj in objs:
|
||||
if not obj.data:
|
||||
continue
|
||||
# Get the object's bounding box coordinates
|
||||
|
||||
bbox_world = [obj.matrix_world @ Vector(b) for b in obj.bound_box]
|
||||
obj_min = []
|
||||
obj_min.append(min([b[0] for b in bbox_world]))
|
||||
obj_min.append(min([b[1] for b in bbox_world]))
|
||||
obj_min.append(min([b[2] for b in bbox_world]))
|
||||
obj_max = []
|
||||
obj_max.append(max([b[0] for b in bbox_world]))
|
||||
obj_max.append(max([b[1] for b in bbox_world]))
|
||||
obj_max.append(max([b[2] for b in bbox_world]))
|
||||
|
||||
# Update the overall bounding box coordinates
|
||||
min_x = min(min_x, min(obj_min[0], obj_max[0]))
|
||||
min_y = min(min_y, min(obj_min[1], obj_max[1]))
|
||||
min_z = min(min_z, min(obj_min[2], obj_max[2]))
|
||||
max_x = max(max_x, max(obj_min[0], obj_max[0]))
|
||||
max_y = max(max_y, max(obj_min[1], obj_max[1]))
|
||||
max_z = max(max_z, max(obj_min[2], obj_max[2]))
|
||||
|
||||
indices = [
|
||||
(min_x, min_y, min_z),
|
||||
(max_x, min_y, min_z),
|
||||
(max_x, max_y, min_z),
|
||||
(min_x, max_y, min_z),
|
||||
(min_x, min_y, max_z),
|
||||
(max_x, min_y, max_z),
|
||||
(max_x, max_y, max_z),
|
||||
(min_x, max_y, max_z),
|
||||
]
|
||||
|
||||
edges = [(0, 1), (1, 2), (2, 3), (3, 0), (4, 5), (5, 6), (6, 7), (7, 4), (0, 4), (1, 5), (2, 6), (3, 7)]
|
||||
|
||||
return indices, edges
|
||||
|
||||
|
||||
class NestDecorator:
|
||||
is_installed = False
|
||||
handlers = []
|
||||
|
||||
@classmethod
|
||||
def install(cls, context):
|
||||
if cls.is_installed:
|
||||
cls.uninstall()
|
||||
handler = cls()
|
||||
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_nest, (context,), "WINDOW", "POST_VIEW"))
|
||||
cls.is_installed = True
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls):
|
||||
for handler in cls.handlers:
|
||||
try:
|
||||
SpaceView3D.draw_handler_remove(handler, "WINDOW")
|
||||
except ValueError:
|
||||
pass
|
||||
cls.is_installed = False
|
||||
|
||||
def dotted_line_shader(self):
|
||||
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface")
|
||||
vert_out.smooth("FLOAT", "v_ArcLength")
|
||||
|
||||
shader_info = gpu.types.GPUShaderCreateInfo()
|
||||
shader_info.push_constant("MAT4", "u_ViewProjectionMatrix")
|
||||
shader_info.push_constant("FLOAT", "u_Scale")
|
||||
shader_info.vertex_in(0, "VEC3", "position")
|
||||
shader_info.vertex_in(1, "FLOAT", "arcLength")
|
||||
shader_info.vertex_out(vert_out)
|
||||
shader_info.fragment_out(0, "VEC4", "FragColor")
|
||||
shader_info.push_constant("VEC4", "color")
|
||||
|
||||
shader_info.vertex_source(
|
||||
"void main()"
|
||||
"{"
|
||||
" v_ArcLength = arcLength;"
|
||||
" gl_Position = u_ViewProjectionMatrix * vec4(position, 1.0f);"
|
||||
"}"
|
||||
)
|
||||
|
||||
shader_info.fragment_source(
|
||||
"void main()" "{" " if (step(sin(v_ArcLength * u_Scale), 0.4) == 1) discard;" " FragColor = color;" "}"
|
||||
)
|
||||
|
||||
shader = gpu.shader.create_from_info(shader_info)
|
||||
del vert_out
|
||||
del shader_info
|
||||
return shader
|
||||
|
||||
def draw_custom_batch(self, coords, color):
|
||||
shader = self.dotted_line_shader()
|
||||
|
||||
arc_lengths = [0]
|
||||
for a, b in zip(coords[:-1], coords[1:]):
|
||||
arc_lengths.append(arc_lengths[-1] + (a - b).length)
|
||||
|
||||
batch = batch_for_shader(
|
||||
shader,
|
||||
"LINE_STRIP",
|
||||
{"position": coords, "arcLength": arc_lengths},
|
||||
)
|
||||
|
||||
matrix = bpy.context.region_data.perspective_matrix
|
||||
shader.uniform_float("color", color)
|
||||
shader.uniform_float("u_ViewProjectionMatrix", matrix)
|
||||
shader.uniform_float("u_Scale", 25)
|
||||
batch.draw(shader)
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
|
||||
def draw_nest(self, context):
|
||||
if context.scene.BIMNestProperties.in_nest_mode:
|
||||
return
|
||||
self.addon_prefs = tool.Blender.get_addon_preferences()
|
||||
decorator_color_special = self.addon_prefs.decorator_color_special
|
||||
decorator_color_selected = self.addon_prefs.decorator_color_selected
|
||||
decorator_color_error = self.addon_prefs.decorator_color_error
|
||||
decorator_color_unselected = self.addon_prefs.decorator_color_unselected
|
||||
decorator_color_background = self.addon_prefs.decorator_color_background
|
||||
theme = context.preferences.themes.items()[0][1]
|
||||
selected_object_color = (*theme.view_3d.object_active, 1)
|
||||
|
||||
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
self.shader.bind()
|
||||
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
|
||||
self.line_shader.bind()
|
||||
# POLYLINE_UNIFORM_COLOR specific uniforms
|
||||
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
|
||||
nests = []
|
||||
if not (selected_objects := context.selected_objects):
|
||||
return
|
||||
for obj in selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not element.is_a("IfcElement"):
|
||||
return
|
||||
|
||||
components = ifcopenshell.util.element.get_components(element)
|
||||
if components:
|
||||
nests.append(obj)
|
||||
continue
|
||||
|
||||
nest = ifcopenshell.util.element.get_nest(element)
|
||||
if nest:
|
||||
nests.append(tool.Ifc.get_object(nest))
|
||||
|
||||
nests = set(nests)
|
||||
for nest in nests:
|
||||
self.line_shader.uniform_float("lineWidth", 1.0)
|
||||
color = decorator_color_unselected
|
||||
if nest in selected_objects:
|
||||
color = selected_object_color
|
||||
size = nest.empty_display_size
|
||||
location = nest.location
|
||||
if nest.type == "EMPTY":
|
||||
line_x = (location - Vector((size, 0.0, 0.0)), location + Vector((size, 0.0, 0.0)))
|
||||
self.draw_batch("LINES", line_x, color, [(0, 1)])
|
||||
line_y = (location - Vector((0.0, size, 0.0)), location + Vector((0.0, size, 0.0)))
|
||||
self.draw_batch("LINES", line_y, color, [(0, 1)])
|
||||
line_z = (location - Vector((0.0, 0.0, size)), location + Vector((0.0, 0.0, size)))
|
||||
self.draw_batch("LINES", line_z, color, [(0, 1)])
|
||||
else:
|
||||
self.draw_batch("POINTS", [location], color)
|
||||
# if context.scene.BIMNestProperties.in_aggregate_mode:
|
||||
# return
|
||||
components = ifcopenshell.util.element.get_components(tool.Ifc.get_entity(nest))
|
||||
components_objs = [tool.Ifc.get_object(p) for p in components]
|
||||
components_objs.append(nest)
|
||||
|
||||
indices, edges = create_bounding_box(components_objs)
|
||||
self.line_shader.uniform_float("lineWidth", 0.5)
|
||||
self.draw_batch("LINES", indices, color, edges)
|
||||
line = (Vector(indices[0]), location)
|
||||
self.draw_custom_batch(line, decorator_color_unselected)
|
||||
|
||||
|
||||
class NestModeDecorator:
|
||||
is_installed = False
|
||||
handlers = []
|
||||
|
||||
@classmethod
|
||||
def install(cls, context):
|
||||
if cls.is_installed:
|
||||
cls.uninstall()
|
||||
handler = cls()
|
||||
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_nest_name, (context,), "WINDOW", "POST_PIXEL"))
|
||||
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_nest_empty, (context,), "WINDOW", "POST_VIEW"))
|
||||
cls.is_installed = True
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls):
|
||||
for handler in cls.handlers:
|
||||
try:
|
||||
SpaceView3D.draw_handler_remove(handler, "WINDOW")
|
||||
except ValueError:
|
||||
pass
|
||||
cls.is_installed = False
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
|
||||
def draw_nest_name(self, context):
|
||||
if context.mode == "EDIT_MESH":
|
||||
return
|
||||
region = context.region
|
||||
rv3d = region.data
|
||||
props = context.scene.BIMNestProperties
|
||||
|
||||
aggregate_obj = props.editing_nest
|
||||
if not aggregate_obj:
|
||||
return
|
||||
self.addon_prefs = tool.Blender.get_addon_preferences()
|
||||
self.font_id = 0
|
||||
font_size = tool.Blender.scale_font_size(12)
|
||||
blf.size(self.font_id, font_size)
|
||||
blf.enable(self.font_id, blf.SHADOW)
|
||||
blf.shadow(self.font_id, 6, 0, 0, 0, 1)
|
||||
color = self.addon_prefs.decorator_color_selected
|
||||
if aggregate_obj in context.selected_objects:
|
||||
color = self.addon_prefs.decorator_color_selected
|
||||
blf.color(self.font_id, *color)
|
||||
text = aggregate_obj.name
|
||||
text_coords = view3d_utils.location_3d_to_region_2d(region, rv3d, aggregate_obj.location)
|
||||
text_length = blf.dimensions(self.font_id, text)
|
||||
text_coords[0] -= text_length[0] / 2
|
||||
text_coords[1] -= 20
|
||||
blf.position(self.font_id, text_coords[0], text_coords[1], 0)
|
||||
|
||||
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
|
||||
blf.draw(self.font_id, text)
|
||||
|
||||
def draw_nest_empty(self, context):
|
||||
if context.mode == "EDIT_MESH":
|
||||
return
|
||||
props = context.scene.BIMNestProperties
|
||||
nest_obj = props.editing_nest
|
||||
if not nest_obj:
|
||||
return
|
||||
location = nest_obj.location
|
||||
|
||||
self.addon_prefs = tool.Blender.get_addon_preferences()
|
||||
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
|
||||
self.line_shader.bind()
|
||||
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
|
||||
self.line_shader.uniform_float("lineWidth", 2.0)
|
||||
theme = context.preferences.themes.items()[0][1]
|
||||
selected_object_color = (*theme.view_3d.object_active, 1)
|
||||
color = self.addon_prefs.decorator_color_selected
|
||||
if nest_obj in context.selected_objects:
|
||||
color = selected_object_color
|
||||
|
||||
if nest_obj.type == "EMPTY":
|
||||
size = nest_obj.empty_display_size
|
||||
line_x = (location - Vector((size, 0.0, 0.0)), location + Vector((size, 0.0, 0.0)))
|
||||
self.draw_batch("LINES", line_x, color, [(0, 1)])
|
||||
line_y = (location - Vector((0.0, size, 0.0)), location + Vector((0.0, size, 0.0)))
|
||||
self.draw_batch("LINES", line_y, color, [(0, 1)])
|
||||
line_z = (location - Vector((0.0, 0.0, size)), location + Vector((0.0, 0.0, size)))
|
||||
self.draw_batch("LINES", line_z, color, [(0, 1)])
|
||||
else:
|
||||
self.draw_batch("POINTS", [location], color)
|
||||
components = ifcopenshell.util.element.get_components(tool.Ifc.get_entity(nest_obj))
|
||||
if components:
|
||||
for component in components:
|
||||
component_obj = tool.Ifc.get_object(component)
|
||||
if component.is_a("IfcElementAssembly"):
|
||||
self.line_shader.uniform_float("lineWidth", 1.0)
|
||||
size = component_obj.empty_display_size
|
||||
location = component_obj.location
|
||||
color = self.addon_prefs.decorator_color_unselected
|
||||
if component_obj in context.selected_objects:
|
||||
color = selected_object_color
|
||||
line_x = (location - Vector((size, 0.0, 0.0)), location + Vector((size, 0.0, 0.0)))
|
||||
self.draw_batch("LINES", line_x, color, [(0, 1)])
|
||||
line_y = (location - Vector((0.0, size, 0.0)), location + Vector((0.0, size, 0.0)))
|
||||
self.draw_batch("LINES", line_y, color, [(0, 1)])
|
||||
line_z = (location - Vector((0.0, 0.0, size)), location + Vector((0.0, 0.0, size)))
|
||||
self.draw_batch("LINES", line_z, color, [(0, 1)])
|
||||
components = ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(nest_obj))
|
||||
|
||||
color = self.addon_prefs.decorator_color_selected
|
||||
components_objs = [tool.Ifc.get_object(p) for p in components]
|
||||
components_objs.append(nest_obj)
|
||||
indices, edges = create_bounding_box(components_objs)
|
||||
self.line_shader.uniform_float("lineWidth", 0.5)
|
||||
self.draw_batch("LINES", indices, color, edges)
|
||||
@@ -140,3 +140,33 @@ class BIM_OT_select_nest(bpy.types.Operator):
|
||||
nest_obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = nest_obj
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class BIM_OT_disable_nest_mode(bpy.types.Operator):
|
||||
bl_idname = "bim.disable_nest_mode"
|
||||
bl_label = "Disable Nest Mode"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
core.disable_nest_mode(tool.Nest)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class BIM_OT_toggle_nest_mode_local_view(bpy.types.Operator):
|
||||
bl_idname = "bim.toggle_nest_mode_local_view"
|
||||
bl_label = "Toggle Nest Mode Local View"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.BIMNestProperties
|
||||
objs = [o.obj for o in props.editing_objects]
|
||||
if props.in_nest_mode:
|
||||
if context.space_data.local_view:
|
||||
bpy.ops.view3d.localview()
|
||||
else:
|
||||
for obj in objs:
|
||||
obj.select_set(True)
|
||||
bpy.ops.view3d.localview()
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
# 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 bonsai.bim.module.spatial.data import SpatialData
|
||||
from bpy.types import PropertyGroup
|
||||
@@ -30,6 +31,7 @@ from bpy.props import (
|
||||
FloatVectorProperty,
|
||||
CollectionProperty,
|
||||
)
|
||||
from bonsai.bim.module.nest.decorator import NestDecorator, NestModeDecorator
|
||||
|
||||
|
||||
def update_relating_object(self, context):
|
||||
@@ -46,3 +48,34 @@ def update_relating_object(self, context):
|
||||
class BIMObjectNestProperties(PropertyGroup):
|
||||
is_editing: BoolProperty(name="Is Editing")
|
||||
relating_object: PointerProperty(name="Nest Host", type=bpy.types.Object, update=update_relating_object)
|
||||
|
||||
|
||||
def update_nest_decorator(self, context):
|
||||
if self.nest_decorator:
|
||||
NestDecorator.install(bpy.context)
|
||||
else:
|
||||
NestDecorator.uninstall()
|
||||
|
||||
|
||||
def update_nest_mode_decorator(self, context):
|
||||
if self.in_nest_mode:
|
||||
NestModeDecorator.install(bpy.context)
|
||||
else:
|
||||
NestModeDecorator.uninstall()
|
||||
|
||||
|
||||
class Objects(bpy.types.PropertyGroup):
|
||||
obj: PointerProperty(type=bpy.types.Object)
|
||||
previous_display_type: bpy.props.StringProperty(default="TEXTURED")
|
||||
|
||||
|
||||
class BIMNestProperties(PropertyGroup):
|
||||
in_nest_mode: BoolProperty(name="In Edit Mode", update=update_nest_mode_decorator)
|
||||
editing_nest: PointerProperty(name="Editing nest", type=bpy.types.Object)
|
||||
editing_objects: CollectionProperty(type=Objects)
|
||||
not_editing_objects: CollectionProperty(type=Objects)
|
||||
nest_decorator: BoolProperty(
|
||||
name="Display Nest",
|
||||
default=False,
|
||||
update=update_nest_decorator,
|
||||
)
|
||||
|
||||
@@ -93,17 +93,11 @@ class ExecuteIfcPatch(bpy.types.Operator):
|
||||
args = ifcpatch.ArgumentsDict(
|
||||
recipe=props.ifc_patch_recipes,
|
||||
arguments=arguments,
|
||||
log=os.path.join(context.scene.BIMProperties.data_dir, "process.log"),
|
||||
log=tool.Blender.get_data_dir_path("process.log").__str__(),
|
||||
)
|
||||
|
||||
if props.should_load_from_memory and tool.Ifc.get():
|
||||
args["file"] = tool.Ifc.get()
|
||||
if ifcpatch.get_patch_input_argument_use(recipe_name) == "REQUIRED":
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
f"The recipe '{recipe_name}' is not currently supported if file is loaded from memory.",
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
else:
|
||||
args["input"] = cast(str, props.ifc_patch_input)
|
||||
args["file"] = cast(ifcopenshell.file, ifcopenshell.open(props.ifc_patch_input))
|
||||
|
||||
@@ -69,11 +69,8 @@ class ProjectData:
|
||||
if not ifc_file:
|
||||
return []
|
||||
current_schema = tool.Ifc.get().schema_identifier
|
||||
files = (Path(bpy.context.scene.BIMProperties.data_dir) / "libraries").iterdir()
|
||||
results = [("0", "Custom Library", "")]
|
||||
for f in files:
|
||||
if not f.suffix.lower().startswith(".ifc"):
|
||||
continue
|
||||
for f in tool.Blender.get_data_dir_paths("libraries", "*.ifc*"):
|
||||
if cls.get_file_schema(f) != current_schema:
|
||||
continue
|
||||
results.append((f.name, f.stem, "Library"))
|
||||
@@ -85,10 +82,7 @@ class ProjectData:
|
||||
for export_schema in cls.data["export_schema"]:
|
||||
template_files[export_schema[0]].append(("0", "Blank Project", ""))
|
||||
|
||||
files = (Path(bpy.context.scene.BIMProperties.data_dir) / "templates" / "projects").iterdir()
|
||||
for f in files:
|
||||
if not f.suffix.lower().startswith(".ifc"):
|
||||
continue
|
||||
for f in tool.Blender.get_data_dir_paths(Path("templates") / "projects", "*.ifc*"):
|
||||
current_schema = cls.get_file_schema(f)
|
||||
if current_schema not in template_files:
|
||||
continue
|
||||
|
||||
@@ -856,8 +856,8 @@ class LoadProjectElements(bpy.types.Operator):
|
||||
bonsai.bim.schema.reload(self.file.schema_identifier)
|
||||
start = time.time()
|
||||
logger = logging.getLogger("ImportIFC")
|
||||
path_log = os.path.join(context.scene.BIMProperties.data_dir, "process.log")
|
||||
if not os.access(context.scene.BIMProperties.data_dir, os.W_OK):
|
||||
path_log = tool.Blender.get_data_dir_path("process.log")
|
||||
if not os.access(path_log.parent, os.W_OK):
|
||||
path_log = os.path.join(tempfile.mkdtemp(), "process.log")
|
||||
logging.basicConfig(
|
||||
filename=path_log,
|
||||
@@ -889,6 +889,7 @@ class LoadProjectElements(bpy.types.Operator):
|
||||
tool.Project.set_default_context()
|
||||
tool.Project.set_default_modeling_dimensions()
|
||||
tool.Root.reload_grid_decorator()
|
||||
bonsai.bim.handler.refresh_ui_data()
|
||||
return {"FINISHED"}
|
||||
|
||||
def get_decomposition_elements(self):
|
||||
@@ -1373,8 +1374,8 @@ class ExportIFC(bpy.types.Operator):
|
||||
def _execute(self, context):
|
||||
start = time.time()
|
||||
logger = logging.getLogger("ExportIFC")
|
||||
path_log = os.path.join(context.scene.BIMProperties.data_dir, "process.log")
|
||||
if not os.access(context.scene.BIMProperties.data_dir, os.W_OK):
|
||||
path_log = tool.Blender.get_data_dir_path("process.log")
|
||||
if not os.access(path_log.parent, os.W_OK):
|
||||
path_log = os.path.join(tempfile.mkdtemp(), "process.log")
|
||||
logging.basicConfig(
|
||||
filename=path_log,
|
||||
@@ -1419,9 +1420,6 @@ class ExportIFC(bpy.types.Operator):
|
||||
f'IFC Project "{os.path.basename(output_file)}" {"" if not save_blend_file else "And Current Blend File Are"} Saved',
|
||||
)
|
||||
|
||||
if bpy.data.is_saved:
|
||||
bpy.ops.wm.save_mainfile("INVOKE_DEFAULT")
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
if properties.should_save_as:
|
||||
|
||||
@@ -20,6 +20,7 @@ import os
|
||||
import bpy
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.placement
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.module.project.data import ProjectData
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
from bonsai.bim.prop import StrProperty, ObjProperty
|
||||
@@ -60,9 +61,8 @@ def get_library_file(self: "BIMProjectProperties", context: bpy.types.Context) -
|
||||
|
||||
def update_library_file(self: "BIMProjectProperties", context: bpy.types.Context) -> None:
|
||||
if self.library_file != "0":
|
||||
bpy.ops.bim.select_library_file(
|
||||
filepath=os.path.join(bpy.context.scene.BIMProperties.data_dir, "libraries", self.library_file)
|
||||
)
|
||||
filepath = next(p for p in tool.Blender.get_data_dir_paths("libraries", "*.ifc") if p.name == self.library_file)
|
||||
bpy.ops.bim.select_library_file(filepath=filepath)
|
||||
|
||||
|
||||
def update_filter_mode(self: "BIMProjectProperties", context: bpy.types.Context) -> None:
|
||||
|
||||
@@ -537,6 +537,7 @@ class BIM_PT_purge(Panel):
|
||||
layout = self.layout
|
||||
layout.operator("bim.purge_unused_objects", text="Purge Unused Profiles").object_type = "PROFILE"
|
||||
layout.operator("bim.purge_unused_objects", text="Purge Unused Types").object_type = "TYPE"
|
||||
layout.operator("bim.purge_unused_openings", text="Purge Unused Openings in Selected Objects")
|
||||
row = layout.row(align=True)
|
||||
row.label(text="Materials: ")
|
||||
row.operator("bim.purge_unused_objects", text="Purge Unused").object_type = "MATERIAL"
|
||||
|
||||
@@ -47,7 +47,6 @@ classes = (
|
||||
ui.BIM_PT_object_psets,
|
||||
ui.BIM_PT_object_qtos,
|
||||
ui.BIM_PT_material_psets,
|
||||
ui.BIM_PT_material_set_psets,
|
||||
ui.BIM_PT_material_set_item_psets,
|
||||
ui.BIM_PT_task_qtos,
|
||||
ui.BIM_PT_resource_qtos,
|
||||
@@ -66,7 +65,6 @@ classes = (
|
||||
def register():
|
||||
bpy.types.Object.PsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
|
||||
bpy.types.Scene.MaterialPsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
|
||||
bpy.types.Object.MaterialSetPsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
|
||||
bpy.types.Object.MaterialSetItemPsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
|
||||
bpy.types.Scene.TaskPsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
|
||||
bpy.types.Scene.ResourcePsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
|
||||
@@ -82,7 +80,6 @@ def register():
|
||||
def unregister():
|
||||
del bpy.types.Object.PsetProperties
|
||||
del bpy.types.Scene.MaterialPsetProperties
|
||||
del bpy.types.Object.MaterialSetPsetProperties
|
||||
del bpy.types.Object.MaterialSetItemPsetProperties
|
||||
del bpy.types.Scene.TaskPsetProperties
|
||||
del bpy.types.Scene.ResourcePsetProperties
|
||||
|
||||
@@ -33,7 +33,6 @@ def refresh():
|
||||
ObjectPsetsData.is_loaded = False
|
||||
ObjectQtosData.is_loaded = False
|
||||
MaterialPsetsData.is_loaded = False
|
||||
MaterialSetPsetsData.is_loaded = False
|
||||
MaterialSetItemPsetsData.is_loaded = False
|
||||
TaskQtosData.is_loaded = False
|
||||
ResourceQtosData.is_loaded = False
|
||||
@@ -185,7 +184,7 @@ class MaterialPsetsData(Data):
|
||||
if material.ifc_definition_id:
|
||||
material = tool.Ifc.get().by_id(material.ifc_definition_id)
|
||||
category = getattr(material, "Category", None) or None
|
||||
psets = bonsai.bim.schema.ifc.psetqto.get_applicable("IfcMaterial", category, pset_only=True)
|
||||
psets = bonsai.bim.schema.ifc.psetqto.get_applicable(props.material_type, category, pset_only=True)
|
||||
psetnames = cls.format_pset_enum(psets)
|
||||
assigned_names = ifcopenshell.util.element.get_psets(
|
||||
material, psets_only=True, should_inherit=False
|
||||
@@ -194,22 +193,6 @@ class MaterialPsetsData(Data):
|
||||
return []
|
||||
|
||||
|
||||
class MaterialSetPsetsData(Data):
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
psets = {}
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
if element:
|
||||
material = ifcopenshell.util.element.get_material(element, should_skip_usage=True)
|
||||
if material and "Set" in material.is_a():
|
||||
psets = cls.psetqtos(material)
|
||||
cls.data = {"psets": psets}
|
||||
cls.is_loaded = True
|
||||
|
||||
|
||||
class MaterialSetItemPsetsData(Data):
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@@ -20,12 +20,11 @@ import bpy
|
||||
import bonsai.tool as tool
|
||||
from bpy.types import Panel
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
from bonsai.bim.helper import prop_with_search
|
||||
from bonsai.bim.helper import prop_with_search, get_display_value
|
||||
from bonsai.bim.module.pset.data import (
|
||||
ObjectPsetsData,
|
||||
ObjectQtosData,
|
||||
MaterialPsetsData,
|
||||
MaterialSetPsetsData,
|
||||
MaterialSetItemPsetsData,
|
||||
TaskQtosData,
|
||||
ResourceQtosData,
|
||||
@@ -88,7 +87,7 @@ def draw_enumerated_property(
|
||||
|
||||
|
||||
def get_active_pset_obj_name(context: bpy.types.Context, obj_type: tool.Ifc.OBJECT_TYPE) -> str:
|
||||
if obj_type in ("Object", "MaterialSet", "MaterialSetItem"):
|
||||
if obj_type in ("Object", "MaterialSetItem"):
|
||||
return context.active_object.name
|
||||
return ""
|
||||
|
||||
@@ -197,7 +196,9 @@ def draw_psetqto_ui(
|
||||
row = box.row(align=True)
|
||||
row.scale_y = 0.8
|
||||
row.label(text=prop["Name"])
|
||||
op = row.operator("bim.select_similar", text=nominal_value, icon="NONE", emboss=False)
|
||||
op = row.operator(
|
||||
"bim.select_similar", text=get_display_value(nominal_value), icon="NONE", emboss=False
|
||||
)
|
||||
op.key = '"' + pset["Name"].replace('"', '\\"') + '"."' + prop["Name"].replace('"', '\\"') + '"'
|
||||
# calculate sum of all selected objects
|
||||
if active_operator:
|
||||
@@ -400,10 +401,8 @@ class BIM_PT_material_psets(Panel):
|
||||
if not ifc_file or ifc_file.schema == "IFC2X3":
|
||||
return False # We don't support material psets in IFC2X3 because they suck
|
||||
props = context.scene.BIMMaterialProperties
|
||||
if props.materials and props.active_material_index < len(props.materials):
|
||||
material = props.materials[props.active_material_index]
|
||||
if material.ifc_definition_id:
|
||||
return True
|
||||
if props.is_editing and (material := props.active_material) and material.ifc_definition_id:
|
||||
return True
|
||||
return False
|
||||
|
||||
def draw(self, context):
|
||||
@@ -430,46 +429,6 @@ class BIM_PT_material_psets(Panel):
|
||||
draw_psetqto_ui(context, pset["id"], pset, props, self.layout, "Material")
|
||||
|
||||
|
||||
class BIM_PT_material_set_psets(Panel):
|
||||
bl_label = "Material Set Property Sets"
|
||||
bl_idname = "BIM_PT_material_set_psets"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "object"
|
||||
bl_parent_id = "BIM_PT_object_material"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not context.active_object:
|
||||
return False
|
||||
if not tool.Ifc.get() or tool.Ifc.get().schema == "IFC2X3":
|
||||
return False # We don't support material psets in IFC2X3 because they suck
|
||||
if not tool.Ifc.get_entity(context.active_object):
|
||||
return False
|
||||
if not ObjectMaterialData.is_loaded:
|
||||
ObjectMaterialData.load()
|
||||
ifc_class = ObjectMaterialData.data["material_class"]
|
||||
return bool(ifc_class and "Set" in ifc_class)
|
||||
|
||||
def draw(self, context):
|
||||
if not MaterialSetPsetsData.is_loaded:
|
||||
MaterialSetPsetsData.load()
|
||||
|
||||
props = context.active_object.MaterialSetPsetProperties
|
||||
row = self.layout.row(align=True)
|
||||
prop_with_search(row, props, "pset_name", text="")
|
||||
op = row.operator("bim.add_pset", icon="ADD", text="")
|
||||
op.obj = context.active_object.name
|
||||
op.obj_type = "MaterialSet"
|
||||
|
||||
if not props.active_pset_id and props.active_pset_name and props.active_pset_type == "PSET":
|
||||
draw_psetqto_ui(context, 0, {}, props, self.layout, "MaterialSet")
|
||||
|
||||
for pset in MaterialSetPsetsData.data["psets"]:
|
||||
draw_psetqto_ui(context, pset["id"], pset, props, self.layout, "MaterialSet")
|
||||
|
||||
|
||||
class BIM_PT_material_set_item_psets(Panel):
|
||||
bl_label = "Material Set Item Property Sets"
|
||||
bl_idname = "BIM_PT_material_set_item_psets"
|
||||
|
||||
@@ -109,19 +109,15 @@ class PsetTemplatesData:
|
||||
return [(i, i, "") for i in enum_items]
|
||||
|
||||
@classmethod
|
||||
def pset_template_files(cls):
|
||||
results = []
|
||||
pset_dir = os.path.join(bpy.context.scene.BIMProperties.data_dir, "pset")
|
||||
files = os.listdir(pset_dir)
|
||||
for f in files:
|
||||
results.append(
|
||||
(os.path.join(pset_dir, f), os.path.splitext(os.path.basename(f))[0], "Global Pset Template")
|
||||
)
|
||||
def pset_template_files(cls) -> list[tuple[str, str, str]]:
|
||||
results: list[tuple[str, str, str]] = []
|
||||
for f in tool.Blender.get_data_dir_paths("pset", "*.ifc"):
|
||||
results.append((f.__str__(), f.stem, "Global Pset Template"))
|
||||
|
||||
pset_dir = tool.Ifc.resolve_uri(bpy.context.scene.BIMProperties.pset_dir)
|
||||
if os.path.isdir(pset_dir):
|
||||
for path in pathlib.Path(pset_dir).glob("*.ifc"):
|
||||
results.append((str(path), os.path.splitext(os.path.basename(str(path)))[0], "Project Pset Template"))
|
||||
results.append((str(path), path.stem, "Project Pset Template"))
|
||||
|
||||
return sorted(results, key=lambda x: x[1])
|
||||
|
||||
|
||||
@@ -42,7 +42,9 @@ class AddPsetTemplateFile(bpy.types.Operator):
|
||||
|
||||
def execute(self, context):
|
||||
template = ifcopenshell.file()
|
||||
filepath = os.path.join(context.scene.BIMProperties.data_dir, "pset", self.new_template_filename + ".ifc")
|
||||
filepath = next(
|
||||
p for p in tool.Blender.get_data_dir_paths("pset", "*.ifc") if p.stem == self.new_template_filename
|
||||
).__str__()
|
||||
|
||||
pset_template = ifcopenshell.api.run("pset_template.add_pset_template", template)
|
||||
ifcopenshell.api.run("pset_template.add_prop_template", template, pset_template=pset_template)
|
||||
|
||||
@@ -220,7 +220,7 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if tool.Geometry.mesh_has_loose_geometry(obj.data):
|
||||
self.report(
|
||||
{"WARNING"},
|
||||
f"Mesh '{obj.data.name}' has loose geometry, loose geometry was be ignored to save mesh to IFC as a tessellation.",
|
||||
f"Mesh '{obj.data.name}' has loose geometry, loose geometry will be ignored to save mesh to IFC as a tessellation.",
|
||||
)
|
||||
|
||||
element = core.assign_class(
|
||||
@@ -341,22 +341,30 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_label = "Add Element"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Add an IFC physical product, construction type, and more"
|
||||
is_specific_tool: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
|
||||
ifc_product: bpy.props.StringProperty(options={"SKIP_SAVE"})
|
||||
ifc_class: bpy.props.StringProperty(options={"SKIP_SAVE"})
|
||||
|
||||
def invoke(self, context, event):
|
||||
return IfcStore.execute_ifc_operator(self, context, is_invoke=True)
|
||||
|
||||
def _invoke(self, context, event):
|
||||
props = context.scene.BIMRootProperties
|
||||
# For convenience, preselect OBJ representation template if applicable
|
||||
if (obj := context.active_object) and obj.type == "MESH":
|
||||
props = context.scene.BIMRootProperties
|
||||
props.representation_template = "OBJ"
|
||||
props.representation_obj = obj
|
||||
# For convenience, preselect IFC class
|
||||
if self.ifc_product:
|
||||
props.ifc_product = self.ifc_product
|
||||
if self.ifc_class:
|
||||
props.ifc_class = self.ifc_class
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMRootProperties
|
||||
predefined_type = (
|
||||
props.userdefined_type if props.ifc_predefined_type == "USERDEFINED" else props.ifc_predefined_type
|
||||
props.ifc_userdefined_type if props.ifc_predefined_type == "USERDEFINED" else props.ifc_predefined_type
|
||||
)
|
||||
representation_template = props.representation_template
|
||||
ifc_file = tool.Ifc.get()
|
||||
@@ -380,6 +388,7 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
|
||||
mesh = bpy.data.meshes.new("Mesh")
|
||||
|
||||
obj = bpy.data.objects.new(props.ifc_class[3:], mesh)
|
||||
obj.name = props.name or "Unnamed"
|
||||
obj.location = bpy.context.scene.cursor.location
|
||||
element = core.assign_class(
|
||||
tool.Ifc,
|
||||
@@ -390,6 +399,7 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
|
||||
predefined_type=predefined_type,
|
||||
should_add_representation=False,
|
||||
)
|
||||
element.Description = props.description or None
|
||||
|
||||
if representation_template == "EMTPY" or not ifc_context:
|
||||
pass
|
||||
@@ -532,34 +542,41 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
|
||||
"material.assign_profile", tool.Ifc.get(), material_profile=material_profile, profile=profile
|
||||
)
|
||||
elif representation_template == "WINDOW":
|
||||
with context.temp_override(active_object=obj):
|
||||
with context.temp_override(active_object=obj, selected_objects=[]):
|
||||
bpy.ops.bim.add_window()
|
||||
elif representation_template == "DOOR":
|
||||
with context.temp_override(active_object=obj):
|
||||
with context.temp_override(active_object=obj, selected_objects=[]):
|
||||
bpy.ops.bim.add_door()
|
||||
elif representation_template == "STAIR":
|
||||
with context.temp_override(active_object=obj):
|
||||
with context.temp_override(active_object=obj, selected_objects=[]):
|
||||
bpy.ops.bim.add_stair()
|
||||
elif representation_template == "RAILING":
|
||||
with context.temp_override(active_object=obj):
|
||||
with context.temp_override(active_object=obj, selected_objects=[]):
|
||||
bpy.ops.bim.add_railing()
|
||||
elif representation_template == "ROOF":
|
||||
with context.temp_override(active_object=obj):
|
||||
with context.temp_override(active_object=obj, selected_objects=[]):
|
||||
bpy.ops.bim.add_roof()
|
||||
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
||||
tool.Blender.set_active_object(obj)
|
||||
|
||||
def draw(self, context):
|
||||
props = context.scene.BIMRootProperties
|
||||
self.layout.use_property_split = True
|
||||
self.layout.use_property_decorate = False
|
||||
prop_with_search(self.layout, props, "ifc_product", text="Definition")
|
||||
prop_with_search(self.layout, props, "ifc_class", should_click_ok_to_validate=True)
|
||||
row = self.layout.row()
|
||||
row.prop(props, "name")
|
||||
row = self.layout.row()
|
||||
row.prop(props, "description")
|
||||
if not self.is_specific_tool:
|
||||
prop_with_search(self.layout, props, "ifc_product", text="Definition", should_click_ok=True)
|
||||
prop_with_search(self.layout, props, "ifc_class", should_click_ok=True)
|
||||
ifc_predefined_types = root_prop.get_ifc_predefined_types(context.scene.BIMRootProperties, context)
|
||||
if ifc_predefined_types:
|
||||
prop_with_search(self.layout, props, "ifc_predefined_type")
|
||||
prop_with_search(self.layout, props, "ifc_predefined_type", should_click_ok=True)
|
||||
if props.ifc_predefined_type == "USERDEFINED":
|
||||
row = self.layout.row()
|
||||
row.prop(props, "ifc_userdefined_type")
|
||||
prop_with_search(self.layout, props, "representation_template", text="Representation")
|
||||
prop_with_search(self.layout, props, "representation_template", text="Representation", should_click_ok=True)
|
||||
if props.representation_template == "OBJ":
|
||||
row = self.layout.row()
|
||||
row.prop(props, "representation_obj", text="Object")
|
||||
@@ -567,7 +584,7 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
|
||||
row = self.layout.row()
|
||||
row.prop(props, "profile", text="Profile")
|
||||
if props.representation_template != "EMPTY":
|
||||
prop_with_search(self.layout, props, "contexts")
|
||||
prop_with_search(self.layout, props, "contexts", should_click_ok=True)
|
||||
|
||||
|
||||
class LaunchAddElement(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
@@ -127,6 +127,8 @@ def poll_representation_obj(self, obj):
|
||||
|
||||
class BIMRootProperties(PropertyGroup):
|
||||
contexts: EnumProperty(items=get_contexts, name="Contexts", options=set())
|
||||
name: StringProperty(name="Name")
|
||||
description: StringProperty(name="Description")
|
||||
ifc_product: EnumProperty(items=get_ifc_products, name="Products", update=refresh_classes)
|
||||
ifc_class: EnumProperty(items=get_ifc_classes, name="Class", update=refresh_predefined_types)
|
||||
ifc_predefined_type: EnumProperty(items=get_ifc_predefined_types, name="Predefined Type", default=None)
|
||||
|
||||
@@ -38,8 +38,6 @@ classes = (
|
||||
operator.AssignSuccessor,
|
||||
operator.AssignWorkSchedule,
|
||||
operator.Bonsai_DatePicker,
|
||||
operator.Bonsai_DatePickerSetDate,
|
||||
operator.Bonsai_RedrawDatePicker,
|
||||
operator.CalculateTaskDuration,
|
||||
operator.ClearPreviousAnimation,
|
||||
operator.ContractAllTasks,
|
||||
|
||||
@@ -46,7 +46,7 @@ class SequenceData:
|
||||
"has_work_calendars": cls.has_work_calendars(),
|
||||
"schedule_predefined_types_enum": cls.schedule_predefined_types_enum(),
|
||||
"task_columns_enum": cls.task_columns_enum(),
|
||||
"tasktimecolumns_enum": cls.tasktimecolumns_enum(),
|
||||
"task_time_columns_enum": cls.task_time_columns_enum(),
|
||||
}
|
||||
cls.load_work_plans()
|
||||
cls.load_work_schedules()
|
||||
@@ -278,7 +278,7 @@ class SequenceData:
|
||||
return taskcolumns_enum
|
||||
|
||||
@classmethod
|
||||
def tasktimecolumns_enum(cls) -> list[tuple[str, str, str]]:
|
||||
def task_time_columns_enum(cls) -> list[tuple[str, str, str]]:
|
||||
schema = tool.Ifc.schema()
|
||||
tasktimecolumns_enum = []
|
||||
for a in schema.declaration_by_name("IfcTaskTime").all_attributes():
|
||||
|
||||
@@ -38,6 +38,7 @@ def parse_duration(value):
|
||||
|
||||
|
||||
def canonicalise_time(time: Union[datetime, None]) -> str:
|
||||
"""Actualy canonicalises datetime as just a date, time is not included."""
|
||||
if not time:
|
||||
return "-"
|
||||
return time.strftime("%d/%m/%y")
|
||||
|
||||
@@ -1178,44 +1178,72 @@ class Bonsai_DatePicker(bpy.types.Operator):
|
||||
bl_label = "Date Picker"
|
||||
bl_idname = "bim.datepicker"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
display_date: bpy.props.StringProperty(name="Display Date")
|
||||
target_prop: bpy.props.StringProperty(name="Target date prop to set")
|
||||
# TODO: base it on property type.
|
||||
include_time: bpy.props.BoolProperty(name="Include Time", default=True)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
target_prop: str
|
||||
include_time: bool
|
||||
|
||||
def execute(self, context):
|
||||
selected_date = context.scene.DatePickerProperties.selected_date
|
||||
try:
|
||||
value = parser.parse(context.scene.DatePickerProperties.selected_date, dayfirst=True, fuzzy=True)
|
||||
self.set_scene_prop(self.target_prop, helper.canonicalise_time(value))
|
||||
except:
|
||||
pass
|
||||
return {"FINISHED"}
|
||||
# Just to make sure the date is valid.
|
||||
tool.Sequence.parse_isodate_datetime(selected_date, self.include_time)
|
||||
self.set_scene_prop(self.target_prop, selected_date)
|
||||
return {"FINISHED"}
|
||||
except Exception as e:
|
||||
self.report({"ERROR"}, f"Provided date is invalid: '{selected_date}'. Exception: {str(e)}.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
def draw(self, context):
|
||||
current_date = parser.parse(context.scene.DatePickerProperties.display_date, dayfirst=True, fuzzy=True)
|
||||
current_month = (current_date.year, current_date.month)
|
||||
props = context.scene.DatePickerProperties
|
||||
display_date = tool.Sequence.parse_isodate_datetime(props.display_date, False)
|
||||
current_month = (display_date.year, display_date.month)
|
||||
lines = calendar.monthcalendar(*current_month)
|
||||
month_title, week_titles = calendar.month(*current_month).splitlines()[:2]
|
||||
|
||||
layout = self.layout
|
||||
row = layout.row()
|
||||
row.prop(context.scene.DatePickerProperties, "selected_date")
|
||||
row.prop(props, "selected_date", text="Date")
|
||||
|
||||
# Time.
|
||||
if self.include_time:
|
||||
row = layout.row()
|
||||
row.label(text="Time:")
|
||||
row.prop(props, "selected_hour", text="H")
|
||||
row.prop(props, "selected_min", text="M")
|
||||
row.prop(props, "selected_sec", text="S")
|
||||
|
||||
# Month.
|
||||
month_delta = relativedelta.relativedelta(months=1)
|
||||
split = layout.split()
|
||||
col = split.row()
|
||||
op = col.operator("bim.redraw_datepicker", icon="TRIA_LEFT", text="")
|
||||
op.action = "previous"
|
||||
op = col.operator("wm.context_set_string", icon="TRIA_LEFT", text="")
|
||||
op.data_path = "scene.DatePickerProperties.display_date"
|
||||
op.value = tool.Sequence.isodate_datetime(display_date - month_delta, False)
|
||||
|
||||
col = split.row()
|
||||
col.label(text=month_title.strip())
|
||||
|
||||
col = split.row()
|
||||
col.alignment = "RIGHT"
|
||||
op = col.operator("bim.redraw_datepicker", icon="TRIA_RIGHT", text="")
|
||||
op.action = "next"
|
||||
op = col.operator("wm.context_set_string", icon="TRIA_RIGHT", text="")
|
||||
op.data_path = "scene.DatePickerProperties.display_date"
|
||||
op.value = tool.Sequence.isodate_datetime(display_date + month_delta, False)
|
||||
|
||||
# Day of week.
|
||||
row = layout.row(align=True)
|
||||
for title in week_titles.split():
|
||||
col = row.column(align=True)
|
||||
col.alignment = "CENTER"
|
||||
col.label(text=title.strip())
|
||||
|
||||
# Days calendar.
|
||||
current_selected_date = tool.Sequence.parse_isodate_datetime(props.selected_date, self.include_time)
|
||||
current_selected_date = current_selected_date.replace(hour=0, minute=0, second=0)
|
||||
|
||||
for line in lines:
|
||||
row = layout.row(align=True)
|
||||
for i in line:
|
||||
@@ -1223,15 +1251,33 @@ class Bonsai_DatePicker(bpy.types.Operator):
|
||||
if i == 0:
|
||||
col.label(text=" ")
|
||||
else:
|
||||
op = col.operator("bim.datepicker_setdate", text="{:2d}".format(i))
|
||||
selected_date = "{}/{}/{}".format(i, current_date.month, current_date.year)
|
||||
selected_date = parser.parse(selected_date, dayfirst=True, fuzzy=True)
|
||||
op.selected_date = helper.canonicalise_time(selected_date)
|
||||
selected_date = datetime(year=display_date.year, month=display_date.month, day=i)
|
||||
is_current_date = current_selected_date == selected_date
|
||||
op = col.operator("wm.context_set_string", text="{:2d}".format(i), depress=is_current_date)
|
||||
if self.include_time:
|
||||
selected_date = selected_date.replace(
|
||||
hour=props.selected_hour, minute=props.selected_min, second=props.selected_sec
|
||||
)
|
||||
op.data_path = "scene.DatePickerProperties.selected_date"
|
||||
op.value = tool.Sequence.isodate_datetime(selected_date, self.include_time)
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.display_date = self.get_scene_prop(self.target_prop) or helper.canonicalise_time(datetime.now())
|
||||
context.scene.DatePickerProperties.display_date = self.display_date
|
||||
context.scene.DatePickerProperties.selected_date = self.display_date
|
||||
props = context.scene.DatePickerProperties
|
||||
current_date_str = self.get_scene_prop(self.target_prop)
|
||||
if current_date_str:
|
||||
current_date = tool.Sequence.parse_isodate_datetime(current_date_str, self.include_time)
|
||||
else:
|
||||
current_date = datetime.now()
|
||||
# Seconds of the moment when datepicker opened will probably only annoy users.
|
||||
current_date = current_date.replace(second=0)
|
||||
|
||||
if self.include_time:
|
||||
props["selected_hour"] = current_date.hour
|
||||
props["selected_min"] = current_date.minute
|
||||
props["selected_sec"] = current_date.second
|
||||
|
||||
props.display_date = tool.Sequence.isodate_datetime(current_date.replace(day=1), False)
|
||||
props.selected_date = tool.Sequence.isodate_datetime(current_date, self.include_time)
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def get_scene_prop(self, prop_path: str) -> str:
|
||||
@@ -1243,36 +1289,6 @@ class Bonsai_DatePicker(bpy.types.Operator):
|
||||
tool.Blender.set_prop_from_path(scene, prop_path, value)
|
||||
|
||||
|
||||
class Bonsai_DatePickerSetDate(bpy.types.Operator):
|
||||
bl_label = "Set Date"
|
||||
bl_idname = "bim.datepicker_setdate"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
selected_date: bpy.props.StringProperty()
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.scene.DatePickerProperties.selected_date = self.selected_date
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class Bonsai_RedrawDatePicker(bpy.types.Operator):
|
||||
bl_label = "Redraw Datepicker Window"
|
||||
bl_idname = "bim.redraw_datepicker"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
action: bpy.props.StringProperty()
|
||||
|
||||
def invoke(self, context, event):
|
||||
current_date = parser.parse(context.scene.DatePickerProperties.display_date, dayfirst=True, fuzzy=True)
|
||||
|
||||
if self.action == "previous":
|
||||
date_to_set = current_date - relativedelta.relativedelta(months=1)
|
||||
elif self.action == "next":
|
||||
date_to_set = current_date + relativedelta.relativedelta(months=1)
|
||||
|
||||
context.scene.DatePickerProperties.display_date = helper.canonicalise_time(date_to_set)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RecalculateSchedule(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.recalculate_schedule"
|
||||
bl_label = "Recalculate Schedule"
|
||||
|
||||
@@ -45,13 +45,13 @@ from bpy.props import (
|
||||
def getTaskColumns(self, context):
|
||||
if not SequenceData.is_loaded:
|
||||
SequenceData.load()
|
||||
return SequenceData.data["taskcolumns_enum"]
|
||||
return SequenceData.data["task_columns_enum"]
|
||||
|
||||
|
||||
def getTaskTimeColumns(self, context):
|
||||
if not SequenceData.is_loaded:
|
||||
SequenceData.load()
|
||||
return SequenceData.data["tasktimecolumns_enum"]
|
||||
return SequenceData.data["task_time_columns_enum"]
|
||||
|
||||
|
||||
def getWorkSchedules(self, context):
|
||||
@@ -520,9 +520,23 @@ class BIMWorkCalendarProperties(PropertyGroup):
|
||||
end_time: StringProperty(name="End Time")
|
||||
|
||||
|
||||
def update_selected_date(self: "DatePickerProperties", context: bpy.types.Context) -> None:
|
||||
# `include_time` is `True`, otherwise time props are not displayed in UI.
|
||||
include_time = True
|
||||
selected_date = tool.Sequence.parse_isodate_datetime(self.selected_date, include_time)
|
||||
selected_date = selected_date.replace(hour=self.selected_hour, minute=self.selected_min, second=self.selected_sec)
|
||||
self.selected_date = tool.Sequence.isodate_datetime(selected_date, include_time)
|
||||
|
||||
|
||||
class DatePickerProperties(PropertyGroup):
|
||||
display_date: StringProperty(name="Display Date")
|
||||
display_date: StringProperty(
|
||||
name="Display Date",
|
||||
description="Needed to keep track of what month is currently opened in date picker without affecting the currently selected date.",
|
||||
)
|
||||
selected_date: StringProperty(name="Selected Date")
|
||||
selected_hour: IntProperty(min=0, max=23, update=update_selected_date)
|
||||
selected_min: IntProperty(min=0, max=59, update=update_selected_date)
|
||||
selected_sec: IntProperty(min=0, max=59, update=update_selected_date)
|
||||
|
||||
|
||||
class BIMDateTextProperties(PropertyGroup):
|
||||
|
||||
@@ -16,7 +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/>.
|
||||
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
import isodate
|
||||
import bonsai.tool as tool
|
||||
import bonsai.bim.helper
|
||||
from bpy.types import Panel, UIList
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
@@ -28,7 +31,7 @@ from bonsai.bim.module.sequence.data import (
|
||||
TaskICOMData,
|
||||
AnimationColorSchemeData,
|
||||
)
|
||||
from typing import Any
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
class BIM_PT_status(Panel):
|
||||
@@ -320,6 +323,7 @@ class BIM_PT_work_schedules(Panel):
|
||||
row.operator("bim.contract_all_tasks", text="Contract All")
|
||||
row = self.layout.row(align=True)
|
||||
self.draw_task_operators()
|
||||
BIM_UL_tasks.draw_header(self.layout)
|
||||
self.layout.template_list(
|
||||
"BIM_UL_tasks",
|
||||
"",
|
||||
@@ -814,10 +818,21 @@ class BIM_UL_product_output_tasks(UIList):
|
||||
|
||||
|
||||
class BIM_UL_tasks(UIList):
|
||||
@classmethod
|
||||
def draw_header(cls, layout: bpy.types.UILayout):
|
||||
props = bpy.context.scene.BIMWorkScheduleProperties
|
||||
row = layout.row(align=True)
|
||||
|
||||
split1 = row.split(factor=0.1)
|
||||
split1.label(text="ID", icon="BLANK1")
|
||||
split2 = split1.split(factor=0.9 - min(0.5, 0.15 * len(props.columns)))
|
||||
split2.label(text="Name")
|
||||
cls.draw_custom_columns(props, split2, header=True)
|
||||
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
if item:
|
||||
self.props = context.scene.BIMWorkScheduleProperties
|
||||
task = IfcStore.get_file().by_id(item.ifc_definition_id)
|
||||
task = SequenceData.data["tasks"][item.ifc_definition_id]
|
||||
row = layout.row(align=True)
|
||||
|
||||
self.draw_hierarchy(row, item)
|
||||
@@ -827,7 +842,7 @@ class BIM_UL_tasks(UIList):
|
||||
split2 = split1.split(factor=0.9 - min(0.5, 0.15 * len(self.props.columns)))
|
||||
split2.prop(item, "name", emboss=False, text="")
|
||||
|
||||
self.draw_custom_columns(split2, item, task)
|
||||
BIM_UL_tasks.draw_custom_columns(self.props, split2, item, task)
|
||||
|
||||
if self.props.active_task_id and self.props.editing_task_type == "ATTRIBUTES":
|
||||
row.prop(
|
||||
@@ -860,7 +875,7 @@ class BIM_UL_tasks(UIList):
|
||||
op = row.operator("bim.assign_successor", text="", icon="TRACKING_FORWARDS", emboss=False)
|
||||
op.task = item.ifc_definition_id
|
||||
|
||||
def draw_order_operator(self, row, ifc_definition_id):
|
||||
def draw_order_operator(self, row: bpy.types.UILayout, ifc_definition_id: int) -> None:
|
||||
task = SequenceData.data["tasks"][ifc_definition_id]
|
||||
if task["NestingIndex"] is not None:
|
||||
if task["NestingIndex"] == 0:
|
||||
@@ -872,7 +887,7 @@ class BIM_UL_tasks(UIList):
|
||||
op.task = ifc_definition_id
|
||||
op.new_index = task["NestingIndex"] - 1
|
||||
|
||||
def draw_hierarchy(self, row, item):
|
||||
def draw_hierarchy(self, row: bpy.types.UILayout, item: bpy.types.PropertyGroup) -> None:
|
||||
for i in range(0, item.level_index):
|
||||
row.label(text="", icon="BLANK1")
|
||||
if item.has_children:
|
||||
@@ -887,37 +902,70 @@ class BIM_UL_tasks(UIList):
|
||||
else:
|
||||
row.label(text="", icon="DOT")
|
||||
|
||||
def draw_custom_columns(self, row, item, task):
|
||||
for column in self.props.columns:
|
||||
@classmethod
|
||||
def draw_custom_columns(
|
||||
cls,
|
||||
props: bpy.types.PropertyGroup,
|
||||
row: bpy.types.UILayout,
|
||||
item: Optional[bpy.types.PropertyGroup] = None,
|
||||
task: Optional[dict[str, Any]] = None,
|
||||
*,
|
||||
header: bool = False,
|
||||
) -> None:
|
||||
if not header:
|
||||
assert item and task
|
||||
|
||||
for column in props.columns:
|
||||
if column.name == "IfcTaskTime.ScheduleStart":
|
||||
if item.derived_start:
|
||||
row.label(text=item.derived_start + "*")
|
||||
if header:
|
||||
row.label(text="Start")
|
||||
else:
|
||||
row.prop(item, "start", emboss=False, text="")
|
||||
if item.derived_start:
|
||||
row.label(text=item.derived_start + "*")
|
||||
else:
|
||||
row.prop(item, "start", emboss=False, text="")
|
||||
elif column.name == "IfcTaskTime.ScheduleFinish":
|
||||
if item.derived_finish:
|
||||
row.label(text=item.derived_finish + "*")
|
||||
if header:
|
||||
row.label(text="Finish")
|
||||
else:
|
||||
row.prop(item, "finish", emboss=False, text="")
|
||||
if item.derived_finish:
|
||||
row.label(text=item.derived_finish + "*")
|
||||
else:
|
||||
row.prop(item, "finish", emboss=False, text="")
|
||||
elif column.name == "IfcTaskTime.ScheduleDuration":
|
||||
if item.derived_duration:
|
||||
row.label(text=item.derived_duration + "*")
|
||||
if header:
|
||||
row.label(text="Duration")
|
||||
else:
|
||||
row.prop(item, "duration", emboss=False, text="")
|
||||
if item.derived_duration:
|
||||
row.label(text=item.derived_duration + "*")
|
||||
else:
|
||||
row.prop(item, "duration", emboss=False, text="")
|
||||
elif column.name == "Controls.Calendar":
|
||||
if item.derived_calendar:
|
||||
row.label(text=item.derived_calendar + "*")
|
||||
if header:
|
||||
row.label(text="Calendar")
|
||||
else:
|
||||
row.label(text=item.calendar or "-")
|
||||
if item.derived_calendar:
|
||||
row.label(text=item.derived_calendar + "*")
|
||||
else:
|
||||
row.label(text=item.calendar or "-")
|
||||
else:
|
||||
ifc_class, name = column.name.split(".")
|
||||
if ifc_class == "IfcTask":
|
||||
value = getattr(task, name)
|
||||
elif ifc_class == "IfcTaskTime":
|
||||
value = getattr(task.TaskTime, name) if task.TaskTime else None
|
||||
if value is None:
|
||||
value = "-"
|
||||
row.label(text=str(value))
|
||||
if header:
|
||||
row.label(text=name)
|
||||
else:
|
||||
if ifc_class == "IfcTask":
|
||||
value = task[name]
|
||||
elif ifc_class == "IfcTaskTime":
|
||||
if (task_time_id := task["TaskTime"]) is None:
|
||||
value = None
|
||||
else:
|
||||
value = SequenceData.data["task_times"][task_time_id][name]
|
||||
else:
|
||||
assert False, f"Unexpected ifc_class '{ifc_class}'."
|
||||
if value is None:
|
||||
value = "-"
|
||||
else:
|
||||
row.label(text=str(value))
|
||||
|
||||
|
||||
class BIM_PT_work_calendars(Panel):
|
||||
|
||||
@@ -76,9 +76,20 @@ class DereferenceStructure(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class AssignContainer(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.assign_container"
|
||||
bl_label = "Assign Container"
|
||||
bl_description = "Assign the selected objects to the selected container"
|
||||
bl_description = "\n".join(
|
||||
(
|
||||
"Assign the selected objects to the selected container.",
|
||||
"This will move objects to the container collection in the outliner.",
|
||||
"ALT + Click to ensure objects are only linked in the container collection",
|
||||
)
|
||||
)
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
container: bpy.props.IntProperty(options={"SKIP_SAVE"})
|
||||
remove_from_other_containers: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.remove_from_other_containers = event.alt
|
||||
return self.execute(context)
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.active_object.BIMObjectSpatialProperties
|
||||
@@ -87,6 +98,9 @@ class AssignContainer(bpy.types.Operator, tool.Ifc.Operator):
|
||||
elif (container_obj := props.container_obj) and (container := tool.Ifc.get_entity(container_obj)):
|
||||
pass
|
||||
for element_obj in context.selected_objects:
|
||||
if self.remove_from_other_containers:
|
||||
for col in element_obj.users_collection[:]:
|
||||
col.objects.unlink(element_obj)
|
||||
core.assign_container(tool.Ifc, tool.Collector, tool.Spatial, container=container, element_obj=element_obj)
|
||||
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import bpy
|
||||
from . import ui, prop, operator, workspace
|
||||
|
||||
classes = (
|
||||
operator.ShowLoads,
|
||||
operator.LoadStructuralAnalysisModels,
|
||||
operator.DisableStructuralAnalysisModelEditingUI,
|
||||
operator.AddStructuralAnalysisModel,
|
||||
@@ -83,6 +84,7 @@ classes = (
|
||||
ui.BIM_PT_connected_structural_members,
|
||||
ui.BIM_UL_structural_analysis_models,
|
||||
ui.BIM_UL_structural_activities,
|
||||
ui.BIM_PT_show_structural_activities,
|
||||
ui.BIM_PT_structural_load_cases,
|
||||
ui.BIM_UL_structural_loads,
|
||||
ui.BIM_PT_structural_loads,
|
||||
|
||||
@@ -30,6 +30,50 @@ def refresh():
|
||||
StructuralLoadCasesData.is_loaded = False
|
||||
StructuralLoadsData.is_loaded = False
|
||||
BoundaryConditionsData.is_loaded = False
|
||||
LoadGroupDecorationData.is_loaded = False
|
||||
|
||||
|
||||
class LoadGroupDecorationData:
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.data = {"load groups to show": cls.load_groups_to_show()}
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def load_groups_to_show(cls):
|
||||
ret = []
|
||||
abrv = {
|
||||
"LOAD_CASE": "L.Case: ",
|
||||
"LOAD_COMBINATION": "L.Comb: ",
|
||||
"LOAD_GROUP": "L.Gr: ",
|
||||
"USERDEFINED": "U.Def: ",
|
||||
"NOTDEFINED": "N.Def: ",
|
||||
}
|
||||
models = tool.Ifc.get().by_type("IfcStructuralAnalysisModel")
|
||||
m = models[0]
|
||||
props = bpy.context.scene.BIMStructuralProperties
|
||||
if props.activity_type == "Action":
|
||||
groups = m.LoadedBy or []
|
||||
for g in groups:
|
||||
ret.append((str(g.id()), ". " + abrv[g.PredefinedType] + " " + g.Name, ""))
|
||||
related_objects = [rel.RelatedObjects for rel in g.IsGroupedBy]
|
||||
for item in related_objects:
|
||||
for subgoup in [sg for sg in item if sg.is_a("IfcStructuralLoadGroup")]:
|
||||
ret.append((str(subgoup.id()), ". " + abrv[subgoup.PredefinedType] + subgoup.Name, ""))
|
||||
|
||||
if props.activity_type == "External Reaction":
|
||||
groups = m.HasResults or []
|
||||
for g in groups:
|
||||
result_name = g.ResultForLoadGroup.Name or ""
|
||||
group_name = g.Name or ""
|
||||
ret.append((str(g.id()), group_name + " " + result_name, ""))
|
||||
|
||||
if len(ret) == 0:
|
||||
ret.append(("", "", ""))
|
||||
return ret
|
||||
|
||||
|
||||
class StructuralBoundaryConditionsData:
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of Bonsai.
|
||||
#
|
||||
# Bonsai is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Bonsai is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from mathutils import Vector
|
||||
import numpy as np
|
||||
import bpy
|
||||
import gpu
|
||||
import blf
|
||||
from bpy.types import SpaceView3D
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
from typing import Iterable, Union
|
||||
from bonsai.bim.module.structural.load_decoration_data import ShaderInfo
|
||||
|
||||
|
||||
class LoadsDecorator:
|
||||
"""Decorator to show strucutural loads in 3D"""
|
||||
|
||||
is_installed = False
|
||||
handlers = []
|
||||
decoration_data = None
|
||||
text_info = []
|
||||
shader_info = []
|
||||
depth_array = None
|
||||
|
||||
@classmethod
|
||||
def install(cls, context: bpy.types.Context) -> None:
|
||||
if cls.is_installed:
|
||||
cls.uninstall()
|
||||
handler = cls()
|
||||
cls.handlers.append(
|
||||
SpaceView3D.draw_handler_add(handler.draw_load_values, ((context,)), "WINDOW", "POST_PIXEL")
|
||||
)
|
||||
cls.handlers.append(SpaceView3D.draw_handler_add(handler, (), "WINDOW", "POST_VIEW"))
|
||||
cls.decoration_data = ShaderInfo()
|
||||
cls.update()
|
||||
cls.is_installed = True
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls) -> None:
|
||||
for handler in cls.handlers:
|
||||
try:
|
||||
SpaceView3D.draw_handler_remove(handler, "WINDOW")
|
||||
except ValueError:
|
||||
pass
|
||||
cls.is_installed = False
|
||||
|
||||
@classmethod
|
||||
def update(cls) -> None:
|
||||
cls.decoration_data.update()
|
||||
cls.text_info = cls.decoration_data.text_info
|
||||
cls.shader_info = cls.decoration_data.info
|
||||
|
||||
def __call__(self) -> None:
|
||||
"""set gpu configurations to draw 3D representations"""
|
||||
# set open gl configurations
|
||||
original_blend = gpu.state.blend_get()
|
||||
original_depth_test = gpu.state.depth_test_get()
|
||||
gpu.state.blend_set("ALPHA")
|
||||
gpu.state.depth_test_set("LESS_EQUAL")
|
||||
|
||||
self.draw_batch()
|
||||
|
||||
# restore opengl configurations
|
||||
gpu.state.blend_set(original_blend)
|
||||
gpu.state.depth_test_set(original_depth_test)
|
||||
|
||||
def draw_batch(self) -> None:
|
||||
"""draw the 3D representation of loads"""
|
||||
if not self.decoration_data.is_empty:
|
||||
for info in self.shader_info:
|
||||
shader = info["shader"]
|
||||
args = info["args"]
|
||||
indices = info["indices"]
|
||||
batch = batch_for_shader(shader, "TRIS", args, indices=indices)
|
||||
matrix = bpy.context.region_data.perspective_matrix
|
||||
shader.bind()
|
||||
shader.uniform_float("viewProjectionMatrix", matrix)
|
||||
|
||||
for key, value in info["uniforms"]:
|
||||
shader.uniform_float(key, value)
|
||||
|
||||
batch.draw(shader)
|
||||
|
||||
def draw_load_values(self, context: bpy.types.Context) -> None:
|
||||
"""draw text representing the load values"""
|
||||
# getting depth buffer info, code adapted from:
|
||||
# https://blender.stackexchange.com/questions/177185/is-there-a-way-to-render-depth-buffer-into-a-texture-with-gpu-bgl-python-modules
|
||||
framebuffer = gpu.state.active_framebuffer_get()
|
||||
width = context.region.width
|
||||
height = context.region.height
|
||||
depth_buffer = framebuffer.read_depth(0, 0, width, height)
|
||||
depth_array = np.array(depth_buffer.to_list())
|
||||
|
||||
f = context.area.spaces.active.clip_end
|
||||
n = context.area.spaces.active.clip_start
|
||||
|
||||
self.depth_array = n / (f - (f - n) * depth_array) * (f - n)
|
||||
|
||||
for info in self.text_info:
|
||||
text_position = self.location_3d_to_region_2d(info["position"], context)
|
||||
if text_position is not None:
|
||||
font_id = 0
|
||||
blf.position(font_id, text_position[0], text_position[1], text_position[2])
|
||||
blf.size(font_id, 20.0)
|
||||
blf.color(font_id, 0.9, 0.9, 0.9, 1.0)
|
||||
blf.draw(font_id, info["text"])
|
||||
|
||||
def location_3d_to_region_2d(self, coord: Iterable, context: bpy.types.Context) -> Union[Vector, None]:
|
||||
"""Convert from 3D space to 2D screen space.
|
||||
Filter out the text supposed to be hidden by 3D elements, using the depth array.
|
||||
It also hides text that are distant from the camera view to avoid clutter"""
|
||||
|
||||
coord = Vector(coord)
|
||||
rv3d = context.region_data
|
||||
perspective = rv3d.view_perspective
|
||||
view_matrix = rv3d.view_matrix
|
||||
point_view_space = view_matrix @ coord
|
||||
|
||||
if perspective == "ORTHO" or -10 < point_view_space.z < 0:
|
||||
prj = rv3d.perspective_matrix @ Vector((coord[0], coord[1], coord[2], 1.0))
|
||||
width_half = context.region.width / 2.0
|
||||
height_half = context.region.height / 2.0
|
||||
coord_2d = Vector(
|
||||
(
|
||||
width_half + width_half * (prj.x / prj.w),
|
||||
height_half + height_half * (prj.y / prj.w),
|
||||
point_view_space.z,
|
||||
)
|
||||
)
|
||||
if (
|
||||
coord_2d[0] < 0
|
||||
or coord_2d[0] > context.region.width
|
||||
or coord_2d[1] > context.region.height
|
||||
or coord_2d[1] < 0
|
||||
):
|
||||
return None
|
||||
depth = self.depth_array[int(coord_2d[1])][int(coord_2d[0])]
|
||||
if -0.98 * point_view_space.z > depth:
|
||||
return None
|
||||
return coord_2d
|
||||
return None
|
||||
@@ -0,0 +1,993 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of Bonsai.
|
||||
#
|
||||
# Bonsai is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Bonsai is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import bmesh
|
||||
import numpy as np
|
||||
from math import sin
|
||||
from mathutils import Vector
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.util.attribute
|
||||
import ifcopenshell.util.unit as ifcunit
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
from bonsai.bim.module.structural.shader import DecorationShader
|
||||
from typing import Literal, TypedDict, Iterable
|
||||
|
||||
MemberInfo = TypedDict(
|
||||
"MemberInfo",
|
||||
{"member": ifcopenshell.entity_instance, "activities": list[tuple[ifcopenshell.entity_instance, float]]},
|
||||
)
|
||||
|
||||
LoadConfigItem = TypedDict(
|
||||
"LoadConfigItem", {"pos": float, "descr": Literal["start", "end", "middle"], "load values": np.ndarray}
|
||||
)
|
||||
|
||||
DiscreteConfigItem = TypedDict("DiscreteConfigItem", {"pos": float, "values": list[float]})
|
||||
|
||||
ParsedLoad = TypedDict(
|
||||
"ParsedLoad",
|
||||
{
|
||||
"constant force": list[float],
|
||||
"quadratic force": list[float],
|
||||
"sinus force": list[float],
|
||||
"linear load configuration": list[list[LoadConfigItem]],
|
||||
"point load configuration": list[list[DiscreteConfigItem]],
|
||||
},
|
||||
)
|
||||
LoadByDirection = TypedDict(
|
||||
"LoadByDirection", {"constant": float, "quadratic": float, "sinus": float, "polyline": list[list[float]]}
|
||||
)
|
||||
|
||||
ProcessedLoad = TypedDict(
|
||||
"ProcessedLoad",
|
||||
{"linear loads": LoadByDirection, "max linear load": float, "discrete loads": list[list[DiscreteConfigItem]]},
|
||||
)
|
||||
|
||||
|
||||
class ShaderInfo:
|
||||
def __init__(self) -> None:
|
||||
self.is_empty = True
|
||||
self.shader = DecorationShader()
|
||||
self.curve_members: dict[str, MemberInfo] = {}
|
||||
self.point_members: dict[str, MemberInfo] = {}
|
||||
self.surface_members: dict[str, MemberInfo] = {}
|
||||
self.text_info = []
|
||||
self.info = []
|
||||
self.force_unit = ""
|
||||
self.moment_unit = ""
|
||||
self.linear_force_unit = ""
|
||||
self.linear_moment_unit = ""
|
||||
self.planar_force_unit = ""
|
||||
|
||||
def update(self) -> None:
|
||||
self.info = []
|
||||
self.text_info = []
|
||||
self.curve_members = {}
|
||||
self.point_members = {}
|
||||
self.surface_members = {}
|
||||
self.get_force_units()
|
||||
self.get_strucutural_elements_and_activities()
|
||||
self.get_linear_loads()
|
||||
self.get_point_loads()
|
||||
self.get_planar_loads()
|
||||
if len(self.info):
|
||||
self.is_empty = False
|
||||
|
||||
def get_force_units(self) -> None:
|
||||
def get_unit_symbol(unit: ifcopenshell.entity_instance) -> str:
|
||||
prefix_symbols = {
|
||||
"EXA": "E",
|
||||
"PETA": "P",
|
||||
"TERA": "T",
|
||||
"GIGA": "G",
|
||||
"MEGA": "M",
|
||||
"KILO": "k",
|
||||
"HECTO": "h",
|
||||
"DECA": "da",
|
||||
"DECI": "d",
|
||||
"CENTI": "c",
|
||||
"MILLI": "m",
|
||||
"MICRO": "μ",
|
||||
"NANO": "n",
|
||||
"PICO": "p",
|
||||
"FEMTO": "f",
|
||||
"ATTO": "a",
|
||||
}
|
||||
|
||||
unit_symbols = {
|
||||
# si units
|
||||
"CUBIC_METRE": "m3",
|
||||
"GRAM": "g",
|
||||
"SECOND": "s",
|
||||
"SQUARE_METRE": "m2",
|
||||
"METRE": "m",
|
||||
"NEWTON": "N",
|
||||
"PASCAL": "Pa",
|
||||
# conversion based units
|
||||
"pound-force": "lbf",
|
||||
"pound-force per square inch": "psi",
|
||||
"thou": "th",
|
||||
"inch": "in",
|
||||
"foot": "ft",
|
||||
"yard": "yd",
|
||||
"mile": "mi",
|
||||
"square thou": "th2",
|
||||
"square inch": "in2",
|
||||
"square foot": "ft2",
|
||||
"square yard": "yd2",
|
||||
"acre": "ac",
|
||||
"square mile": "mi2",
|
||||
"cubic thou": "th3",
|
||||
"cubic inch": "in3",
|
||||
"cubic foot": "ft3",
|
||||
"cubic yard": "yd3",
|
||||
"cubic mile": "mi3",
|
||||
"litre": "L",
|
||||
"fluid ounce UK": "fl oz",
|
||||
"fluid ounce US": "fl oz",
|
||||
"pint UK": "pt",
|
||||
"pint US": "pt",
|
||||
"gallon UK": "gal",
|
||||
"gallon US": "gal",
|
||||
"degree": "°",
|
||||
"ounce": "oz",
|
||||
"pound": "lb",
|
||||
"ton UK": "ton",
|
||||
"ton US": "ton",
|
||||
"lbf": "lbf",
|
||||
"kip": "kip",
|
||||
"psi": "psi",
|
||||
"ksi": "ksi",
|
||||
"minute": "min",
|
||||
"hour": "hr",
|
||||
"day": "day",
|
||||
"btu": "btu",
|
||||
"fahrenheit": "°F",
|
||||
}
|
||||
symbol = ""
|
||||
if unit.is_a("IfcSIUnit"):
|
||||
symbol += prefix_symbols.get(unit.Prefix, "")
|
||||
symbol += unit_symbols.get(unit.Name.replace("METER", "METRE"), "?")
|
||||
return symbol
|
||||
|
||||
length_units = [u for u in tool.Ifc.get().by_type("IfcNamedUnit") if u.UnitType == "LENGTHUNIT"]
|
||||
force_units = [u for u in tool.Ifc.get().by_type("IfcNamedUnit") if u.UnitType == "FORCEUNIT"]
|
||||
linear_force_units = [u for u in tool.Ifc.get().by_type("IfcDerivedUnit") if u.UnitType == "LINEARFORCEUNIT"]
|
||||
linear_moment_units = [u for u in tool.Ifc.get().by_type("IfcDerivedUnit") if u.UnitType == "LINEARMOMENTUNIT"]
|
||||
planar_force_units = [u for u in tool.Ifc.get().by_type("IfcDerivedUnit") if u.UnitType == "PLANARFORCEUNIT"]
|
||||
|
||||
conversion_force_unit = [u for u in force_units if u.is_a("IfcConversionBasedUnit")]
|
||||
if len(conversion_force_unit) == 0:
|
||||
conversion_force_unit.append(force_units[0])
|
||||
self.force_unit = ifcunit.get_unit_symbol(conversion_force_unit[0])
|
||||
|
||||
conversion_length_unit = [u for u in length_units if u.is_a("IfcConversionBasedUnit")]
|
||||
if len(conversion_length_unit) == 0:
|
||||
conversion_length_unit.append(length_units[0])
|
||||
length_unit = ifcunit.get_unit_symbol(conversion_length_unit[0])
|
||||
self.moment_unit = self.force_unit + "." + length_unit
|
||||
|
||||
first = ""
|
||||
second = ""
|
||||
for e in linear_force_units[0].Elements:
|
||||
if e.Unit.UnitType == "FORCEUNIT":
|
||||
first = ifcunit.get_unit_symbol(e.Unit)
|
||||
if e.Unit.UnitType == "LENGTHUNIT":
|
||||
second = ifcunit.get_unit_symbol(e.Unit)
|
||||
self.linear_force_unit = first + "/" + second
|
||||
|
||||
first = ""
|
||||
second = ""
|
||||
for e in linear_moment_units[0].Elements:
|
||||
if e.Unit.UnitType == "FORCEUNIT":
|
||||
first = ifcunit.get_unit_symbol(e.Unit)
|
||||
if e.Unit.UnitType == "LENGTHUNIT":
|
||||
second = ifcunit.get_unit_symbol(e.Unit)
|
||||
self.linear_moment_unit = first + "." + second + "/" + second
|
||||
|
||||
first = ""
|
||||
second = ""
|
||||
for e in planar_force_units[0].Elements:
|
||||
if e.Unit.UnitType == "FORCEUNIT":
|
||||
first = ifcunit.get_unit_symbol(e.Unit)
|
||||
if e.Unit.UnitType == "LENGTHUNIT":
|
||||
second = ifcunit.get_unit_symbol(e.Unit) + "2"
|
||||
if e.Unit.UnitType == "AREAUNIT":
|
||||
second = ifcunit.get_unit_symbol(e.Unit)
|
||||
self.planar_force_unit = first + "/" + second
|
||||
|
||||
def get_strucutural_elements_and_activities(self) -> None:
|
||||
"""fills self.point_members, self.curve_members and self.surface_members dictionaries"""
|
||||
|
||||
def populate_members_dict(
|
||||
dict_name: Literal["point_members", "curve_members", "surface_members"],
|
||||
element: ifcopenshell.entity_instance,
|
||||
activity: ifcopenshell.entity_instance,
|
||||
factor: float,
|
||||
) -> None:
|
||||
"""
|
||||
fills self.point_members, self.curve_members and self.surface_members dictionaries
|
||||
thoses dicts will contain the strucutural member global id as key and a second dict as value
|
||||
the second dict contais two keys, as follow:
|
||||
{
|
||||
"member": the strucutral member itself
|
||||
"activities: list[(activity, factor)] for each activity applied to the member
|
||||
}
|
||||
dict_name: "point_members", "curve_members" or "surface_members"
|
||||
element: IfcStructuralMember
|
||||
activity: IfcStructuralActivity
|
||||
factor: float to multiply the loads values in the structural activity
|
||||
"""
|
||||
dic = getattr(self, dict_name, None)
|
||||
if dic is None:
|
||||
return
|
||||
member = dic.get(element.GlobalId)
|
||||
if member is None:
|
||||
dic.update({element.GlobalId: {"member": element, "activities": [(activity, factor)]}})
|
||||
else:
|
||||
member["activities"].append((activity, factor))
|
||||
|
||||
def recursive_subgroups(
|
||||
groups: list[ifcopenshell.entity_instance],
|
||||
rec_limit: int,
|
||||
activity_type: Literal["Action", "External Reaction"],
|
||||
factor: float = 1,
|
||||
) -> None:
|
||||
"""
|
||||
Recursively fills self.point_members, self.curve_members and self.surface_members dictionaries
|
||||
with the structural members to wich the activities in the load group and its subgroups are applied
|
||||
it also creates a list with all the activities applied to that member that are in the same
|
||||
load group or subgroup (see populate_members_dict description)
|
||||
|
||||
groups: list of Ifc load case, load group or load combination
|
||||
rec_limit: maximum number of recursions
|
||||
activity_type: "Action" or "External Reaction"
|
||||
factor: the factor applied to the group, default = 1
|
||||
this factor will be multiplied by the group coefficient and the factor of a
|
||||
IfcRelAssignsToGroupByFactor relationship of subgroups in load combinations
|
||||
|
||||
"""
|
||||
if rec_limit == 0 or len(groups) == 0:
|
||||
return None
|
||||
for group in groups:
|
||||
subgorups = []
|
||||
activities = []
|
||||
relationship = [rel for rel in group.IsGroupedBy]
|
||||
coef = getattr(group, "Coefficient", 1.0)
|
||||
group_coef = coef if coef is not None else 1.0
|
||||
rel_factor = 1.0
|
||||
|
||||
for rel in relationship:
|
||||
if rel.is_a("IfcRelAssignsToGroupByFactor"):
|
||||
rel_factor = rel.Factor if rel.Factor is not None else 1.0
|
||||
objects = rel.RelatedObjects
|
||||
subgorups = [sg for sg in objects if sg.is_a("IfcStructuralLoadGroup")]
|
||||
activities = [a for a in objects if a.is_a("IfcStructuralActivity")]
|
||||
factor = factor * group_coef * rel_factor
|
||||
for activity in activities:
|
||||
if len(activity.AssignedToStructuralItem):
|
||||
element = activity.AssignedToStructuralItem[0].RelatingElement
|
||||
if element is not None:
|
||||
if activity_type == "Action":
|
||||
if element.is_a("IfcStructuralCurveMember"):
|
||||
populate_members_dict("curve_members", element, activity, factor)
|
||||
elif element.is_a("IfcStructuralPointConnection"):
|
||||
populate_members_dict("point_members", element, activity, factor)
|
||||
elif element.is_a("IfcStructuralSurfaceMember"):
|
||||
populate_members_dict("surface_members", element, activity, factor)
|
||||
|
||||
elif (
|
||||
activity_type == "External Reaction"
|
||||
and getattr(element, "AppliedCondition", None) is not None
|
||||
):
|
||||
if element.is_a("IfcStructuralCurveMember"):
|
||||
populate_members_dict("curve_members", element, activity, factor)
|
||||
elif element.is_a("IfcStructuralPointConnection"):
|
||||
populate_members_dict("point_members", element, activity, factor)
|
||||
elif element.is_a("IfcStructuralSurfaceMember"):
|
||||
populate_members_dict("surface_members", element, activity, factor)
|
||||
recursive_subgroups(subgorups, rec_limit - 1, activity_type, factor=factor)
|
||||
|
||||
props = bpy.context.scene.BIMStructuralProperties
|
||||
group_definition_id = int(props.load_group_to_show)
|
||||
file = IfcStore.get_file()
|
||||
groups = [file.by_id(group_definition_id)]
|
||||
recursive_subgroups(groups, 10, props.activity_type)
|
||||
|
||||
def get_planar_loads(self) -> None:
|
||||
"""get the necessary information to render the planar load representation in 3D and its text information"""
|
||||
|
||||
list_of_surfaces = self.surface_members
|
||||
shader = self.shader.new("PLANAR LOAD")
|
||||
maximum = 0
|
||||
for value in list_of_surfaces.values():
|
||||
surf = value["member"]
|
||||
activity_list = value["activities"]
|
||||
if len(activity_list) == 0:
|
||||
continue
|
||||
rotation = self.get_surface_member_rotation(surf)
|
||||
values = self.get_planar_loads_values(activity_list, rotation)
|
||||
if maximum == 0:
|
||||
maximum = max([abs(float(i)) for i in values])
|
||||
if maximum == 0:
|
||||
continue
|
||||
props = bpy.context.scene.BIMStructuralProperties
|
||||
reference_frame = props.reference_frame
|
||||
orientation = np.eye(3)
|
||||
if reference_frame == "LOCAL_COORDS":
|
||||
orientation = rotation
|
||||
blender_object: bpy.types.Object = IfcStore.get_element(getattr(surf, "GlobalId", None))
|
||||
mat = blender_object.matrix_world
|
||||
mesh: bpy.types.Mesh = blender_object.data
|
||||
|
||||
positions = []
|
||||
indices = []
|
||||
coord = []
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
bmesh.ops.triangulate(bm, faces=bm.faces)
|
||||
bm.edges.ensure_lookup_table()
|
||||
bm.verts.ensure_lookup_table()
|
||||
bm.faces.ensure_lookup_table()
|
||||
|
||||
for v in bm.verts:
|
||||
p1 = np.array(mat @ v.co)
|
||||
positions.append(p1)
|
||||
p2 = p1 - (orientation @ values) * 0.2 / maximum
|
||||
positions.append(p2)
|
||||
coord.append((float(p1[0] + p1[1]), 0, 1))
|
||||
coord.append((float(p1[0] + p1[1]), 1, 1))
|
||||
for e in bm.edges:
|
||||
if len(e.link_faces) > 1:
|
||||
continue
|
||||
indices.append((2 * e.verts[0].index, 2 * e.verts[0].index + 1, 2 * e.verts[1].index))
|
||||
indices.append((2 * e.verts[0].index + 1, 2 * e.verts[1].index, 2 * e.verts[1].index + 1))
|
||||
for p in bm.faces:
|
||||
indices.append((2 * p.verts[0].index + 1, 2 * p.verts[1].index + 1, 2 * p.verts[2].index + 1))
|
||||
bmesh.ops.dissolve_limit(bm, angle_limit=0.01, verts=bm.verts, edges=bm.edges)
|
||||
bm.faces.ensure_lookup_table()
|
||||
center = bm.faces[0].calc_center_bounds()
|
||||
|
||||
self.text_info.append(
|
||||
{
|
||||
"position": mat @ center - Vector((orientation @ values) * 0.2 / maximum),
|
||||
"text": f"{values[2]:.5f} {self.planar_force_unit}",
|
||||
}
|
||||
)
|
||||
|
||||
self.info.append(
|
||||
{
|
||||
"shader": shader,
|
||||
"args": {"position": positions, "coord": coord},
|
||||
"indices": indices,
|
||||
"uniforms": [["color", (0.2, 0, 1, 1)], ["spacing", 0.2]],
|
||||
}
|
||||
)
|
||||
|
||||
def get_planar_loads_values(
|
||||
self, activity_list: list[tuple[ifcopenshell.entity_instance, float]], element_rotation_matrix: np.ndarray
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
returns a numpy array with the sum of the values of structural activities
|
||||
applied loads in each direction, multiplied by the factors in load combinations
|
||||
"""
|
||||
values = np.zeros((3))
|
||||
for item in activity_list:
|
||||
activity = item[0]
|
||||
factor = item[1]
|
||||
load = activity.AppliedLoad
|
||||
temp = np.zeros((3))
|
||||
if load is not None and load.is_a("IfcStructuralLoadPlanarForce"):
|
||||
temp[0] = load.PlanarForceX if load.PlanarForceX is not None else 0
|
||||
temp[1] = load.PlanarForceY if load.PlanarForceY is not None else 0
|
||||
temp[2] = load.PlanarForceZ if load.PlanarForceZ is not None else 0
|
||||
temp = temp * factor
|
||||
transform = self.get_activity_transform_matrix(activity, element_rotation_matrix)
|
||||
values += transform @ temp
|
||||
return values
|
||||
|
||||
def get_surface_member_rotation(self, surface_member: ifcopenshell.entity_instance) -> np.ndarray:
|
||||
"""returns the rotation matrix of a structural surface member"""
|
||||
representation = ifcopenshell.util.representation.get_representation(surface_member, "Model")
|
||||
repr_item = representation.Items[0]
|
||||
placement = ifcopenshell.util.placement.get_axis2placement(repr_item.FaceSurface.Position)
|
||||
rotation = placement[0:3, 0:3]
|
||||
return rotation
|
||||
|
||||
def get_point_connection_rotation(self, point_connection: ifcopenshell.entity_instance) -> np.ndarray:
|
||||
"""returns the rotation matrix of a structural point connection"""
|
||||
if point_connection.ConditionCoordinateSystem is not None:
|
||||
placement = ifcopenshell.util.placement.get_axis2placement(point_connection.ConditionCoordinateSystem)
|
||||
else:
|
||||
placement = np.eye(4)
|
||||
rotation = placement[0:3, 0:3]
|
||||
return rotation
|
||||
|
||||
def get_curve_member_rotation(self, curve_member: ifcopenshell.entity_instance) -> np.ndarray:
|
||||
"""returns the rotation matrix of a structural surface member"""
|
||||
z = curve_member.Axis.DirectionRatios
|
||||
edge = curve_member.Representation.Representations[0].Items[0]
|
||||
origin = edge.EdgeStart.VertexGeometry.Coordinates
|
||||
end = edge.EdgeEnd.VertexGeometry.Coordinates
|
||||
x = [c2 - c1 for c1, c2 in zip(origin, end)]
|
||||
placement = ifcopenshell.util.placement.a2p(origin, z, x)
|
||||
rotation = placement[0:3, 0:3]
|
||||
return rotation
|
||||
|
||||
def get_activity_transform_matrix(
|
||||
self, activity: ifcopenshell.entity_instance, element_rotation_matrix: np.ndarray
|
||||
) -> np.ndarray:
|
||||
"provides the transformation matrix to convert between reference frames"
|
||||
global_or_local = activity.GlobalOrLocal
|
||||
props = bpy.context.scene.BIMStructuralProperties
|
||||
reference_frame = props.reference_frame
|
||||
transform_matrix = np.eye(3)
|
||||
if reference_frame == "LOCAL_COORDS" and global_or_local != reference_frame:
|
||||
transform_matrix = np.linalg.inv(element_rotation_matrix)
|
||||
elif reference_frame == "GLOBAL_COORDS" and global_or_local != reference_frame:
|
||||
transform_matrix = element_rotation_matrix
|
||||
return transform_matrix
|
||||
|
||||
def get_point_loads(self) -> None:
|
||||
"""get the necessary information to render the point load representation in 3D and its text information"""
|
||||
|
||||
list_of_point_connections = self.point_members
|
||||
for value in list_of_point_connections.values():
|
||||
conn = value["member"]
|
||||
activity_list = value["activities"]
|
||||
if len(activity_list) == 0:
|
||||
continue
|
||||
blender_object = IfcStore.get_element(getattr(conn, "GlobalId", None))
|
||||
if blender_object.type == "MESH":
|
||||
conn_location = blender_object.matrix_world @ blender_object.data.vertices[0].co
|
||||
rotation = self.get_point_connection_rotation(conn)
|
||||
loads = self.get_point_loads_values(activity_list, rotation)
|
||||
self.get_point_shader_args(loads, conn_location, rotation)
|
||||
|
||||
def get_point_shader_args(self, loads: Iterable, location: np.ndarray, rotation: np.ndarray) -> None:
|
||||
"""get the args to the point shader"""
|
||||
location = np.array(location)
|
||||
indices = []
|
||||
direction_dict = {
|
||||
"fx": (np.array((1, 0, 0)), np.array((0, 1, 0)), np.array((0, 0, 1))),
|
||||
"fy": (np.array((0, 1, 0)), np.array((1, 0, 0)), np.array((0, 0, 1))),
|
||||
"fz": (np.array((0, 0, 1)), np.array((0, 1, 0)), np.array((1, 0, 0))),
|
||||
"mx": (np.array((0, 1, 0)), np.array((0, 0, 1))),
|
||||
"my": (np.array((1, 0, 0)), np.array((0, 0, 1))),
|
||||
"mz": (np.array((1, 0, 0)), np.array((0, 1, 0))),
|
||||
}
|
||||
keys = ["fx", "fy", "fz", "mx", "my", "mz"]
|
||||
props = bpy.context.scene.BIMStructuralProperties
|
||||
reference_frame = props.reference_frame
|
||||
if reference_frame == "LOCAL_COORDS":
|
||||
for key in keys:
|
||||
tup = direction_dict[key]
|
||||
li = []
|
||||
for item in tup:
|
||||
li.append(rotation @ item)
|
||||
direction_dict[key] = li
|
||||
|
||||
for i, key in enumerate(keys):
|
||||
if loads[i] == 0:
|
||||
continue
|
||||
color = (1, 0, 0, 1)
|
||||
if i in [1, 4]:
|
||||
color = (0, 1, 0, 1)
|
||||
elif i in [2, 5]:
|
||||
color = (0, 0, 1, 1)
|
||||
d1 = -(direction_dict[key][0] * loads[i])
|
||||
d1 = d1 / np.linalg.norm(d1)
|
||||
if i < 3:
|
||||
d2 = direction_dict[key][1]
|
||||
d3 = direction_dict[key][2]
|
||||
p1 = location
|
||||
p2 = location + d1 + d2
|
||||
p3 = location + d1 - d2
|
||||
p4 = location + d1 + d3
|
||||
p5 = location + d1 - d3
|
||||
position = [p1, p2, p3, p4, p5]
|
||||
indices = [(0, 1, 2), (0, 3, 4)]
|
||||
c1 = (0, 0, 0)
|
||||
c2 = (1, 1, 0)
|
||||
c3 = (-1, 1, 0)
|
||||
coords_for_shader = [c1, c2, c3, c2, c3]
|
||||
shader = self.shader.new("SINGLE FORCE")
|
||||
self.info.append(
|
||||
{
|
||||
"shader": shader,
|
||||
"args": {"position": position, "coord": coords_for_shader},
|
||||
"indices": indices,
|
||||
"uniforms": [["color", color], ["spacing", 0.2]],
|
||||
}
|
||||
)
|
||||
self.text_info.append({"position": location + d1, "text": f"{loads[i]:.2f} {self.force_unit}"})
|
||||
else:
|
||||
d2 = d2 = direction_dict[key][1]
|
||||
p1 = location - d2
|
||||
p2 = location + d1 + d2
|
||||
p3 = location - d1 + d2
|
||||
position = [p1, p2, p3]
|
||||
indices = [(0, 1, 2)]
|
||||
c1 = (-1, 0, 0)
|
||||
c2 = (1, 1, 0)
|
||||
c3 = (1, -1, 0)
|
||||
coords_for_shader = [c1, c2, c3]
|
||||
shader = self.shader.new("SINGLE MOMENT")
|
||||
self.info.append(
|
||||
{
|
||||
"shader": shader,
|
||||
"args": {"position": position, "coord": coords_for_shader},
|
||||
"indices": indices,
|
||||
"uniforms": [["color", color]],
|
||||
}
|
||||
)
|
||||
self.text_info.append(
|
||||
{"position": location + 0.25 * (d1 + d2), "text": f"{loads[i]:.2f} {self.moment_unit}"}
|
||||
)
|
||||
|
||||
def get_point_loads_values(
|
||||
self, activity_list: list[tuple[ifcopenshell.entity_instance, float]], element_rotation_matrix: np.ndarray
|
||||
) -> np.ndarray:
|
||||
"""returns a numpy array with the sum of the point load values"""
|
||||
result_list = np.zeros(6)
|
||||
attr_list = ["ForceX", "ForceY", "ForceZ", "MomentX", "MomentY", "MomentZ"]
|
||||
for item in activity_list:
|
||||
activity = item[0]
|
||||
factor = item[1]
|
||||
load = activity.AppliedLoad
|
||||
temp = np.zeros(6)
|
||||
for i, attr in enumerate(attr_list):
|
||||
value = 0 if getattr(load, attr, 0) is None else getattr(load, attr, 0)
|
||||
temp[i] += value * factor
|
||||
transform_3 = self.get_activity_transform_matrix(activity, element_rotation_matrix)
|
||||
transform_6 = np.zeros((6, 6))
|
||||
transform_6[0:3, 0:3] = transform_3
|
||||
transform_6[3:6, 3:6] = transform_3
|
||||
result_list += transform_6 @ temp
|
||||
|
||||
return result_list
|
||||
|
||||
def get_linear_loads(self) -> None:
|
||||
position = []
|
||||
indices = []
|
||||
sin_quad_lin = []
|
||||
coords_for_shader = []
|
||||
color = []
|
||||
info = []
|
||||
maxforce = 0
|
||||
|
||||
list_of_curve_members = self.curve_members
|
||||
for value in list_of_curve_members.values():
|
||||
member = value["member"]
|
||||
activity_list = value["activities"]
|
||||
if len(activity_list) == 0:
|
||||
continue
|
||||
|
||||
blender_object = IfcStore.get_element(getattr(member, "GlobalId", None))
|
||||
|
||||
start_co = blender_object.matrix_world @ blender_object.data.vertices[0].co
|
||||
end_co = blender_object.matrix_world @ blender_object.data.vertices[1].co
|
||||
x_axis = Vector(end_co - start_co).normalized()
|
||||
z_direction = getattr(member, "Axis")
|
||||
# local coordinates
|
||||
z_axis = Vector(getattr(z_direction, "DirectionRatios", None)).normalized()
|
||||
y_axis = z_axis.cross(x_axis).normalized()
|
||||
z_axis = x_axis.cross(y_axis).normalized()
|
||||
rotation = self.get_curve_member_rotation(member)
|
||||
|
||||
props = bpy.context.scene.BIMStructuralProperties
|
||||
reference_frame = props.reference_frame
|
||||
is_local = reference_frame == "LOCAL_COORDS"
|
||||
x_match = abs(Vector((1, 0, 0)).dot(x_axis)) > 0.99
|
||||
y_match = abs(Vector((0, 1, 0)).dot(x_axis)) > 0.99
|
||||
z_match = abs(Vector((0, 0, 1)).dot(x_axis)) > 0.99
|
||||
direction_dict = {
|
||||
"fx": y_axis + z_axis if is_local else Vector((1, 0, 0)) if not x_match else Vector((0, 1, 1)),
|
||||
"fy": y_axis if is_local else Vector((0, 1, 0)) if not y_match else Vector((1, 0, 1)),
|
||||
"fz": z_axis if is_local else Vector((0, 0, 1)) if not z_match else Vector((1, 1, 0)),
|
||||
"mx": z_axis - y_axis if is_local or x_match else Vector((1, 0, 0)).cross(x_axis),
|
||||
"my": (
|
||||
z_axis
|
||||
if is_local
|
||||
else Vector((-1, 0, 1)) if y_match else Vector((0, 1, 0)).cross(x_axis).normalized()
|
||||
),
|
||||
"mz": (
|
||||
y_axis
|
||||
if is_local
|
||||
else Vector((-1, 1, 0)) if z_match else Vector((0, 0, 1)).cross(x_axis).normalized()
|
||||
),
|
||||
}
|
||||
match_dict = {"fx": x_match or is_local, "fy": y_match, "fz": z_match}
|
||||
member_length = Vector(end_co - start_co).length
|
||||
processed_loads = self.process_total_linear_loads(activity_list, rotation, member_length)
|
||||
linear_loads = processed_loads["linear loads"]
|
||||
maxforce = max(maxforce, processed_loads["max linear load"])
|
||||
point_loads = processed_loads["discrete loads"]
|
||||
|
||||
if len(point_loads) > 0:
|
||||
for item in point_loads:
|
||||
for sub_item in item:
|
||||
pos = sub_item["pos"]
|
||||
values = sub_item["values"]
|
||||
pos_vector = start_co + x_axis * pos
|
||||
self.get_point_shader_args(values, pos_vector, rotation)
|
||||
if linear_loads is None:
|
||||
continue
|
||||
keys = ["fx", "fy", "fz", "mx", "my", "mz"]
|
||||
|
||||
for key in keys:
|
||||
polyline = linear_loads[key]["polyline"]
|
||||
sinus = linear_loads[key]["sinus"]
|
||||
quadratic = linear_loads[key]["quadratic"]
|
||||
constant = linear_loads[key]["constant"]
|
||||
direction = direction_dict[key]
|
||||
color_axis = (0, 0, 1, 1)
|
||||
if "x" in key:
|
||||
color_axis = (1, 0, 0, 1)
|
||||
if "y" in key:
|
||||
color_axis = (0, 1, 0, 1)
|
||||
|
||||
if "f" in key:
|
||||
unit = self.linear_force_unit
|
||||
if match_dict[key]:
|
||||
shader = self.shader.new("PARALLEL DISTRIBUTED FORCE")
|
||||
else:
|
||||
shader = self.shader.new("PERPENDICULAR DISTRIBUTED FORCE")
|
||||
else:
|
||||
unit = self.linear_moment_unit
|
||||
shader = self.shader.new("DISTRIBUTED MOMENT")
|
||||
|
||||
counter = 0
|
||||
for i in range(len(polyline) - 1):
|
||||
current = Vector(polyline[i] + [0])
|
||||
nextitem = Vector(polyline[i + 1] + [0])
|
||||
|
||||
if any([current.y, nextitem.y, constant, quadratic, sinus]):
|
||||
negative = -1 * direction + start_co + x_axis * current.x
|
||||
positive = direction + start_co + x_axis * current.x
|
||||
position.append(negative)
|
||||
coords_for_shader.append((current.x, 1.0, member_length))
|
||||
sin_quad_lin.append((sinus, quadratic, current.y + constant))
|
||||
color.append(color_axis)
|
||||
|
||||
x = current.x / member_length
|
||||
func = sin(x * 3.1416) * sinus + (-4.0 * x * x + 4.0 * x) * quadratic + constant + current.y
|
||||
if func:
|
||||
self.text_info.append(
|
||||
{
|
||||
"position": -1 * direction * func / maxforce + start_co + x_axis * current.x,
|
||||
"text": f"{func:.2f} {unit}",
|
||||
}
|
||||
)
|
||||
|
||||
position.append(positive)
|
||||
coords_for_shader.append((current[0], -1.0, member_length))
|
||||
sin_quad_lin.append((sinus, quadratic, current.y + constant))
|
||||
color.append(color_axis)
|
||||
|
||||
indices.append((0 + counter, 1 + counter, 2 + counter))
|
||||
indices.append((3 + counter, 2 + counter, 1 + counter))
|
||||
if i == len(polyline) - 2:
|
||||
negative = -1 * direction + start_co + x_axis * nextitem.x
|
||||
positive = direction + start_co + x_axis * nextitem.x
|
||||
position.append(negative)
|
||||
coords_for_shader.append((nextitem.x, 1.0, member_length))
|
||||
sin_quad_lin.append((sinus, quadratic, nextitem.y + constant))
|
||||
color.append(color_axis)
|
||||
|
||||
x = nextitem.x / member_length
|
||||
func = (
|
||||
sin(x * 3.1416) * sinus + (-4.0 * x * x + 4.0 * x) * quadratic + constant + nextitem.y
|
||||
)
|
||||
if func:
|
||||
self.text_info.append(
|
||||
{
|
||||
"position": -1 * direction * func / maxforce + start_co + x_axis * nextitem.x,
|
||||
"text": f"{func:.2f} {unit}",
|
||||
}
|
||||
)
|
||||
|
||||
position.append(positive)
|
||||
coords_for_shader.append((nextitem.x, -1.0, member_length))
|
||||
sin_quad_lin.append((sinus, quadratic, nextitem.y + constant))
|
||||
color.append(color_axis)
|
||||
|
||||
counter += 2
|
||||
if position:
|
||||
self.info.append(
|
||||
{
|
||||
"shader": shader,
|
||||
"args": {
|
||||
"position": position,
|
||||
"sin_quad_lin_forces": sin_quad_lin,
|
||||
"coord": coords_for_shader,
|
||||
},
|
||||
"indices": indices,
|
||||
"uniforms": [["color", color_axis], ["spacing", 0.2], ["maxload", maxforce]],
|
||||
}
|
||||
)
|
||||
position = []
|
||||
sin_quad_lin = []
|
||||
coords_for_shader = []
|
||||
indices = []
|
||||
for info in self.info:
|
||||
info["uniforms"][2][1] = maxforce
|
||||
|
||||
def process_total_linear_loads(
|
||||
self,
|
||||
activity_list: list[tuple[ifcopenshell.entity_instance, float]],
|
||||
element_rotation_matrix: np.ndarray,
|
||||
member_length: float,
|
||||
) -> ProcessedLoad:
|
||||
"""returns a dict with total values for applied loads in each direction
|
||||
along with the maximum value for the loads in the member and the discrete loads applied
|
||||
|
||||
"""
|
||||
loads_dict = self.parse_linear_loads_to_dict(activity_list, element_rotation_matrix)
|
||||
const = loads_dict["constant force"]
|
||||
quad = loads_dict["quadratic force"]
|
||||
sinus = loads_dict["sinus force"]
|
||||
loads = loads_dict["linear load configuration"]
|
||||
unique_list = self.getuniquepositionlist(loads)
|
||||
final_list = []
|
||||
distributed_loads = None
|
||||
max_load = 0
|
||||
for pos in unique_list:
|
||||
value = self.get_before_and_after(pos, loads)
|
||||
if value["before"] == value["after"]:
|
||||
final_list.append([pos] + value["before"])
|
||||
else:
|
||||
final_list.append([pos] + value["before"])
|
||||
final_list.append([pos] + value["after"])
|
||||
|
||||
if len(final_list) == 0 and any(const + quad + sinus):
|
||||
final_list.append([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])
|
||||
final_list.append([member_length, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])
|
||||
|
||||
elif len(final_list) > 0:
|
||||
if final_list[0][0] and any(
|
||||
const + quad + sinus
|
||||
): # if first item location is not 0 append an item at the zero
|
||||
final_list = [[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]] + final_list
|
||||
else:
|
||||
del final_list[0]
|
||||
if abs(final_list[-1][0] - member_length) > 0.01 and any(const + quad + sinus):
|
||||
final_list.append([member_length, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])
|
||||
else:
|
||||
del final_list[-1]
|
||||
if len(final_list) > 0:
|
||||
array = np.array(final_list) # 7xn -> ["pos","fx","fy","fz","mx","my","mz"]
|
||||
keys = ["fx", "fy", "fz", "mx", "my", "mz"]
|
||||
polyline = []
|
||||
distributed_loads = {
|
||||
"fx": {"constant": 0, "quadratic": 0, "sinus": 0, "polyline": []},
|
||||
"fy": {"constant": 0, "quadratic": 0, "sinus": 0, "polyline": []},
|
||||
"fz": {"constant": 0, "quadratic": 0, "sinus": 0, "polyline": []},
|
||||
"mx": {"constant": 0, "quadratic": 0, "sinus": 0, "polyline": []},
|
||||
"my": {"constant": 0, "quadratic": 0, "sinus": 0, "polyline": []},
|
||||
"mz": {"constant": 0, "quadratic": 0, "sinus": 0, "polyline": []},
|
||||
}
|
||||
max_load = 0
|
||||
for component, key in enumerate(keys):
|
||||
if any([sinus[component], quad[component], const[component]]) or any(
|
||||
item for item in array[:, component + 1]
|
||||
):
|
||||
|
||||
for currentitem in final_list:
|
||||
polyline.append([currentitem[0], currentitem[component + 1]])
|
||||
max_load = max(
|
||||
max_load,
|
||||
abs(sinus[component] + quad[component] + const[component] + currentitem[component + 1]),
|
||||
)
|
||||
inner_dict = distributed_loads[key]
|
||||
inner_dict["constant"] = const[component]
|
||||
inner_dict["quadratic"] = quad[component]
|
||||
inner_dict["sinus"] = sinus[component]
|
||||
inner_dict["polyline"] = polyline
|
||||
distributed_loads[key] = inner_dict
|
||||
|
||||
return {
|
||||
"linear loads": distributed_loads,
|
||||
"max linear load": max_load,
|
||||
"discrete loads": loads_dict["point load configuration"],
|
||||
}
|
||||
|
||||
def getuniquepositionlist(self, load_config_list: list[list[LoadConfigItem]]) -> list[float]:
|
||||
"""return an ordereded list of unique locations based on the load configuration list
|
||||
ex: load_config_list = [[{"pos":1.0,...},{"pos":3.0,...}],
|
||||
[{"pos":2.0,...},{"pos":3.0,...}],
|
||||
[{"pos":1.5,...},{"pos":2.5,...}]]
|
||||
return = [1.0, 1.5, 2.0, 2.5, 3.0]
|
||||
"""
|
||||
unique = []
|
||||
for config in load_config_list:
|
||||
for info in config:
|
||||
if info["pos"] in unique:
|
||||
continue
|
||||
unique.append(info["pos"])
|
||||
unique.sort()
|
||||
return unique
|
||||
|
||||
def interp1d(self, l1: list[float], l2: list[float], pos: float) -> float:
|
||||
"""1d linear interpolation for the vector components"""
|
||||
fac = (l2[1] - l1[1]) / (l2[0] - l1[0])
|
||||
v = l1[1] + fac * (pos - l1[0])
|
||||
return v
|
||||
|
||||
def interpolate(self, pos: float, loadinfo: list[LoadConfigItem], start: int, end: int, key: str) -> np.ndarray:
|
||||
"""interpolate the result vectors between load poits"""
|
||||
result = np.zeros(6)
|
||||
for i in range(6):
|
||||
value1 = [loadinfo[start]["pos"], loadinfo[start][key][i]] # [position, force_component]
|
||||
value2 = [loadinfo[end]["pos"], loadinfo[end][key][i]] # [position, force_component]
|
||||
result[i] = self.interp1d(value1, value2, pos) # interpolated [position, force_component]
|
||||
return result
|
||||
|
||||
def get_before_and_after(self, pos: float, load_config_list: list[list[LoadConfigItem]]) -> dict[str, list[float]]:
|
||||
"""get total values for forces and moments before and after the position
|
||||
example:
|
||||
pos = 2.0
|
||||
load_config_list = [[{"pos":1.0, "descr":"start, "load values":[1,0,0,0,0,0]},
|
||||
{"pos":3.0, "descr":"end, "load values":[3,0,0,0,0,0]}],
|
||||
[{"pos":2.0, "descr":"start, "load values":[1,0,0,0,0,0]},
|
||||
{"pos":3.0, "descr":"end, "load values":[1,0,0,0,0,0]}],
|
||||
[{"pos":1.5, "descr":"start, "load values":[1,0,0,0,0,0]},
|
||||
{"pos":2.5, "descr":"end, "load values":[1,0,0,0,0,0]}],
|
||||
return = {
|
||||
"before": [3,0,0,0,0,0], ->(fx, fy, fz, mx, my, mz)
|
||||
" after": [4,0,0,0,0,0] ->(fx, fy, fz, mx, my, mz)
|
||||
}
|
||||
"""
|
||||
load_before = np.zeros(6)
|
||||
load_after = np.zeros(6)
|
||||
|
||||
for config in load_config_list:
|
||||
if pos < config[0]["pos"] or pos > config[-1]["pos"]:
|
||||
continue
|
||||
start = 0
|
||||
end = len(config) - 1
|
||||
while end - start > 0:
|
||||
if pos < config[start]["pos"] or pos > config[end]["pos"]:
|
||||
break
|
||||
if config[start]["pos"] == pos:
|
||||
if config[start]["descr"] in ["start", "middle"]:
|
||||
load_after += config[start]["load values"]
|
||||
|
||||
elif config[start]["descr"] in ["end", "middle"]:
|
||||
load_before += config[start]["load values"]
|
||||
|
||||
elif config[end]["pos"] == pos:
|
||||
if config[end]["descr"] in ["start", "middle"]:
|
||||
load_after += config[end]["load values"]
|
||||
|
||||
elif config[end]["descr"] in ["end", "middle"]:
|
||||
load_before += config[end]["load values"]
|
||||
|
||||
elif end - start == 1:
|
||||
load_before += self.interpolate(pos, config, start, end, "load values")
|
||||
load_after += self.interpolate(pos, config, start, end, "load values")
|
||||
start += 1
|
||||
end -= 1
|
||||
return_value = {"before": load_before.tolist(), "after": load_after.tolist()}
|
||||
return return_value
|
||||
|
||||
def parse_linear_loads_to_dict(
|
||||
self, activity_list: list[tuple[ifcopenshell.entity_instance, float]], element_rotation_matrix: np.ndarray
|
||||
) -> ParsedLoad:
|
||||
"""
|
||||
get load list
|
||||
activity_list: list of IfcStructuralCurveAction or IfcStructuralCurveReaction
|
||||
applied in the structural curve member
|
||||
global_to_local: transformation matrix from global coordinates to local coordinetes
|
||||
return: dict{
|
||||
"constant force": (fx,fy,fz,mx,my,mz), -> sum of linear loads applied with
|
||||
constant distribution
|
||||
"quadratic force": (fx,fy,fz,mx,my,mz), -> sum of linear loads applied with
|
||||
quadratic distribution
|
||||
"sinus force": (fx,fy,fz,mx,my,mz), -> sum of linear loads applied with
|
||||
sinus distribution
|
||||
"linear load configuration": list -> list of load configurations for linear
|
||||
and polyline distributions of linear loads
|
||||
}
|
||||
description of "linear load configuration":
|
||||
list[ -> one item (list)for each IfcStructuralCurveAction applied in the member
|
||||
with IfcStructuralLoadConfiguration as the applied load
|
||||
list[ -> one item (dict) for each item found in the
|
||||
Locations attribute of IfcLoadConfiguration
|
||||
dict{
|
||||
"pos": float, -> local position along curve length
|
||||
"descr": string, -> describe if the item is at the start, middle or end of the list
|
||||
"load values": Array, -> linear force applied at that point
|
||||
}
|
||||
]
|
||||
]
|
||||
"""
|
||||
constant = np.zeros(6)
|
||||
quadratic = np.zeros(6)
|
||||
sinus = np.zeros(6)
|
||||
linear_load_configurations = []
|
||||
point_load_configurations = []
|
||||
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get(), "LENGTHUNIT")
|
||||
|
||||
def get_load_values(load, transform_matrix, factor=1.0):
|
||||
result = np.zeros(6)
|
||||
keys = ["LinearForceX", "LinearForceY", "LinearForceZ", "LinearMomentX", "LinearMomentY", "LinearMomentZ"]
|
||||
for i, key in enumerate(keys):
|
||||
value = 0 if getattr(load, key, 0) is None else getattr(load, key, 0)
|
||||
result[i] += value * factor
|
||||
return transform_matrix @ result
|
||||
|
||||
for item in activity_list:
|
||||
activity = item[0]
|
||||
factor = item[1]
|
||||
load = activity.AppliedLoad
|
||||
|
||||
transform_3by3 = self.get_activity_transform_matrix(activity, element_rotation_matrix)
|
||||
transform_6by6 = np.zeros((6, 6))
|
||||
transform_6by6[0:3, 0:3] = transform_3by3
|
||||
transform_6by6[3:6, 3:6] = transform_3by3
|
||||
|
||||
# values for linear loads
|
||||
if load.is_a("IfcStructuralLoadConfiguration"):
|
||||
locations = getattr(load, "Locations", [])
|
||||
values = [l for l in getattr(load, "Values", None) if l.is_a() == "IfcStructuralLoadLinearForce"]
|
||||
config_list = []
|
||||
for i, l in enumerate(values):
|
||||
load_values = get_load_values(l, transform_6by6, factor)
|
||||
if i == 0:
|
||||
descr = "start"
|
||||
elif i == len(values) - 1:
|
||||
descr = "end"
|
||||
else:
|
||||
descr = "middle"
|
||||
config_list.append(
|
||||
{"pos": locations[i][0] * unit_scale, "descr": descr, "load values": load_values}
|
||||
)
|
||||
linear_load_configurations.append(config_list)
|
||||
|
||||
# load configurations with point loads
|
||||
values = [l for l in getattr(load, "Values", None) if l.is_a() == "IfcStructuralLoadSingleForce"]
|
||||
attr_list = ["ForceX", "ForceY", "ForceZ", "MomentX", "MomentY", "MomentZ"]
|
||||
config_list = []
|
||||
for i, val in enumerate(values):
|
||||
result_list = [0, 0, 0, 0, 0, 0]
|
||||
for j, attr in enumerate(attr_list):
|
||||
value = 0 if getattr(val, attr, 0) is None else getattr(val, attr, 0)
|
||||
result_list[j] += value * factor
|
||||
config_list.append({"pos": locations[i][0] * unit_scale, "values": result_list})
|
||||
point_load_configurations.append(config_list)
|
||||
|
||||
else:
|
||||
load_values = get_load_values(load, transform_6by6, factor)
|
||||
if "CONST" == getattr(activity, "PredefinedType", None) or activity.is_a("IfcStructuralLinearAction"):
|
||||
constant += load_values
|
||||
elif "PARABOLA" == getattr(activity, "PredefinedType", None):
|
||||
quadratic += load_values
|
||||
elif "SINUS" == getattr(activity, "PredefinedType", None):
|
||||
sinus += load_values
|
||||
return_value = {
|
||||
"constant force": constant.tolist(),
|
||||
"quadratic force": quadratic.tolist(),
|
||||
"sinus force": sinus.tolist(),
|
||||
"linear load configuration": linear_load_configurations,
|
||||
"point load configuration": point_load_configurations,
|
||||
}
|
||||
return return_value
|
||||
@@ -27,6 +27,45 @@ import bonsai.tool as tool
|
||||
from math import degrees
|
||||
from mathutils import Vector, Matrix
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
from bonsai.bim.module.structural.decorator import LoadsDecorator
|
||||
|
||||
|
||||
class ShowLoads(bpy.types.Operator):
|
||||
"""Draw decorations to show strucutural actions in 3d view"""
|
||||
|
||||
bl_idname = "bim.show_loads"
|
||||
bl_label = "Show loads in 3D View"
|
||||
|
||||
def modal(self, context, event):
|
||||
if event.type == "F5":
|
||||
LoadsDecorator.update()
|
||||
for area in context.screen.areas:
|
||||
if area.type == "VIEW_3D":
|
||||
area.tag_redraw()
|
||||
if event.type == "ESC":
|
||||
LoadsDecorator.uninstall()
|
||||
for area in context.screen.areas:
|
||||
if area.type == "VIEW_3D":
|
||||
area.tag_redraw()
|
||||
return {"FINISHED"}
|
||||
return {"PASS_THROUGH"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
collection = bpy.data.collections["IfcStructuralItem"]
|
||||
collection.hide_viewport = False
|
||||
context.window.cursor_modal_set("WAIT")
|
||||
try:
|
||||
LoadsDecorator.install(context)
|
||||
except Exception as exc:
|
||||
context.window.cursor_modal_restore()
|
||||
raise exc
|
||||
context.window.cursor_modal_restore()
|
||||
context.window_manager.modal_handler_add(self)
|
||||
for area in context.screen.areas:
|
||||
if area.type == "VIEW_3D":
|
||||
area.tag_redraw()
|
||||
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class AddStructuralMemberConnection(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
@@ -21,7 +21,12 @@ import bpy
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
from bonsai.bim.prop import StrProperty, Attribute
|
||||
from bonsai.bim.module.structural.data import StructuralLoadCasesData, StructuralLoadsData, BoundaryConditionsData
|
||||
from bonsai.bim.module.structural.data import (
|
||||
StructuralLoadCasesData,
|
||||
StructuralLoadsData,
|
||||
BoundaryConditionsData,
|
||||
LoadGroupDecorationData,
|
||||
)
|
||||
from bpy.types import PropertyGroup
|
||||
from bpy.props import (
|
||||
PointerProperty,
|
||||
@@ -35,6 +40,16 @@ from bpy.props import (
|
||||
)
|
||||
|
||||
|
||||
def get_load_groups_to_show(self, context):
|
||||
if not LoadGroupDecorationData.is_loaded:
|
||||
LoadGroupDecorationData.load()
|
||||
return LoadGroupDecorationData.data["load groups to show"]
|
||||
|
||||
|
||||
def update_activity_type(self, context):
|
||||
LoadGroupDecorationData.is_loaded = False
|
||||
|
||||
|
||||
def get_applicable_structural_load_types(self, context):
|
||||
if not StructuralLoadCasesData.is_loaded:
|
||||
StructuralLoadCasesData.load()
|
||||
@@ -152,6 +167,27 @@ class BIMStructuralProperties(PropertyGroup):
|
||||
boundary_condition_attributes: CollectionProperty(name="Boundary Condition Attributes", type=Attribute)
|
||||
filtered_boundary_conditions: BoolProperty(name="Filtered Boundary Conditions", default=False)
|
||||
|
||||
show_loads: BoolProperty(name="Show Loads", default=False)
|
||||
update_load_repr: BoolProperty(name="Update Load Representation", default=False)
|
||||
enable_repr_auto_update: BoolProperty(name="Auto Update", default=False)
|
||||
reference_frame: EnumProperty(
|
||||
items=[
|
||||
("GLOBAL_COORDS", "Global", "Show loads in global reference frame"),
|
||||
("LOCAL_COORDS", "Local", "Show loads in local reference frame"),
|
||||
],
|
||||
name="Reference Frame",
|
||||
)
|
||||
activity_type: EnumProperty(
|
||||
items=[
|
||||
("Action", "Actions", "Show actions loads"),
|
||||
("External Reaction", "External Reactions", "Show reactions on boundary conditions"),
|
||||
("Internal Reactions", "Internal Reactions", "Show internal reactions on members"),
|
||||
],
|
||||
name="Activity Type",
|
||||
update=update_activity_type,
|
||||
)
|
||||
load_group_to_show: EnumProperty(items=get_load_groups_to_show, name="Load Groups")
|
||||
|
||||
|
||||
class BIMObjectStructuralProperties(PropertyGroup):
|
||||
boundary_condition_attributes: CollectionProperty(name="Boundary Condition Attributes", type=Attribute)
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of Bonsai.
|
||||
#
|
||||
# Bonsai is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Bonsai is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import gpu
|
||||
from typing import Literal
|
||||
|
||||
|
||||
class DecorationShader:
|
||||
"shader for the load decorations"
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def new(
|
||||
self,
|
||||
pattern: Literal[
|
||||
"PERPENDICULAR DISTRIBUTED FORCE",
|
||||
"PARALLEL DISTRIBUTED FORCE",
|
||||
"DISTRIBUTED MOMENT",
|
||||
"SINGLE FORCE",
|
||||
"SINGLE MOMENT",
|
||||
"PLANAR LOAD",
|
||||
],
|
||||
) -> gpu.types.GPUShader:
|
||||
"""pattern: string description of the desired shader
|
||||
Possible values
|
||||
PERPENDICULAR DISTRIBUTED FORCE: pattern for distributed force
|
||||
perpendicular to the curve member axis
|
||||
PARALLEL DISTRIBUTED FORCE: pattern for distributed force
|
||||
along the curve member axis
|
||||
DISTRIBUTED MOMENT: pattern for distributed moment in curve members
|
||||
SINGLE FORCE: pattern for single forces
|
||||
SINGLE MOMENT: pattern for single moments
|
||||
PLANAR LOAD: pattern for planar loads
|
||||
"""
|
||||
valid_patterns = {
|
||||
"PERPENDICULAR DISTRIBUTED FORCE",
|
||||
"PARALLEL DISTRIBUTED FORCE",
|
||||
"DISTRIBUTED MOMENT",
|
||||
"SINGLE FORCE",
|
||||
"SINGLE MOMENT",
|
||||
"PLANAR LOAD",
|
||||
}
|
||||
if pattern not in valid_patterns:
|
||||
raise ValueError(
|
||||
"""pattern must be one of:
|
||||
PERPENDICULAR DISTRIBUTED FORCE
|
||||
PARALLEL DISTRIBUTED FORCE,
|
||||
DISTRIBUTED MOMENT,
|
||||
SINGLE FORCE,
|
||||
SINGLE MOMENT,
|
||||
PLANAR LOAD"""
|
||||
)
|
||||
if "DISTRIBUTED" in pattern.upper():
|
||||
shader = self.get_linear_shader(pattern)
|
||||
return shader
|
||||
elif "SINGLE" in pattern.upper():
|
||||
shader = self.get_point_shader(pattern)
|
||||
return shader
|
||||
elif "PLANAR" in pattern.upper():
|
||||
shader = self.get_planar_shader()
|
||||
return shader
|
||||
|
||||
def get_linear_shader(
|
||||
self, pattern: Literal["PERPENDICULAR DISTRIBUTED FORCE", "PARALLEL DISTRIBUTED FORCE", "DISTRIBUTED MOMENT"]
|
||||
) -> gpu.types.GPUShader:
|
||||
"""pattern: type of pattern
|
||||
PERPENDICULAR DISTRIBUTED FORCE
|
||||
PARALLEL DISTRIBUTED FORCE,
|
||||
DISTRIBUTED MOMENT,
|
||||
"""
|
||||
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface")
|
||||
vert_out.smooth("VEC3", "forces")
|
||||
vert_out.smooth("VEC3", "co")
|
||||
|
||||
shader_info = gpu.types.GPUShaderCreateInfo()
|
||||
shader_info.push_constant("MAT4", "viewProjectionMatrix")
|
||||
shader_info.push_constant("VEC4", "color")
|
||||
shader_info.push_constant("FLOAT", "spacing")
|
||||
shader_info.push_constant("FLOAT", "maxload")
|
||||
|
||||
shader_info.vertex_in(0, "VEC3", "position")
|
||||
shader_info.vertex_in(1, "VEC3", "sin_quad_lin_forces")
|
||||
shader_info.vertex_in(2, "VEC3", "coord")
|
||||
|
||||
shader_info.vertex_out(vert_out)
|
||||
shader_info.fragment_out(0, "VEC4", "FragColor")
|
||||
|
||||
shader_info.vertex_source(
|
||||
"void main()"
|
||||
"{"
|
||||
" gl_Position = viewProjectionMatrix * vec4(position, 1.0f);"
|
||||
" co = coord;"
|
||||
" forces = sin_quad_lin_forces;"
|
||||
"}"
|
||||
)
|
||||
|
||||
if pattern == "PERPENDICULAR DISTRIBUTED FORCE":
|
||||
shader_info.fragment_source(
|
||||
"void main()"
|
||||
"{"
|
||||
"float x = co.x;"
|
||||
"float y = co.y;"
|
||||
"float abs_y = abs(y);"
|
||||
"float a = abs(mod(x,spacing)-0.5*spacing)*5.0;"
|
||||
"float b = step(a,abs_y)*(step(abs_y,1.2*spacing));"
|
||||
"float c = step(0.8*spacing,mod(x+0.4*spacing,spacing))*(step(1.2*spacing,abs_y));"
|
||||
"float sinvalue = forces.x;"
|
||||
"float quadraticvalue = forces.y;"
|
||||
"float linearvalue = forces.z;"
|
||||
"x = co.x/co.z;"
|
||||
"float f = (sin(x*3.1416)*sinvalue"
|
||||
"+(-4.*x*x+4.*x)*quadraticvalue"
|
||||
"+linearvalue)/maxload;"
|
||||
"float mask = step(0.,y)*step(y,f)+step(y,0.)*step(f,y);"
|
||||
"float top = step(abs(y-f),0.2*1.2*spacing);"
|
||||
"float d = clamp(top+b+c,0.0,0.9)*mask;"
|
||||
"if (d == 0.0) discard;"
|
||||
"FragColor = vec4(color.xyz,d*color.w);"
|
||||
"}"
|
||||
)
|
||||
|
||||
elif pattern == "PARALLEL DISTRIBUTED FORCE":
|
||||
shader_info.fragment_source(
|
||||
"void main()"
|
||||
"{"
|
||||
"float y = co.y;"
|
||||
"float x = step(0.,y)*(co.z-co.x)+step(y,0.)*(co.x);"
|
||||
"float abs_y = abs(y);"
|
||||
"float a = abs(mod(abs_y,spacing)-0.5*spacing)*5.0;"
|
||||
"float a2 = mod(x,3.0*spacing);"
|
||||
"float b = step(a,a2)*step(a2,1.2*spacing);"
|
||||
"float c = step(0.8*spacing,mod(abs_y+0.4*spacing,spacing))"
|
||||
"*(step(1.2*spacing,a2))*step(a2,2.5*spacing);"
|
||||
"float sinvalue = forces.x;"
|
||||
"float quadraticvalue = forces.y;"
|
||||
"float linearvalue = forces.z;"
|
||||
"x = co.x/co.z;"
|
||||
"float f = (sin(x*3.1416)*sinvalue"
|
||||
"+(-4.*x*x+4.*x)*quadraticvalue"
|
||||
"+linearvalue)/maxload;"
|
||||
"float mask = step(0.,y)*step(y,f)+step(y,0.)*step(f,y);"
|
||||
"float top = step(abs(y-f),0.2*1.2*spacing);"
|
||||
"float d = clamp(top+b+c,0.0,0.9)*mask;"
|
||||
"if (d == 0.0) discard;"
|
||||
"FragColor = vec4(color.xyz,d*color.w);"
|
||||
"}"
|
||||
)
|
||||
|
||||
elif pattern == "DISTRIBUTED MOMENT":
|
||||
shader_info.fragment_source(
|
||||
"void main()"
|
||||
"{"
|
||||
"float x = step(co.y,0.)*(co.x)+step(0.,co.y)*(co.z-co.x);"
|
||||
"float y = step(co.y,-0.00001)*(co.y)+step(0.,co.y)*(0.-co.y);"
|
||||
"x = mod((0.5/spacing)*x,1.4)-0.7;"
|
||||
"y = mod((0.5/spacing)*y,1.4)-0.7;"
|
||||
"float abs_y = abs(y);"
|
||||
"vec2 st = vec2(1.9*x,y);"
|
||||
"vec2 orig = vec2(0.,0.);"
|
||||
"float circ = step(distance(st,orig),0.33)*step(0.27,distance(st,orig));"
|
||||
"float tri_mask = step(st.y,st.x)+step(-st.x,st.y);"
|
||||
"float circ_arrow = step(st.x,4.0*st.y-0.75)*step(0.25*st.y-0.34,st.x)*(1.-tri_mask);"
|
||||
"float circmask = step(distance(st,orig),0.1)+step(0.5,distance(st,orig))+step(st.x,0.);"
|
||||
"float body = step(-0.03,st.y)*step(st.y,0.03)*step(-0.3,x)*step(x,0.576);"
|
||||
"float body_arrow = step(-0.5+3.*st.y,x)*step(-0.5-3.*st.y,x)*step(x,-0.3);"
|
||||
"float d = clamp(circmask*(body+body_arrow)+circ_arrow+circ*tri_mask,0.,1.);"
|
||||
"float sinvalue = forces.x;"
|
||||
"float quadraticvalue = forces.y;"
|
||||
"float linearvalue = forces.z;"
|
||||
"x = co.x/co.z;"
|
||||
"float f = (sin(x*3.1416)*sinvalue"
|
||||
"+(-4.*x*x+4.*x)*quadraticvalue"
|
||||
"+linearvalue)/maxload;"
|
||||
"float mask = step(0.,co.y)*step(co.y,f)+step(co.y,0.)*step(f,co.y);"
|
||||
"float top = step(abs(co.y-f),0.2*1.2*spacing);"
|
||||
"d = clamp(top+d,0.0,0.9)*mask;"
|
||||
"if (d == 0.0) discard;"
|
||||
"FragColor = vec4(color.xyz,d*color.w);"
|
||||
"}"
|
||||
)
|
||||
|
||||
shader = gpu.shader.create_from_info(shader_info)
|
||||
del vert_out
|
||||
del shader_info
|
||||
return shader
|
||||
|
||||
def get_point_shader(self, pattern: Literal["SINGLE FORCE", "SINGLE MOMENT"]) -> gpu.types.GPUShader:
|
||||
"""param: pattern: type of pattern
|
||||
SINGLE FORCE,
|
||||
SINGLE MOMENT"""
|
||||
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface")
|
||||
vert_out.smooth("VEC3", "co")
|
||||
|
||||
shader_info = gpu.types.GPUShaderCreateInfo()
|
||||
shader_info.push_constant("MAT4", "viewProjectionMatrix")
|
||||
shader_info.push_constant("VEC4", "color")
|
||||
shader_info.push_constant("FLOAT", "spacing")
|
||||
|
||||
shader_info.vertex_in(0, "VEC3", "position")
|
||||
shader_info.vertex_in(1, "VEC3", "coord")
|
||||
|
||||
shader_info.vertex_out(vert_out)
|
||||
shader_info.fragment_out(0, "VEC4", "FragColor")
|
||||
|
||||
shader_info.vertex_source(
|
||||
"void main()" "{" " gl_Position = viewProjectionMatrix * vec4(position, 1.0f);" " co = coord;" "}"
|
||||
)
|
||||
|
||||
if pattern == "SINGLE FORCE":
|
||||
shader_info.fragment_source(
|
||||
"void main()"
|
||||
"{"
|
||||
"float body = step(abs(co.x),0.2*spacing)*step(2.*spacing,co.y);"
|
||||
"float arrow = step(3.5*abs(co.x)+0.02,abs(co.y))*step(co.y,2.*spacing)*step(0.,co.y);"
|
||||
"float d = clamp(body+arrow,0.0,0.5);"
|
||||
"if (d == 0.0) discard;"
|
||||
"FragColor = vec4(color.xyz,d*color.w);"
|
||||
"}"
|
||||
)
|
||||
|
||||
elif pattern == "SINGLE MOMENT":
|
||||
shader_info.fragment_source(
|
||||
"void main()"
|
||||
"{"
|
||||
"float circ = step(distance(co.xy,vec2(0.,0.)),0.33)*step(0.27,distance(co.xy,vec2(0.,0.)));"
|
||||
"float mask = step(co.y,co.x)+step(-co.x,co.y);"
|
||||
"float circ_arrow = step(co.x,4.0*co.y-0.75)*step(0.25*co.y-0.34,co.x)*(1.-mask);"
|
||||
"float d = clamp(circ_arrow+circ*mask,0.0,0.5);"
|
||||
"if (d == 0.0) discard;"
|
||||
"FragColor = vec4(color.xyz,d*color.w);"
|
||||
"}"
|
||||
)
|
||||
|
||||
shader = gpu.shader.create_from_info(shader_info)
|
||||
del vert_out
|
||||
del shader_info
|
||||
return shader
|
||||
|
||||
def get_planar_shader(self) -> gpu.types.GPUShader:
|
||||
"""shader for planar loads"""
|
||||
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface")
|
||||
vert_out.smooth("VEC3", "co")
|
||||
|
||||
shader_info = gpu.types.GPUShaderCreateInfo()
|
||||
shader_info.push_constant("MAT4", "viewProjectionMatrix")
|
||||
shader_info.push_constant("VEC4", "color")
|
||||
shader_info.push_constant("FLOAT", "spacing")
|
||||
|
||||
shader_info.vertex_in(0, "VEC3", "position")
|
||||
shader_info.vertex_in(1, "VEC3", "coord")
|
||||
|
||||
shader_info.vertex_out(vert_out)
|
||||
shader_info.fragment_out(0, "VEC4", "FragColor")
|
||||
|
||||
shader_info.vertex_source(
|
||||
"void main()" "{" " gl_Position = viewProjectionMatrix * vec4(position, 1.0f);" " co = coord;" "}"
|
||||
)
|
||||
|
||||
shader_info.fragment_source(
|
||||
"void main()"
|
||||
"{"
|
||||
"float x = co.x;"
|
||||
"float y = co.y;"
|
||||
"float abs_y = abs(y);"
|
||||
"float a = abs(mod(x,spacing)-0.5*spacing)*5.0;"
|
||||
"float b = step(a,abs_y)*(step(abs_y,1.2*spacing));"
|
||||
"float c = step(0.8*spacing,mod(x+0.4*spacing,spacing))*(step(1.2*spacing,abs_y));"
|
||||
"float mask = step(0.,y)*step(y,0.98)+step(y,0.)*step(0.98,y);"
|
||||
"float top = step(abs(y-0.98),0.2*1.2*spacing);"
|
||||
"float d = clamp(0.2*y+(top+b+c)*mask,0.0,0.4);"
|
||||
"FragColor = vec4(color.xyz,d*color.w);"
|
||||
"}"
|
||||
)
|
||||
|
||||
shader = gpu.shader.create_from_info(shader_info)
|
||||
del vert_out
|
||||
del shader_info
|
||||
return shader
|
||||
@@ -445,6 +445,37 @@ class BIM_UL_structural_activities(UIList):
|
||||
row.label(text=item.applied_load_class)
|
||||
|
||||
|
||||
class BIM_PT_show_structural_activities(Panel):
|
||||
bl_label = "Show Loads"
|
||||
bl_idname = "BIM_PT_show_structural_activities"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_tab_structural"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
file = IfcStore.get_file()
|
||||
return file and hasattr(file, "schema") and file.schema != "IFC2X3"
|
||||
|
||||
def draw(self, context):
|
||||
|
||||
self.props = context.scene.BIMStructuralProperties
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.operator(
|
||||
"bim.show_loads",
|
||||
text="Show loads",
|
||||
icon="HIDE_OFF",
|
||||
)
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.props, "reference_frame")
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.props, "activity_type")
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.props, "load_group_to_show")
|
||||
|
||||
|
||||
class BIM_PT_structural_loads(Panel):
|
||||
bl_label = "Structural Loads"
|
||||
bl_idname = "BIM_PT_structural_loads"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user