Compare commits
397 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 535e974cc3 | |||
| 629aff1e50 | |||
| 628b6830db | |||
| 1b2ab4e3f0 | |||
| f97752c368 | |||
| d2d5db9bf6 | |||
| a8554d9e56 | |||
| 11a38af901 | |||
| 27a4976b9d | |||
| eb54f5a0cb | |||
| a4a6dce6de | |||
| 4184e25c68 | |||
| d98b0766c2 | |||
| b8990dd0a4 | |||
| 2198201e0b | |||
| 53e58848f3 | |||
| 9c367d1702 | |||
| db18a81035 | |||
| d1d6b00f92 | |||
| 32fc147a7c | |||
| d98054234c | |||
| 88ba6514e8 | |||
| 894a688bd8 | |||
| c69ead4893 | |||
| e68fdb2db7 | |||
| ae9372c3d3 | |||
| 0365c6d43b | |||
| 18b6969221 | |||
| a1935acb3d | |||
| 9548c5e657 | |||
| 762299124f | |||
| f682bf5688 | |||
| f7ed3a8727 | |||
| c124506e41 | |||
| 273cdc6ea8 | |||
| 0ef6ea6bd4 | |||
| 607d9a06ec | |||
| 1b472ca5f2 | |||
| 18568d6ba7 | |||
| bfb59dd387 | |||
| 38cf88d5d2 | |||
| 7ca6f9d7a1 | |||
| 5874508263 | |||
| 954de793dd | |||
| df74d5efbe | |||
| f7b9a7bfb5 | |||
| 63eaa88d3d | |||
| f518521ea1 | |||
| 5e1ee22bcd | |||
| ca09ff2ef0 | |||
| 8643aff800 | |||
| 2e88993654 | |||
| b77849d279 | |||
| c7e288466f | |||
| bb48d4aac1 | |||
| 2b99cc1e58 | |||
| ca286f8d19 | |||
| 73a3cc2cc5 | |||
| 169709ed04 | |||
| 2e746242ce | |||
| 951b8e2b15 | |||
| ede3edcf1e | |||
| 184c9f87b2 | |||
| 0793a4602d | |||
| bb285c47dc | |||
| 4cb6fc05d6 | |||
| a302e5db64 | |||
| ff21749da4 | |||
| 23b5da7670 | |||
| c31e479f47 | |||
| 260eb31a69 | |||
| 1fb6dac6fb | |||
| 34d947c51f | |||
| 23f7a6d5a6 | |||
| b15926c20a | |||
| 0a7ef80ca7 | |||
| be08406cc1 | |||
| cc9777b1e8 | |||
| 539b3a243b | |||
| 41e58a3d46 | |||
| 8c302ed85c | |||
| dc4dc46b62 | |||
| a07fa5a2c2 | |||
| ad9dbce003 | |||
| d0526d0ae5 | |||
| 75e9c3265d | |||
| b37705ab6e | |||
| d9d5f86a6e | |||
| 1f3ad95c00 | |||
| 9e360f7efa | |||
| 64fd6a1f52 | |||
| ba04df4ce3 | |||
| cd27a026fd | |||
| baedf71106 | |||
| 4f6c95b119 | |||
| b1761ac393 | |||
| fcdf3e583f | |||
| 2001119dff | |||
| 60a9d96ac8 | |||
| 6bd177d250 | |||
| 1ece84e31d | |||
| 40ea9457a8 | |||
| e95d91a91e | |||
| c8fb92175f | |||
| a6fb4cbfbf | |||
| 30bb6d05c5 | |||
| 9422f804fb | |||
| 415a7746be | |||
| ee903208dc | |||
| f89e834fea | |||
| e8b4335741 | |||
| 83010f0542 | |||
| 04e5f5036c | |||
| 5d86d38a14 | |||
| a82db1f622 | |||
| fb618d536b | |||
| 23f96779fa | |||
| 7b67854654 | |||
| 2e7f941465 | |||
| 1c1706fde3 | |||
| ca77a895e0 | |||
| 0f447f964e | |||
| 2c31fc6b25 | |||
| 0ba79dc105 | |||
| 52c686b656 | |||
| 64d98f1295 | |||
| ceb2c4075c | |||
| 166e7d52ec | |||
| 644c2652ea | |||
| 63b201fd95 | |||
| d70616056c | |||
| a65e376886 | |||
| a985c70f0c | |||
| 0d09e02e7e | |||
| 23e3551f27 | |||
| 885bca0865 | |||
| 0f874dad51 | |||
| 107873cf6e | |||
| 4a8f5af7ba | |||
| 83084aeb7b | |||
| 51640ccc28 | |||
| aefbc134ea | |||
| 29e1d70334 | |||
| 9118e529e6 | |||
| 8ee0a1611d | |||
| 2af4388f40 | |||
| 93a2d18ea6 | |||
| fc49859ea4 | |||
| c0ac9cb380 | |||
| d5cc6c3c8e | |||
| 9b22b5000f | |||
| 3b69a4cc78 | |||
| adf54efcc5 | |||
| f6962eb58e | |||
| 11601d6532 | |||
| be2131d62c | |||
| 3ffbf46ac2 | |||
| 3943107eec | |||
| 94e3f75341 | |||
| 8bc8b950e7 | |||
| 05a95ac666 | |||
| f016c803e4 | |||
| bac0dd37fa | |||
| 75a37cf737 | |||
| bb7e9c4610 | |||
| ee2f18349f | |||
| 8829a3cbe9 | |||
| b210e39c7e | |||
| 4d4500f061 | |||
| 79860a0c04 | |||
| d53fcdc616 | |||
| 5e6a094ac6 | |||
| 757bf4e916 | |||
| 7bb57b3ab9 | |||
| cb22b5804d | |||
| bfbced3859 | |||
| c090ee9b83 | |||
| b6caf069d0 | |||
| 5b4449eea0 | |||
| 7dc17a3653 | |||
| 598bf799de | |||
| 4774a37d48 | |||
| d9a1e6f4b1 | |||
| 72dae8e9ad | |||
| b2d7fb9443 | |||
| bf945e36bd | |||
| 2bcfe93dd2 | |||
| 195ef743c6 | |||
| 8478eb9aa0 | |||
| 47f57d20cd | |||
| 8f8eb06d6b | |||
| 7d6803f325 | |||
| f9b7a8b92b | |||
| 7ee0c047e5 | |||
| bfbfe17644 | |||
| 046940f949 | |||
| 2950ddcf4f | |||
| 3d441b4ad3 | |||
| caa5e8983b | |||
| 59b7944d8e | |||
| 648eb8fdd1 | |||
| f160516772 | |||
| 4beb7780f8 | |||
| d44b6d40e9 | |||
| 8fd9f5a8cb | |||
| 414af2f55e | |||
| 8624be6bcf | |||
| 4aa26b613b | |||
| 57cb062d48 | |||
| b4b6404b65 | |||
| 989dd2be50 | |||
| 50b8baddec | |||
| 2e00c3db0b | |||
| 232260787e | |||
| 54054c4aee | |||
| 1a7843a6b5 | |||
| ed4ba4d0d3 | |||
| 849d6118d0 | |||
| 364138d408 | |||
| 32c8da06ca | |||
| 2df4c5825f | |||
| 527cbc3ae0 | |||
| 4fee8744d6 | |||
| 8f8862d23d | |||
| 836f36fd8e | |||
| a1326cb5ed | |||
| 4bfda8851c | |||
| f02f15cbfb | |||
| 30182684d5 | |||
| 7df80af30b | |||
| 6b2e867107 | |||
| d333090b09 | |||
| 8ae7763ac5 | |||
| 51ac86be79 | |||
| 552078c8a2 | |||
| 4b4ede733f | |||
| d07e8bc747 | |||
| 8850d78e5a | |||
| 4fbf4e7d42 | |||
| d6e1a670b9 | |||
| 1a32c8827e | |||
| 784b5156df | |||
| 6e3336afb1 | |||
| d0ddee3623 | |||
| 9cf55033d4 | |||
| 7371efde17 | |||
| 156269a53f | |||
| ed7e5fb482 | |||
| 2e973a4591 | |||
| f1bd82d301 | |||
| f51bfa314d | |||
| 7b3410c7b1 | |||
| 6b0e7d51b1 | |||
| 4e5659efad | |||
| 66f10adde8 | |||
| 459c4d7fcc | |||
| 03d9efb790 | |||
| 33bd6099ee | |||
| 850a1d326b | |||
| f0bb19d48d | |||
| a56eac0131 | |||
| d3201809ab | |||
| 3628a0e241 | |||
| 07badd3df5 | |||
| f63091ce05 | |||
| 617b2978c0 | |||
| 364e96ad1e | |||
| 9170e5ca65 | |||
| cbe71f221c | |||
| c039c01407 | |||
| 629ba41f5f | |||
| 198fa67cc1 | |||
| e7830e3d55 | |||
| 90bfb25f41 | |||
| 037cb00caa | |||
| f002124c38 | |||
| ac42f9f599 | |||
| 88d4640d3f | |||
| d2da01041c | |||
| 75c17b820f | |||
| 2dc4bbf8cd | |||
| c9a5abc3be | |||
| 7ea468a9a6 | |||
| c6223520e0 | |||
| 176cbf9861 | |||
| 00315a6f2d | |||
| 2145b2f8e2 | |||
| 31155e7bc9 | |||
| 2d07208d0f | |||
| 35ad7e16c6 | |||
| 27c52fe6d5 | |||
| e153f63a23 | |||
| 2cbbc360c0 | |||
| 8a4a42777f | |||
| 352d6b5ef9 | |||
| cc49b0efdc | |||
| e71ee988bf | |||
| 937a5b2c2d | |||
| 92221e19c3 | |||
| e5c920864c | |||
| 6f63682bdf | |||
| 7623eddcd7 | |||
| d1dee1e908 | |||
| 8b302f8d21 | |||
| dfeebd6b6b | |||
| df53ce9f6e | |||
| 9bb2c00b6d | |||
| 641253c4c6 | |||
| 6ce49b7130 | |||
| 804ad0cece | |||
| 6c60d14c05 | |||
| def2d2bcc9 | |||
| 9bca705dc3 | |||
| 2dcc271dff | |||
| 33415fa1af | |||
| 10315e0e29 | |||
| f321cc81c8 | |||
| 68b685f580 | |||
| c87c79b937 | |||
| de3dfcdc43 | |||
| 657ddc0fa1 | |||
| 1da01489f1 | |||
| 64a27614ff | |||
| 6c5acfc8bb | |||
| 884ee5b587 | |||
| c2320a3c1e | |||
| ab23030051 | |||
| 699c692f6c | |||
| 0c85e26950 | |||
| 2cd7e737cd | |||
| a1adbe2b34 | |||
| a144c2b7ea | |||
| 201c056ffa | |||
| 2e95168610 | |||
| b18c05cbb9 | |||
| 102a4c6110 | |||
| b6c0ef3d19 | |||
| ea3065dbc2 | |||
| 534c3ad329 | |||
| 90a7d807a1 | |||
| 4baafb5e2f | |||
| f911765cdb | |||
| d0a36c79d5 | |||
| ffea5320a0 | |||
| 0b54e41a40 | |||
| 26c67f770a | |||
| f6623db0e6 | |||
| d7844ee8e8 | |||
| 1c8970bbf2 | |||
| eb70849a30 | |||
| 77ce3c9067 | |||
| 56cd226a27 | |||
| a1acdc0977 | |||
| 1a624c957b | |||
| 1c9eeb2aca | |||
| 45f06eeea7 | |||
| 66a53b486a | |||
| fc45f5b48f | |||
| c7105a22e6 | |||
| e1fc7cad44 | |||
| afeeaeb984 | |||
| 2bc60b87f2 | |||
| 4ad9ecb7db | |||
| 3ec7b82b85 | |||
| 48666def6b | |||
| 5732921fb6 | |||
| b86c11e1dc | |||
| 1bae4b219a | |||
| 70ff73f805 | |||
| a9636c5038 | |||
| a406b9f7ef | |||
| f291345111 | |||
| c947ccb8be | |||
| 28c6b0d4bb | |||
| 19317a9cc2 | |||
| d67da6425e | |||
| 901f41358f | |||
| 061922aea2 | |||
| 5f7b6a5498 | |||
| 2ef9d5eed9 | |||
| 9fba57d5db | |||
| e9074e3b3f | |||
| f053ba714f | |||
| b6598f09a3 | |||
| b9d7b95304 | |||
| 3db540f786 | |||
| 071b99c6c6 | |||
| 3a3d96be0a | |||
| a03fbab4d5 | |||
| cc3b119288 | |||
| 1a750d1b85 | |||
| 5b3ae9b9fd | |||
| 81471d80cd | |||
| eb14db4deb | |||
| 50d989363a | |||
| e52a99bd71 | |||
| a7a7c86657 |
@@ -4,6 +4,7 @@ on:
|
||||
push:
|
||||
paths:
|
||||
- .github/workflows/ci-ifcopenshell-conda-daily.yml
|
||||
- conda/**
|
||||
schedule:
|
||||
# ┌───────────── minute (0 - 59)
|
||||
# │ ┌───────────── hour (0 - 23)
|
||||
@@ -53,11 +54,13 @@ jobs:
|
||||
sudo mv -v MacOSX10.13.sdk /opt/ && \
|
||||
ls /opt/
|
||||
- uses: seanmiddleditch/gha-setup-ninja@master
|
||||
- uses: conda-incubator/setup-miniconda@v2 # https://github.com/conda-incubator/setup-miniconda
|
||||
- uses: mamba-org/provision-with-micromamba@main # https://github.com/mamba-org/provision-with-micromamba
|
||||
with:
|
||||
activate-environment: conda-build
|
||||
python-version: ${{ matrix.pyver.distver }}
|
||||
channels: conda-forge
|
||||
channel-priority: strict
|
||||
environment-file: conda/environment.yml
|
||||
extra-specs: |
|
||||
python=${{ matrix.pyver.distver }}
|
||||
- name: build, test and upload ifcopenshell
|
||||
if: ${{ matrix.platform.upload == 'true' }}
|
||||
run: |
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
name: ci-ifctester-pypi
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# ┌───────────── minute (0 - 59)
|
||||
# │ ┌───────────── hour (0 - 23)
|
||||
# │ │ ┌───────────── day of the month (1 - 31)
|
||||
# │ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
|
||||
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
|
||||
# * * * * *
|
||||
- cron: "0 0 18 * *"
|
||||
push:
|
||||
paths:
|
||||
- '.github/workflows/ci-ifctester-pypi.yml'
|
||||
|
||||
|
||||
env:
|
||||
major: 0
|
||||
minor: 0
|
||||
name: ifcopenshell
|
||||
|
||||
jobs:
|
||||
activate:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.repository == 'IfcOpenShell/IfcOpenShell'
|
||||
steps:
|
||||
- name: Set env
|
||||
run: echo ok go
|
||||
|
||||
build:
|
||||
needs: activate
|
||||
name: ${{ matrix.config.name }}-${{ matrix.pyver }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.10' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
|
||||
- run: echo ${{ env.DATE }}
|
||||
- name: Get current date
|
||||
id: date
|
||||
run: echo "::set-output name=date::$(date +'%y%m%d')"
|
||||
- name: Compile
|
||||
run: |
|
||||
pip install build
|
||||
cd src/ifctester &&
|
||||
make dist
|
||||
- name: Publish a Python distribution to PyPI
|
||||
uses: ortega2247/pypi-upload-action@master
|
||||
with:
|
||||
user: __token__
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
packages_dir: src/ifctester/dist
|
||||
@@ -86,6 +86,8 @@ src/blenderbim/blenderbim/libs
|
||||
|
||||
# blenderbim test temp files
|
||||
src/blenderbim/test/files/temp
|
||||
src/blenderbim/drawings
|
||||
src/blenderbim/layouts
|
||||
|
||||
# ifcopenshell swig and compiled files
|
||||
src/ifcopenshell-python/ifcopenshell/_ifcopenshell_wrapper.so
|
||||
|
||||
@@ -10,5 +10,4 @@ dependencies:
|
||||
- ripgrep
|
||||
- pip
|
||||
- pip:
|
||||
- --extra-index-url https://lief.s3-website.fr-par.scw.cloud/latest
|
||||
- lief==0.13.0.dev0
|
||||
- lief==0.13
|
||||
|
||||
@@ -24,7 +24,7 @@ requirements:
|
||||
host:
|
||||
- python
|
||||
- boost-cpp
|
||||
- occt
|
||||
- occt ==7.7.0
|
||||
- libxml2
|
||||
- cgal-cpp
|
||||
- hdf5
|
||||
@@ -32,11 +32,12 @@ requirements:
|
||||
- gmp # [unix]
|
||||
- mpir # [win]
|
||||
- nlohmann_json
|
||||
- zlib
|
||||
|
||||
run:
|
||||
- python
|
||||
- boost-cpp
|
||||
- occt
|
||||
- occt ==7.7.0
|
||||
- libxml2
|
||||
- cgal-cpp
|
||||
- hdf5
|
||||
@@ -44,6 +45,7 @@ requirements:
|
||||
- gmp # [unix]
|
||||
- mpir # [win]
|
||||
- nlohmann_json
|
||||
- zlib
|
||||
|
||||
test:
|
||||
imports:
|
||||
|
||||
@@ -59,6 +59,10 @@ import multiprocessing
|
||||
import platform
|
||||
import sysconfig
|
||||
|
||||
# @todo temporary for expired mpfr.org certificate on 2023-04-08
|
||||
import ssl
|
||||
ssl._create_default_https_context = ssl._create_unverified_context
|
||||
|
||||
from urllib.request import urlretrieve
|
||||
|
||||
|
||||
|
||||
@@ -236,6 +236,11 @@ endif
|
||||
cp -r dist/working/IfcOpenShell-0.7.0/src/ifc5d/ifc5d dist/blenderbim/libs/site/packages/
|
||||
# Provides IFCCityJSON functionality
|
||||
cp -r dist/working/IfcOpenShell-0.7.0/src/ifccityjson dist/blenderbim/libs/site/packages/
|
||||
# Provides IfcGit functionality
|
||||
cd dist/working && . env/bin/activate && $(PIP) install GitPython --target=./site-packages
|
||||
cp -r dist/working/site-packages/smmap dist/blenderbim/libs/site/packages/
|
||||
cp -r dist/working/site-packages/gitdb dist/blenderbim/libs/site/packages/
|
||||
cp -r dist/working/site-packages/git dist/blenderbim/libs/site/packages/
|
||||
rm -rf dist/working
|
||||
|
||||
# Provides Mustache templating in construction documentation
|
||||
@@ -659,6 +664,13 @@ endif
|
||||
cp -r dist/working/bpypolyskel-master/bpypolyskel dist/blenderbim/libs/site/packages/
|
||||
rm -rf dist/working
|
||||
|
||||
# Required for Desktop icon and file association
|
||||
cp -r blenderbim/libs/desktop dist/blenderbim/libs/
|
||||
|
||||
# Remove dependencies also bundled with Blender
|
||||
rm -rf dist/blenderbim/libs/site/packages/numpy
|
||||
rm -rf dist/blenderbim/libs/site/packages/numpy.libs
|
||||
|
||||
cd dist/blenderbim && $(SED) "s/999999/$(VERSION)/" __init__.py
|
||||
cd dist && zip -r blenderbim-$(VERSION)-$(PYVERSION)-$(PLATFORM).zip ./*
|
||||
rm -rf dist/blenderbim
|
||||
|
||||
@@ -34,7 +34,8 @@ bl_info = {
|
||||
if sys.modules.get("bpy", None):
|
||||
# Process *.pth in /libs/site/packages to setup globally importable modules
|
||||
# This is 3 levels deep as required by the static RPATH of ../../ from dependencies taken from Anaconda
|
||||
site.addsitedir(os.path.join(os.path.dirname(os.path.realpath(__file__)), "libs", "site", "packages"))
|
||||
# site.addsitedir(os.path.join(os.path.dirname(os.path.realpath(__file__)), "libs", "site", "packages"))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), "libs", "site", "packages"))
|
||||
|
||||
import blenderbim.bim
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import blenderbim
|
||||
import importlib
|
||||
from . import handler, ui, prop, operator, helper
|
||||
|
||||
@@ -70,6 +71,7 @@ modules = {
|
||||
"covetool": None,
|
||||
"augin": None,
|
||||
"debug": None,
|
||||
"ifcgit": None,
|
||||
# Uncomment this line to enable loading of the demo module. Happy hacking!
|
||||
# The name "demo" must correlate to a folder name in `bim/module/`.
|
||||
# "demo": None,
|
||||
@@ -93,6 +95,8 @@ classes = [
|
||||
operator.SelectIfcFile,
|
||||
operator.ReloadSelectedIfcFile,
|
||||
operator.SelectSchemaDir,
|
||||
operator.FileAssociate,
|
||||
operator.FileUnassociate,
|
||||
operator.SelectURIAttribute,
|
||||
operator.EditBlenderCollection,
|
||||
operator.BIM_OT_open_webbrowser,
|
||||
@@ -171,7 +175,11 @@ def register():
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
if getattr(cls, "is_registered", None) is None:
|
||||
bpy.utils.unregister_class(cls)
|
||||
elif cls.is_registered:
|
||||
bpy.utils.unregister_class(cls)
|
||||
|
||||
bpy.app.handlers.load_post.remove(handler.setDefaultProperties)
|
||||
bpy.app.handlers.load_post.remove(handler.loadIfcStore)
|
||||
bpy.app.handlers.save_post.remove(handler.ensureIfcExported)
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
text, tspan { /* 2.5mm */ fill: black; stroke: none; font-family: 'OpenGost Type B TT', 'DejaVu Sans Condensed', 'Liberation Sans', 'Arial Narrow', 'Arial'; font-size: 4.13px; }
|
||||
.cut { fill: black; stroke: black; stroke-linecap: 'round'; stroke-width: 0.35; fill-rule: evenodd; }
|
||||
.projection { fill: white; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; }
|
||||
.surface { stroke: none; fill: #fff; fill-rule: evenodd; }
|
||||
.annotation { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; }
|
||||
.IfcAnnotation { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; }
|
||||
.IfcGeographicElement { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 1; }
|
||||
@@ -96,4 +96,12 @@
|
||||
<path d="M 7.5 10 L 10 0 L 15 10" class="annotation" style="stroke-width:1; fill: white;" />
|
||||
</g>
|
||||
</marker>
|
||||
<filter x="0" y="0" width="1" height="1" id="fill-background">
|
||||
<feFlood flood-color="white" result="bg" />
|
||||
<feMerge>
|
||||
<feMergeNode in="bg"/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.6 KiB |
@@ -17,16 +17,49 @@
|
||||
<line x1="0" y1="0" x2="0" y2="3" style="stroke:black; stroke-width:0.25" />
|
||||
</pattern>
|
||||
<pattern id="square1" width="1" height="1" patternUnits="userSpaceOnUse">
|
||||
<line x1="0" y1="0" x2="0" y2="1" style="stroke:black; stroke-width:0.25" />
|
||||
<line x1="0" y1="0" x2="1" y2="0" style="stroke:black; stroke-width:0.25" />
|
||||
<path style="stroke: black; stroke-width: 0.25; fill: none;" d="M 0 0 1 0 1 1" />
|
||||
</pattern>
|
||||
<pattern id="square2" width="2" height="2" patternUnits="userSpaceOnUse">
|
||||
<line x1="0" y1="0" x2="0" y2="2" style="stroke:black; stroke-width:0.25" />
|
||||
<line x1="0" y1="0" x2="2" y2="0" style="stroke:black; stroke-width:0.25" />
|
||||
<path style="stroke: black; stroke-width: 0.25; fill: none;" d="M 0 0 2 0 2 2" />
|
||||
</pattern>
|
||||
<pattern id="square3" width="3" height="3" patternUnits="userSpaceOnUse">
|
||||
<line x1="0" y1="0" x2="0" y2="3" style="stroke:black; stroke-width:0.25" />
|
||||
<line x1="0" y1="0" x2="3" y2="0" style="stroke:black; stroke-width:0.25" />
|
||||
<path style="stroke: black; stroke-width: 0.25; fill: none;" d="M 0 0 3 0 3 3" />
|
||||
</pattern>
|
||||
<pattern id="recth1" width="2" height="1" patternUnits="userSpaceOnUse">
|
||||
<path style="stroke: black; stroke-width: 0.25; fill: none;" d="M 0 0 2 0 2 1" />
|
||||
</pattern>
|
||||
<pattern id="recth2" width="4" height="2" patternUnits="userSpaceOnUse">
|
||||
<path style="stroke: black; stroke-width: 0.25; fill: none;" d="M 0 0 4 0 4 2" />
|
||||
</pattern>
|
||||
<pattern id="recth3" width="6" height="3" patternUnits="userSpaceOnUse">
|
||||
<path style="stroke: black; stroke-width: 0.25; fill: none;" d="M 0 0 6 0 6 3" />
|
||||
</pattern>
|
||||
<pattern id="rectv1" width="1" height="2" patternUnits="userSpaceOnUse">
|
||||
<path style="stroke: black; stroke-width: 0.25; fill: none;" d="M 0 0 1 0 1 2" />
|
||||
</pattern>
|
||||
<pattern id="rectv2" width="2" height="4" patternUnits="userSpaceOnUse">
|
||||
<path style="stroke: black; stroke-width: 0.25; fill: none;" d="M 0 0 2 0 2 4" />
|
||||
</pattern>
|
||||
<pattern id="rectv3" width="3" height="6" patternUnits="userSpaceOnUse">
|
||||
<path style="stroke: black; stroke-width: 0.25; fill: none;" d="M 0 0 3 0 3 6" />
|
||||
</pattern>
|
||||
<pattern id="tile1" width="2" height="2" patternUnits="userSpaceOnUse">
|
||||
<path style="stroke: black; stroke-width: 0.15; fill: none;" d="M 0 0 2 0 2 1 0 1 M 1 1 1 2" />
|
||||
</pattern>
|
||||
<pattern id="tile2" width="4" height="4" patternUnits="userSpaceOnUse">
|
||||
<path style="stroke: black; stroke-width: 0.15; fill: none;" d="M 0 0 4 0 4 2 0 2 M 2 2 2 4" />
|
||||
</pattern>
|
||||
<pattern id="tile3" width="6" height="6" patternUnits="userSpaceOnUse">
|
||||
<path style="stroke: black; stroke-width: 0.15; fill: none;" d="M 0 0 6 0 6 3 0 3 M 3 3 3 6" />
|
||||
</pattern>
|
||||
<pattern id="board1" width="4" height="2" patternUnits="userSpaceOnUse">
|
||||
<path style="stroke: black; stroke-width: 0.15; fill: none;" d="M 0 0 4 0 4 1 0 1 M 2 1 2 2" />
|
||||
</pattern>
|
||||
<pattern id="board2" width="8" height="4" patternUnits="userSpaceOnUse">
|
||||
<path style="stroke: black; stroke-width: 0.15; fill: none;" d="M 0 0 8 0 8 2 0 2 M 4 2 4 4" />
|
||||
</pattern>
|
||||
<pattern id="board3" width="12" height="6" patternUnits="userSpaceOnUse">
|
||||
<path style="stroke: black; stroke-width: 0.15; fill: none;" d="M 0 0 12 0 12 3 0 3 M 6 3 6 6" />
|
||||
</pattern>
|
||||
<pattern id="crosshatch1" width="1" height="1" patternTransform="rotate(45 0 0)" patternUnits="userSpaceOnUse">
|
||||
<line x1="0" y1="0" x2="0" y2="1" style="stroke:black; stroke-width:0.25" />
|
||||
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 69 KiB |
@@ -0,0 +1,125 @@
|
||||
{
|
||||
"Technical": {
|
||||
"raster_style": {
|
||||
"bpy.data.worlds[0].color": [
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"scene.render.engine": "BLENDER_WORKBENCH",
|
||||
"scene.render.film_transparent": false,
|
||||
"scene.display.shading.show_object_outline": true,
|
||||
"scene.display.shading.show_cavity": false,
|
||||
"scene.display.shading.cavity_type": "BOTH",
|
||||
"scene.display.shading.curvature_ridge_factor": 1,
|
||||
"scene.display.shading.curvature_valley_factor": 1,
|
||||
"scene.view_settings.view_transform": "Standard",
|
||||
"scene.display.shading.light": "FLAT",
|
||||
"scene.display.shading.color_type": "SINGLE",
|
||||
"scene.display.shading.single_color": [
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"scene.display.shading.show_shadows": false,
|
||||
"scene.display.shading.shadow_intensity": 0.5,
|
||||
"scene.display.light_direction": [
|
||||
0.5,
|
||||
0.5,
|
||||
0.5
|
||||
],
|
||||
"scene.view_settings.use_curve_mapping": false,
|
||||
"space.overlay.show_wireframes": true,
|
||||
"space.overlay.wireframe_threshold": 0,
|
||||
"space.overlay.show_floor": false,
|
||||
"space.overlay.show_axis_x": false,
|
||||
"space.overlay.show_axis_y": false,
|
||||
"space.overlay.show_axis_z": false,
|
||||
"space.overlay.show_object_origins": false,
|
||||
"space.overlay.show_relationship_lines": false
|
||||
},
|
||||
"render_type": "VIEWPORT"
|
||||
},
|
||||
"Shaded": {
|
||||
"raster_style": {
|
||||
"bpy.data.worlds[0].color": [
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"scene.render.engine": "BLENDER_WORKBENCH",
|
||||
"scene.render.film_transparent": false,
|
||||
"scene.display.shading.show_object_outline": true,
|
||||
"scene.display.shading.show_cavity": true,
|
||||
"scene.display.shading.cavity_type": "BOTH",
|
||||
"scene.display.shading.curvature_ridge_factor": 1,
|
||||
"scene.display.shading.curvature_valley_factor": 1,
|
||||
"scene.view_settings.view_transform": "Standard",
|
||||
"scene.display.shading.light": "STUDIO",
|
||||
"scene.display.shading.color_type": "MATERIAL",
|
||||
"scene.display.shading.single_color": [
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"scene.display.shading.show_shadows": true,
|
||||
"scene.display.shading.shadow_intensity": 0.5,
|
||||
"scene.display.light_direction": [
|
||||
0.5,
|
||||
0.5,
|
||||
0.5
|
||||
],
|
||||
"scene.view_settings.use_curve_mapping": false,
|
||||
"space.overlay.show_wireframes": false,
|
||||
"space.overlay.wireframe_threshold": 0,
|
||||
"space.overlay.show_floor": false,
|
||||
"space.overlay.show_axis_x": false,
|
||||
"space.overlay.show_axis_y": false,
|
||||
"space.overlay.show_axis_z": false,
|
||||
"space.overlay.show_object_origins": false,
|
||||
"space.overlay.show_relationship_lines": false
|
||||
},
|
||||
"render_type": "VIEWPORT"
|
||||
},
|
||||
"Blender Default": {
|
||||
"raster_style": {
|
||||
"bpy.data.worlds[0].color": [
|
||||
0.05087608844041824,
|
||||
0.05087608844041824,
|
||||
0.05087608844041824
|
||||
],
|
||||
"scene.render.engine": "BLENDER_EEVEE",
|
||||
"scene.render.film_transparent": false,
|
||||
"scene.view_settings.view_transform": "Filmic",
|
||||
"scene.display.shading.show_object_outline": false,
|
||||
"scene.display.shading.show_cavity": false,
|
||||
"scene.display.shading.cavity_type": "WORLD",
|
||||
"scene.display.shading.curvature_ridge_factor": 0.0,
|
||||
"scene.display.shading.curvature_valley_factor": 0.0,
|
||||
"scene.display.shading.light": "STUDIO",
|
||||
"scene.display.shading.color_type": "MATERIAL",
|
||||
"scene.display.shading.single_color": [
|
||||
0.800000011920929,
|
||||
0.800000011920929,
|
||||
0.800000011920929
|
||||
],
|
||||
"scene.display.shading.show_shadows": false,
|
||||
"scene.display.shading.shadow_intensity": 0.5,
|
||||
"scene.display.light_direction": [
|
||||
0.5773502588272095,
|
||||
0.5773502588272095,
|
||||
0.5773502588272095
|
||||
],
|
||||
"scene.view_settings.use_curve_mapping": false,
|
||||
"space.overlay.show_wireframes": false,
|
||||
"space.overlay.wireframe_threshold": 1.0,
|
||||
"space.overlay.show_floor": true,
|
||||
"space.overlay.show_axis_x": true,
|
||||
"space.overlay.show_axis_y": true,
|
||||
"space.overlay.show_axis_z": false,
|
||||
"space.overlay.show_object_origins": true,
|
||||
"space.overlay.show_relationship_lines": true
|
||||
},
|
||||
"render_type": "VIEWPORT"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<svg baseProfile="full" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="rectangle-tag">
|
||||
<rect x="-6" y="-2.5" width="12" height="5" fill="white" stroke="black" style="stroke-width: 0.25;" />
|
||||
</g>
|
||||
<g id="triangle-tag">
|
||||
<path style="fill: white; stroke: black; stroke-width: 0.25;" d="M 3.1409425e-7,-7.1069207 5.7650623,2.8784603 l -11.5301246,-6e-7 z" />
|
||||
</g>
|
||||
<g id="hexagon-tag">
|
||||
<path style="fill: white; stroke: black; stroke-width: 0.25;" d="m 0.0161133,-6.0583333 5.2339915,3.8366023 -2.031433,6.1633977 c 0,0 -6.4894866,-0.027414 -6.4894866,-0.027414 l -1.97929,-6.1803395 5.2662181,-3.7922467" />
|
||||
</g>
|
||||
<g id="capsule-tag">
|
||||
<rect x="-6" y="-2.5" width="12" height="5" rx="2.5" ry="2.5" fill="white" stroke="black" style="stroke-width: 0.25;" />
|
||||
</g>
|
||||
<g id="circle-tag">
|
||||
<circle r="5" fill="white" stroke="black" style="stroke-width: 0.25;" />
|
||||
</g>
|
||||
<g id="door-tag">
|
||||
<circle r="5" fill="white" stroke="black" style="stroke-width: 0.25;" />
|
||||
<text y="-1.75" class="regular" text-anchor="middle" dominant-baseline="middle" data-type="text-template"></text>
|
||||
<line x1="-5" x2="5" style="stroke: black; stroke-width: 0.25;" />
|
||||
<text y="2.5" class="regular" text-anchor="middle" dominant-baseline="middle" data-type="text-template"></text>
|
||||
</g>
|
||||
<g id="space-tag">
|
||||
<text y="-5" class="large" text-anchor="middle" dominant-baseline="middle" data-type="text-template"></text>
|
||||
<rect x="-6" y="-2.5" width="12" height="5" fill="white" stroke="black" style="stroke-width: 0.25;" />
|
||||
<text class="regular" text-anchor="middle" dominant-baseline="middle" data-type="text-template"></text>
|
||||
<text y="5" class="regular" text-anchor="middle" dominant-baseline="middle" data-type="text-template"></text>
|
||||
</g>
|
||||
<g id="elevation-arrow">
|
||||
<path style="fill: black;" d="M -7.07106 0 0 -7.07106 7.07106 0" />
|
||||
</g>
|
||||
<g id="elevation-tag">
|
||||
<circle r="5" fill="white" stroke="black" style="stroke-width: 0.25;" />
|
||||
<line x1="-5" y1="0" x2="5" y2="0" style="stroke: black; stroke-width: 0.25;" />
|
||||
</g>
|
||||
<g id="section-arrow">
|
||||
<path style="fill: black;" d="M -7.07106 0 0 -7.07106 7.07106 0" />
|
||||
</g>
|
||||
<g id="section-tag">
|
||||
<circle r="5" fill="white" stroke="black" style="stroke-width: 0.25;" />
|
||||
<line x1="-5" y1="0" x2="5" y2="0" style="stroke: black; stroke-width: 0.25;" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
@@ -1,67 +1,55 @@
|
||||
<link href="jsgantt.css" rel="stylesheet" type="text/css"/>
|
||||
<link href="../../module/sequence/gantt/main.css" rel="stylesheet" type="text/css"/>
|
||||
<script src="jsgantt.js" type="text/javascript"></script>
|
||||
<style>
|
||||
.gtaskcellwkend,
|
||||
.gtaskcellcurrent,
|
||||
.gminorheadingwkend {
|
||||
background-color: #e1e1e1;
|
||||
}
|
||||
|
||||
.gitemhighlight td {
|
||||
background-color: #ffdaaa;
|
||||
}
|
||||
|
||||
.gtaskblue {
|
||||
background: #4281A4;
|
||||
}
|
||||
|
||||
.gtaskred {
|
||||
background: #C1666B;
|
||||
}
|
||||
|
||||
.gtaskgreen {
|
||||
background: #48A9A6;
|
||||
}
|
||||
|
||||
.gtaskyellow {
|
||||
background: #D4B483;
|
||||
}
|
||||
|
||||
.gmainleft {
|
||||
overflow: visible;
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
</style>
|
||||
<a href="#print" id="print">Print Mode</a>
|
||||
<div style="position:relative" class="gantt" id="GanttChartDIV"></div>
|
||||
<script type="text/javascript">
|
||||
var g = new JSGantt.GanttChart(document.getElementById('GanttChartDIV'), 'day');
|
||||
g.setOptions({
|
||||
vCaptionType: 'Caption', // Set to Show Caption : None,Caption,Resource,Duration,Complete,
|
||||
vQuarterColWidth: 36,
|
||||
vDateTaskDisplayFormat: 'day dd month yyyy', // Shown in tool tip box
|
||||
vDayMajorDateDisplayFormat: 'mon yyyy - Week ww',// Set format to dates in the "Major" header of the "Day" view
|
||||
vWeekMinorDateDisplayFormat: 'dd mon', // Set format to display dates in the "Minor" header of the "Week" view
|
||||
vLang: 'en',
|
||||
vShowTaskInfoLink: 1, // Show link in tool tip (0/1)
|
||||
vShowEndWeekDate: 0, // Show/Hide the date for the last day of the week in header for daily
|
||||
vUseSingleCell: 10000, // Set the threshold cell per table row (Helps performance for large data.
|
||||
vFormatArr: ['Day', 'Week', 'Month', 'Quarter'], // Even with setUseSingleCell using Hour format on such a large chart can cause issues in some browsers,
|
||||
vShowRes: false, // Disable the resource column.
|
||||
vShowComp: false, // Disable the completion column.
|
||||
vShowDur: false, // Disable the duration column, because jsgantt doesn't calculate durations the way we want.
|
||||
vAdditionalHeaders: {ifcduration: {title: 'Duration'}},
|
||||
vUseToolTip: false, // Disable tooltips.
|
||||
vTotalHeight: 900,
|
||||
});
|
||||
document.getElementById('print').addEventListener('click', function() {
|
||||
g.setTotalHeight("");
|
||||
g.Draw();
|
||||
});
|
||||
var json_data = `
|
||||
{{{json_data}}}
|
||||
`;
|
||||
JSGantt.parseJSONString(json_data, g);
|
||||
g.Draw();
|
||||
<script type="text/javascript" src="../../module/sequence/gantt/index.js"></script>
|
||||
<script>
|
||||
var json_data = `{{{json_data}}}`;
|
||||
</script>
|
||||
|
||||
<div id="options" class="no-print">
|
||||
<h4>Choose a language:
|
||||
<select id="lang" class="sweet_button" onchange="set_language(event)">
|
||||
<option value='cn'>Chinese (cn)</option>
|
||||
<option value='cs'>Czech (cs)</option>
|
||||
<option value='nl'>Dutch (Standard)</option>
|
||||
<option value='en' selected>English (en)</option>
|
||||
<option value='fr'>French (fr)</option>
|
||||
<option value='de'>German (de)</option>
|
||||
<option value='hu'>Hungarian (hu)</option>
|
||||
<option value='id'>Indonesian (id)</option>
|
||||
<option value='it'>Italian (it)</option>
|
||||
<option value='ja'>Japanese (ja)</option>
|
||||
<option value='pt'>Portuguese (pt)</option>
|
||||
<option value='ru'>Russian (ru)</option>
|
||||
<option value='es'>Spanish (es)</option>
|
||||
<option value='sv'>Swedish (sv)</option>
|
||||
<option value='tr'>Turkish (tr)</option>
|
||||
</select>
|
||||
</h4>
|
||||
|
||||
<br>
|
||||
</div>
|
||||
<div class="top-right no-print" id="print_options">
|
||||
<select class="sweet_button" id="print_page_size">
|
||||
<option value="210,297">A4 Portrait</option>
|
||||
<option value="297,210">A4 Landscape</option>
|
||||
<option value="297,420">A3 Portrait</option>
|
||||
<option value="420,297">A3 Landscape</option>
|
||||
<option value="420,594">A2 Portrait</option>
|
||||
<option value="594,420">A2 Landscape</option>
|
||||
<option value="594,841">A1 Portrait</option>
|
||||
<option value="841,594">A1 Landscape</option>
|
||||
<option value="841,1189">A0 Portrait</option>
|
||||
<option value="1189,841">A0 Landscape</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="schedule-data" class="no-print"></div>
|
||||
<div id="schedule-header" style=""></div>
|
||||
<div style="position:relative" class="gantt" id="GanttChartDIV"></div>
|
||||
|
||||
<script>
|
||||
var data = `{{{data}}}`;
|
||||
setupPage(data);
|
||||
create_gantt_chart(json_data)
|
||||
</script>
|
||||
@@ -5,20 +5,26 @@ FILE_NAME('EPset_Drawing.ifc','2020-01-01T00:00:00',(),(),'EPset_Drawing','EPset
|
||||
FILE_SCHEMA(('IFC4'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation',(#2,#3,#4,#5,#6,#7,#8,#9,#10,#11,#12,#13,#14,#15));
|
||||
#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation',(#2,#3,#4,#5,#6,#7,#8,#9,#10,#11,#12,#13,#14,#15,#16,#17,#18,#19,#20,#21));
|
||||
#2=IFCSIMPLEPROPERTYTEMPLATE('23JavTMk98ZxXhrUEnjAcf',$,'TargetView','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#3=IFCSIMPLEPROPERTYTEMPLATE('1yVWUt5H9DAOuu0OaMMLpe',$,'Scale','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#4=IFCSIMPLEPROPERTYTEMPLATE('3gsuPBtU93b8f0gg1pjkq6',$,'HumanScale','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#3=IFCSIMPLEPROPERTYTEMPLATE('1yVWUt5H9DAOuu0OaMMLpe',$,'Scale','The scale of this drawing represented as a numerator and denominator, such as 1/100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#4=IFCSIMPLEPROPERTYTEMPLATE('3gsuPBtU93b8f0gg1pjkq6',$,'HumanScale','The scale of this drawing in human readable format, such as 1:100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#5=IFCSIMPLEPROPERTYTEMPLATE('0AK5C2UpL4$eaac2LszAx$',$,'HasUnderlay','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#6=IFCSIMPLEPROPERTYTEMPLATE('2j2ZEZR8X5tONm7kli5hM6',$,'HasLinework','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#7=IFCSIMPLEPROPERTYTEMPLATE('1ttChRysH9UuEX2FeMj5Hu',$,'HasAnnotation','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#8=IFCSIMPLEPROPERTYTEMPLATE('2NPPxuABv1huDTVh32TFgw',$,'GlobalReferencing','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#9=IFCSIMPLEPROPERTYTEMPLATE('10hT_1zrzEbRRKMXYAWvtD',$,'Metadata','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||
#10=IFCSIMPLEPROPERTYTEMPLATE('3Z0BXPSG5CWgtI33ioV7aj',$,'Include','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||
#11=IFCSIMPLEPROPERTYTEMPLATE('1RVts_g3PAw98PJA2yL3bO',$,'Exclude','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||
#10=IFCSIMPLEPROPERTYTEMPLATE('3Z0BXPSG5CWgtI33ioV7aj',$,'Include','Selector expression to include ifc elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||
#11=IFCSIMPLEPROPERTYTEMPLATE('1RVts_g3PAw98PJA2yL3bO',$,'Exclude','Selector expression to exclude ifc elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||
#12=IFCSIMPLEPROPERTYTEMPLATE('0c1$8NpYDEaBiJrj16jHIo',$,'Stylesheet','',.P_SINGLEVALUE.,'IfcURIReference',$,$,$,$,$,.READWRITE.);
|
||||
#13=IFCSIMPLEPROPERTYTEMPLATE('3mRF52q81FQB$h4oTh7M45',$,'Markers','',.P_SINGLEVALUE.,'IfcURIReference',$,$,$,$,$,.READWRITE.);
|
||||
#14=IFCSIMPLEPROPERTYTEMPLATE('1rhr_0N3LDtuORcEJP0KXM',$,'Symbols','',.P_SINGLEVALUE.,'IfcURIReference',$,$,$,$,$,.READWRITE.);
|
||||
#15=IFCSIMPLEPROPERTYTEMPLATE('2sHDBuW7P4TROy$hL2w7ct',$,'Patterns','',.P_SINGLEVALUE.,'IfcURIReference',$,$,$,$,$,.READWRITE.);
|
||||
#16=IFCSIMPLEPROPERTYTEMPLATE('1$xfo9EVb26QLqmPll2_RK',$,'MetricPrecision','',.P_SINGLEVALUE.,'IfcReal',$,$,$,$,$,.READWRITE.);
|
||||
#17=IFCSIMPLEPROPERTYTEMPLATE('38uAtrp9nD_901NO42zd$7',$,'ImperialPrecision','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#18=IFCSIMPLEPROPERTYTEMPLATE('1MX0uffTL6TOvtvEJpxFmk',$,'DecimalPlaces','',.P_SINGLEVALUE.,'IfcInteger',$,$,$,$,$,.READWRITE.);
|
||||
#19=IFCSIMPLEPROPERTYTEMPLATE('0joEq0Rd10cxweEh0NeHT6',$,'JoinCriteria','Comma separated selection keys which determine what cut objects are to be joined.',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||
#20=IFCSIMPLEPROPERTYTEMPLATE('0nYMT3OSj5gArVniCWZRtv',$,'ShadingStyles','',.P_SINGLEVALUE.,'IfcURIReference',$,$,$,$,$,.READWRITE.);
|
||||
#21=IFCSIMPLEPROPERTYTEMPLATE('3VWG22eZXBdQwdKlzMeVQH',$,'CurrentShadingStyle','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
|
||||
@@ -5,18 +5,34 @@ FILE_NAME('Psets_BBIM_Annotation.ifc','2020-01-01T00:00:00',(),(),'Psets_BBIM_An
|
||||
FILE_SCHEMA(('IFC4'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1=IFCPROPERTYSETTEMPLATE('3VuPUwdCD2Qx3XDDRs0R1N',$,'EPset_Annotation','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation',(#2,#3,#4));
|
||||
#1=IFCPROPERTYSETTEMPLATE('3VuPUwdCD2Qx3XDDRs0R1N',$,'EPset_Annotation','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation,IfcTypeProduct',(#2,#3,#4));
|
||||
#2=IFCSIMPLEPROPERTYTEMPLATE('2P7JN79n96Q9pElZ83LKe4',$,'ZIndex','',.P_SINGLEVALUE.,'IfcInteger',$,$,$,$,$,.READWRITE.);
|
||||
#3=IFCSIMPLEPROPERTYTEMPLATE('1Wpx_r2xj1_9w5JpI0QRJy',$,'Symbol','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#4=IFCSIMPLEPROPERTYTEMPLATE('3q0oxMUKP47vZ4jnyG$dDb',$,'Classes','Classes separarated by spaces that end up in classes for this element in svg. Can be used to specify the text font size: small - 1.8mm; regular - 2.5mm; large - 3.5mm; header - 5mm; title - 7mm. By default regular size is used.',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#5=IFCPROPERTYSETTEMPLATE('0iKwujnQL9IevVQato8f7Z',$,'BBIM_Batting','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation',(#6,#7));
|
||||
#5=IFCPROPERTYSETTEMPLATE('0iKwujnQL9IevVQato8f7Z',$,'BBIM_Batting','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation,IfcTypeProduct',(#6,#7));
|
||||
#6=IFCSIMPLEPROPERTYTEMPLATE('0t2LEesGT1QRQtrIZUAR8L',$,'Thickness','Batting thickness',.P_SINGLEVALUE.,'IfcPositiveLengthMeasure',$,$,$,$,$,.READWRITE.);
|
||||
#7=IFCSIMPLEPROPERTYTEMPLATE('082PndS6v2kBOiJoSboMnh',$,'Reverse pattern direction','Reverse batting pattern (swap starting and ending points)',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#8=IFCPROPERTYSETTEMPLATE('1Dx2EiZnP67xotInXpwz90',$,'BBIM_Section','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation',(#9,#10,#11,#12,#13));
|
||||
#8=IFCPROPERTYSETTEMPLATE('1Dx2EiZnP67xotInXpwz90',$,'BBIM_Section','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation,IfcTypeProduct',(#9,#10,#11,#12,#13));
|
||||
#9=IFCSIMPLEPROPERTYTEMPLATE('2a_9s8spHDc9dZHtgg71XL',$,'ShowStartArrow','Display start arrow.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#10=IFCSIMPLEPROPERTYTEMPLATE('3i6SH_GbT7zhKea$wVE56A',$,'StartArrowSymbol','Custom symbol for the start of the section marker arrow. Need to make sure it''s present in "symbols.svg".',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#11=IFCSIMPLEPROPERTYTEMPLATE('1DtsPn5a9FG8$zXHDDMavY',$,'ShowEndArrow','Display end arrow.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#12=IFCSIMPLEPROPERTYTEMPLATE('2$6U0mLI9AiPRWeBabdY3u',$,'EndArrowSymbol','Custom symbol for the end of the section marker arrow. Need to make sure it''s present in "symbols.svg".',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#13=IFCSIMPLEPROPERTYTEMPLATE('1naFqntIL7igCY7hCaE7kq',$,'HasConnectedSectionLine','Connect or disconnect section markers with line (by default = True).',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#14=IFCPROPERTYSETTEMPLATE('3V8oZ8YRD3_O7uR5vcUleS',$,'BBIM_Documentation','',.PSET_OCCURRENCEDRIVEN.,'IfcProject',(#15,#16,#17,#18,#19,#20,#21,#22,#23));
|
||||
#15=IFCSIMPLEPROPERTYTEMPLATE('0ulAhgk3v9qfGlDILauR6J',$,'SheetsDir','Default sheets directory',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#16=IFCSIMPLEPROPERTYTEMPLATE('2yvlVKiQXASucfH40deCvu',$,'LayoutsDir','Default layouts directory',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#17=IFCSIMPLEPROPERTYTEMPLATE('2kXZqXicL3jRwsOnLM_0ho',$,'TitleblocksDir','Default titleblocks directory',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#18=IFCSIMPLEPROPERTYTEMPLATE('1lmOfo9Bf9beMBJkCZKf5Y',$,'DrawingsDir','Default drawings directory',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#19=IFCSIMPLEPROPERTYTEMPLATE('30cOKpb4D9CADKM04$N9X3',$,'StylesheetPath','Default stylesheet CSS',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#20=IFCSIMPLEPROPERTYTEMPLATE('23tejOrxj859R_eCRp1AFP',$,'MarkersPath','Default markers SVG',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#21=IFCSIMPLEPROPERTYTEMPLATE('1UDakJ5_f7kBhggNSW4$h5',$,'SymbolsPath','Default symbols SVG',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#22=IFCSIMPLEPROPERTYTEMPLATE('0d53LEtgLDQxnv__NfgH7i',$,'PatternsPath','Default patterns SVG',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#23=IFCSIMPLEPROPERTYTEMPLATE('26qFNMv7nCHgU6Jd7Anga5',$,'ShadingStylesPath','Default shading styles',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#24=IFCPROPERTYSETTEMPLATE('0I9merLinF5Ap$aZwaclgm',$,'BBIM_Dimension','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation,IfcTypeProduct',(#25,#26,#27,#28));
|
||||
#25=IFCSIMPLEPROPERTYTEMPLATE('1rL2AbQsXD8RbpoWH5pYOV',$,'ShowDescriptionOnly','Hide the measurement values and show only annotation description',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#26=IFCSIMPLEPROPERTYTEMPLATE('0SVyOfB0rC2xNfdRYf3XvY',$,'SuppressZeroInches','Suppress 0 inch values in dimension annotation text (for example: 12'' - 0" -> 12'')',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#27=IFCSIMPLEPROPERTYTEMPLATE('2bUmj458PBqPAtUoI3MXsb',$,'TextPrefix','Text to add before annotation measurement value',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#28=IFCSIMPLEPROPERTYTEMPLATE('0bnzttUb9BPuN597uNTXOE',$,'TextSuffix','Text to add after annotation measurement value',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<svg baseProfile="full" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="rectangle-tag">
|
||||
<rect x="-6" y="-2.5" width="12" height="5" fill="white" stroke="black" style="stroke-width: 0.25;" />
|
||||
</g>
|
||||
<g id="door-tag">
|
||||
<circle cy="1.75" r="5" fill="white" stroke="black" style="stroke-width: 0.25;" />
|
||||
<text text-anchor="middle" dominant-baseline="middle" data-type="text-template"></text>
|
||||
<line x1="-5" y1="1.75" x2="5" y2="1.75" style="stroke: black; stroke-width: 0.25;" />
|
||||
<text y="5" text-anchor="middle" dominant-baseline="middle" data-type="text-template"></text>
|
||||
</g>
|
||||
<g id="elevation-arrow">
|
||||
<path style="fill: black;" d="M -7.07106 0 0 -7.07106 7.07106 0" />
|
||||
</g>
|
||||
<g id="elevation-tag">
|
||||
<circle r="5" fill="white" stroke="black" style="stroke-width: 0.25;" />
|
||||
<line x1="-5" y1="0" x2="5" y2="0" style="stroke: black; stroke-width: 0.25;" />
|
||||
</g>
|
||||
<g id="section-arrow">
|
||||
<path style="fill: black;" d="M -7.07106 0 0 -7.07106 7.07106 0" />
|
||||
</g>
|
||||
<g id="section-tag">
|
||||
<circle r="5" fill="white" stroke="black" style="stroke-width: 0.25;" />
|
||||
<line x1="-5" y1="0" x2="5" y2="0" style="stroke: black; stroke-width: 0.25;" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -45,7 +45,6 @@ class IfcExporter:
|
||||
self.set_header()
|
||||
IfcStore.update_cache()
|
||||
if bpy.context.scene.BIMProjectProperties.is_authoring:
|
||||
self.sync_deletions()
|
||||
self.sync_all_objects()
|
||||
self.sync_edited_objects()
|
||||
extension = self.ifc_export_settings.output_file.split(".")[-1].lower()
|
||||
@@ -87,19 +86,6 @@ class IfcExporter:
|
||||
self.get_application_name(), self.get_application_version()
|
||||
)
|
||||
|
||||
def sync_deletions(self):
|
||||
results = []
|
||||
for ifc_definition_id in IfcStore.deleted_ids:
|
||||
try:
|
||||
product = self.file.by_id(ifc_definition_id)
|
||||
if hasattr(product, "GlobalId"):
|
||||
results.append(product.GlobalId)
|
||||
except:
|
||||
continue
|
||||
ifcopenshell.api.run("root.remove_product", self.file, **{"product": product})
|
||||
IfcStore.deleted_ids.clear()
|
||||
return results
|
||||
|
||||
def sync_all_objects(self):
|
||||
results = []
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
|
||||
@@ -126,7 +112,7 @@ class IfcExporter:
|
||||
continue
|
||||
try:
|
||||
if isinstance(obj, bpy.types.Material):
|
||||
if self.has_changed_shading(obj):
|
||||
if tool.Ifc.has_changed_shading(obj):
|
||||
blenderbim.core.style.update_style_colours(tool.Ifc, tool.Style, obj=obj)
|
||||
else:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
@@ -149,10 +135,6 @@ class IfcExporter:
|
||||
checksum = obj.data.BIMMeshProperties.material_checksum
|
||||
return checksum != str([s.id() for s in tool.Geometry.get_styles(obj) if s])
|
||||
|
||||
def has_changed_shading(self, obj):
|
||||
checksum = obj.BIMMaterialProperties.shading_checksum
|
||||
return checksum != repr(np.array(obj.diffuse_color).tobytes())
|
||||
|
||||
def sync_object_placement(self, obj):
|
||||
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
if not tool.Ifc.is_moved(obj):
|
||||
|
||||
@@ -27,6 +27,8 @@ from bpy.app.handlers import persistent
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.module.owner.prop import get_user_person, get_user_organisation
|
||||
from blenderbim.bim.module.model.data import AuthoringData
|
||||
from mathutils import Vector
|
||||
from math import cos, degrees
|
||||
|
||||
|
||||
global_subscription_owner = object()
|
||||
@@ -99,22 +101,69 @@ def color_callback(obj, data):
|
||||
|
||||
def active_object_callback():
|
||||
refresh_ui_data()
|
||||
update_bim_tool_props()
|
||||
|
||||
|
||||
def update_bim_tool_props():
|
||||
"""update BIM Tools props (such as extrusion_depth, length and x_angle) when active object changes"""
|
||||
obj = bpy.context.active_object
|
||||
|
||||
# bunch of checks to see if we're in a valid state
|
||||
if not obj:
|
||||
return
|
||||
mode = bpy.context.mode
|
||||
current_tool = bpy.context.workspace.tools.from_space_view3d_mode(mode).idname
|
||||
if current_tool != "bim.bim_tool":
|
||||
return
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
return
|
||||
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
||||
if not representation:
|
||||
return
|
||||
extrusion = tool.Model.get_extrusion(representation)
|
||||
if not extrusion:
|
||||
return
|
||||
|
||||
def get_x_angle(extrusion):
|
||||
x, y, z = extrusion.ExtrudedDirection.DirectionRatios
|
||||
x_angle = Vector((0, 1)).angle_signed(Vector((y, z)))
|
||||
return x_angle
|
||||
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
props = bpy.context.scene.BIMModelProperties
|
||||
if not AuthoringData.is_loaded:
|
||||
AuthoringData.load()
|
||||
|
||||
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.length = (axis[1] - axis[0]).length
|
||||
props.x_angle = x_angle
|
||||
|
||||
elif AuthoringData.data["active_material_usage"] == "LAYER3":
|
||||
x_angle = get_x_angle(extrusion)
|
||||
props.x_angle = x_angle
|
||||
|
||||
elif AuthoringData.data["active_material_usage"] == "PROFILE":
|
||||
props.extrusion_depth = extrusion.Depth * si_conversion
|
||||
|
||||
|
||||
def active_material_index_callback(obj, data):
|
||||
refresh_ui_data()
|
||||
|
||||
|
||||
def subscribe_to(object, data_path, callback):
|
||||
def subscribe_to(obj, data_path, callback):
|
||||
try:
|
||||
subscribe_to = object.path_resolve(data_path, False)
|
||||
subscribe_to = obj.path_resolve(data_path, False)
|
||||
except:
|
||||
return
|
||||
bpy.msgbus.subscribe_rna(
|
||||
key=subscribe_to,
|
||||
owner=object,
|
||||
owner=obj,
|
||||
args=(
|
||||
object,
|
||||
obj,
|
||||
data_path,
|
||||
),
|
||||
notify=callback,
|
||||
@@ -233,72 +282,4 @@ def setDefaultProperties(scene):
|
||||
)
|
||||
ifcopenshell.api.owner.settings.get_user = lambda ifc: core_owner.get_user(tool.Owner)
|
||||
ifcopenshell.api.owner.settings.get_application = get_application
|
||||
# TODO: Move to drawing module
|
||||
if len(bpy.context.scene.DocProperties.drawing_styles) == 0:
|
||||
drawing_style = bpy.context.scene.DocProperties.drawing_styles.add()
|
||||
drawing_style.name = "Technical"
|
||||
drawing_style.render_type = "VIEWPORT"
|
||||
drawing_style.raster_style = json.dumps(
|
||||
{
|
||||
RasterStyleProperty.WORLD_COLOR.value: (1, 1, 1),
|
||||
RasterStyleProperty.RENDER_ENGINE.value: "BLENDER_WORKBENCH",
|
||||
RasterStyleProperty.RENDER_TRANSPARENT.value: False,
|
||||
RasterStyleProperty.SHADING_SHOW_OBJECT_OUTLINE.value: True,
|
||||
RasterStyleProperty.SHADING_SHOW_CAVITY.value: False,
|
||||
RasterStyleProperty.SHADING_CAVITY_TYPE.value: "BOTH",
|
||||
RasterStyleProperty.SHADING_CURVATURE_RIDGE_FACTOR.value: 1,
|
||||
RasterStyleProperty.SHADING_CURVATURE_VALLEY_FACTOR.value: 1,
|
||||
RasterStyleProperty.VIEW_TRANSFORM.value: "Standard",
|
||||
RasterStyleProperty.SHADING_LIGHT.value: "FLAT",
|
||||
RasterStyleProperty.SHADING_COLOR_TYPE.value: "SINGLE",
|
||||
RasterStyleProperty.SHADING_SINGLE_COLOR.value: (1, 1, 1),
|
||||
RasterStyleProperty.SHADING_SHOW_SHADOWS.value: False,
|
||||
RasterStyleProperty.SHADING_SHADOW_INTENSITY.value: 0.5,
|
||||
RasterStyleProperty.DISPLAY_LIGHT_DIRECTION.value: (0.5, 0.5, 0.5),
|
||||
RasterStyleProperty.VIEW_USE_CURVE_MAPPING.value: False,
|
||||
RasterStyleProperty.OVERLAY_SHOW_WIREFRAMES.value: True,
|
||||
RasterStyleProperty.OVERLAY_WIREFRAME_THRESHOLD.value: 0,
|
||||
RasterStyleProperty.OVERLAY_SHOW_FLOOR.value: False,
|
||||
RasterStyleProperty.OVERLAY_SHOW_AXIS_X.value: False,
|
||||
RasterStyleProperty.OVERLAY_SHOW_AXIS_Y.value: False,
|
||||
RasterStyleProperty.OVERLAY_SHOW_AXIS_Z.value: False,
|
||||
RasterStyleProperty.OVERLAY_SHOW_OBJECT_ORIGINS.value: False,
|
||||
RasterStyleProperty.OVERLAY_SHOW_RELATIONSHIP_LINES.value: False,
|
||||
}
|
||||
)
|
||||
drawing_style = bpy.context.scene.DocProperties.drawing_styles.add()
|
||||
drawing_style.name = "Shaded"
|
||||
drawing_style.render_type = "VIEWPORT"
|
||||
drawing_style.raster_style = json.dumps(
|
||||
{
|
||||
RasterStyleProperty.WORLD_COLOR.value: (1, 1, 1),
|
||||
RasterStyleProperty.RENDER_ENGINE.value: "BLENDER_WORKBENCH",
|
||||
RasterStyleProperty.RENDER_TRANSPARENT.value: False,
|
||||
RasterStyleProperty.SHADING_SHOW_OBJECT_OUTLINE.value: True,
|
||||
RasterStyleProperty.SHADING_SHOW_CAVITY.value: True,
|
||||
RasterStyleProperty.SHADING_CAVITY_TYPE.value: "BOTH",
|
||||
RasterStyleProperty.SHADING_CURVATURE_RIDGE_FACTOR.value: 1,
|
||||
RasterStyleProperty.SHADING_CURVATURE_VALLEY_FACTOR.value: 1,
|
||||
RasterStyleProperty.VIEW_TRANSFORM.value: "Standard",
|
||||
RasterStyleProperty.SHADING_LIGHT.value: "STUDIO",
|
||||
RasterStyleProperty.SHADING_COLOR_TYPE.value: "MATERIAL",
|
||||
RasterStyleProperty.SHADING_SINGLE_COLOR.value: (1, 1, 1),
|
||||
RasterStyleProperty.SHADING_SHOW_SHADOWS.value: True,
|
||||
RasterStyleProperty.SHADING_SHADOW_INTENSITY.value: 0.5,
|
||||
RasterStyleProperty.DISPLAY_LIGHT_DIRECTION.value: (0.5, 0.5, 0.5),
|
||||
RasterStyleProperty.VIEW_USE_CURVE_MAPPING.value: False,
|
||||
RasterStyleProperty.OVERLAY_SHOW_WIREFRAMES.value: False,
|
||||
RasterStyleProperty.OVERLAY_WIREFRAME_THRESHOLD.value: 0,
|
||||
RasterStyleProperty.OVERLAY_SHOW_FLOOR.value: False,
|
||||
RasterStyleProperty.OVERLAY_SHOW_AXIS_X.value: False,
|
||||
RasterStyleProperty.OVERLAY_SHOW_AXIS_Y.value: False,
|
||||
RasterStyleProperty.OVERLAY_SHOW_AXIS_Z.value: False,
|
||||
RasterStyleProperty.OVERLAY_SHOW_OBJECT_ORIGINS.value: False,
|
||||
RasterStyleProperty.OVERLAY_SHOW_RELATIONSHIP_LINES.value: False,
|
||||
}
|
||||
)
|
||||
drawing_style = bpy.context.scene.DocProperties.drawing_styles.add()
|
||||
drawing_style.name = "Blender Default"
|
||||
drawing_style.render_type = "DEFAULT"
|
||||
bpy.ops.bim.save_drawing_style(index="2")
|
||||
AuthoringData.type_thumbnails = {}
|
||||
AuthoringData.type_thumbnails = {}
|
||||
|
||||
@@ -24,6 +24,7 @@ import zipfile
|
||||
import tempfile
|
||||
import ifcopenshell
|
||||
import blenderbim.bim.handler
|
||||
import blenderbim.tool as tool
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@@ -35,7 +36,6 @@ class IfcStore:
|
||||
cache_path = None
|
||||
id_map = {}
|
||||
guid_map = {}
|
||||
deleted_ids = set()
|
||||
edited_objs = set()
|
||||
pset_template_path = ""
|
||||
pset_template_file = None
|
||||
@@ -62,7 +62,6 @@ class IfcStore:
|
||||
IfcStore.cache_path = None
|
||||
IfcStore.id_map = {}
|
||||
IfcStore.guid_map = {}
|
||||
IfcStore.deleted_ids = set()
|
||||
IfcStore.edited_objs = set()
|
||||
IfcStore.pset_template_path = ""
|
||||
IfcStore.pset_template_file = None
|
||||
@@ -277,23 +276,6 @@ class IfcStore:
|
||||
data = {"id": element.id(), "obj": obj.name}
|
||||
IfcStore.commit_link_element(data)
|
||||
|
||||
@staticmethod
|
||||
def delete_element(element):
|
||||
IfcStore.deleted_ids.add(element.id())
|
||||
if IfcStore.history:
|
||||
data = {"id": element.id()}
|
||||
IfcStore.history[-1]["operations"].append(
|
||||
{"rollback": IfcStore.rollback_delete_element, "commit": IfcStore.commit_delete_element, "data": data}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def rollback_delete_element(data):
|
||||
IfcStore.deleted_ids.remove(data["id"])
|
||||
|
||||
@staticmethod
|
||||
def commit_delete_element(data):
|
||||
IfcStore.deleted_ids.add(data["id"])
|
||||
|
||||
@staticmethod
|
||||
def link_element(element, obj):
|
||||
existing_obj = IfcStore.id_map.get(element.id(), None)
|
||||
@@ -371,7 +353,7 @@ class IfcStore:
|
||||
def unlink_element(element=None, obj=None):
|
||||
if element is None:
|
||||
try:
|
||||
element = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ import ifcopenshell.util.geolocation
|
||||
import blenderbim.tool as tool
|
||||
from itertools import chain, accumulate
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.module.drawing.prop import get_diagram_scales
|
||||
from blenderbim.bim.module.drawing.prop import ANNOTATION_TYPES_DATA
|
||||
|
||||
|
||||
class FileCopy(threading.Thread):
|
||||
@@ -60,7 +60,6 @@ class MaterialCreator:
|
||||
|
||||
def create(self, element, obj, mesh):
|
||||
self.mesh = mesh
|
||||
self.obj = obj
|
||||
if (hasattr(element, "Representation") and not element.Representation) or (
|
||||
hasattr(element, "RepresentationMaps") and not element.RepresentationMaps
|
||||
):
|
||||
@@ -140,11 +139,11 @@ class MaterialCreator:
|
||||
def assign_material_slots_to_faces(self):
|
||||
if "ios_materials" not in self.mesh or not self.mesh["ios_materials"]:
|
||||
return
|
||||
if len(self.obj.material_slots) == 1:
|
||||
if len(self.mesh.materials) == 1:
|
||||
return
|
||||
material_to_slot = {}
|
||||
for i, material in enumerate(self.mesh["ios_materials"]):
|
||||
slot_index = self.obj.material_slots.find(self.styles[material].name)
|
||||
slot_index = self.mesh.materials.find(self.styles[material].name)
|
||||
material_to_slot[i] = slot_index
|
||||
|
||||
if len(self.mesh.polygons) == len(self.mesh["ios_material_ids"]):
|
||||
@@ -179,6 +178,11 @@ class IfcImporter:
|
||||
self.settings.set_deflection_tolerance(self.ifc_import_settings.deflection_tolerance)
|
||||
self.settings.set_angular_tolerance(self.ifc_import_settings.angular_tolerance)
|
||||
self.settings.set(self.settings.STRICT_TOLERANCE, True)
|
||||
self.settings_curve = ifcopenshell.geom.settings()
|
||||
self.settings_curve.set_deflection_tolerance(self.ifc_import_settings.deflection_tolerance)
|
||||
self.settings_curve.set_angular_tolerance(self.ifc_import_settings.angular_tolerance)
|
||||
self.settings_curve.set(self.settings_curve.STRICT_TOLERANCE, True)
|
||||
self.settings_curve.set(self.settings_curve.INCLUDE_CURVES, True)
|
||||
self.settings_native = ifcopenshell.geom.settings()
|
||||
self.settings_native.set(self.settings_native.INCLUDE_CURVES, True)
|
||||
self.settings_2d = ifcopenshell.geom.settings()
|
||||
@@ -242,6 +246,8 @@ class IfcImporter:
|
||||
self.profile_code("Create native elements")
|
||||
self.create_elements()
|
||||
self.profile_code("Create elements")
|
||||
self.create_generic_elements(self.annotations)
|
||||
self.profile_code("Create annotations")
|
||||
self.create_grids()
|
||||
self.profile_code("Create grids")
|
||||
self.create_spatial_elements()
|
||||
@@ -321,10 +327,13 @@ class IfcImporter:
|
||||
if isinstance(self.elements, set):
|
||||
self.elements = list(self.elements)
|
||||
# TODO: enable filtering for annotations
|
||||
self.annotations = set(self.file.by_type("IfcAnnotation"))
|
||||
self.annotations = set([a for a in self.file.by_type("IfcAnnotation") if not a.HasAssignments])
|
||||
else:
|
||||
self.elements = self.file.by_type("IfcElement")
|
||||
self.annotations = set(self.file.by_type("IfcAnnotation"))
|
||||
if self.file.schema in ("IFC2X3", "IFC4"):
|
||||
self.elements = self.file.by_type("IfcElement") + self.file.by_type("IfcProxy")
|
||||
else:
|
||||
self.elements = self.file.by_type("IfcElement")
|
||||
self.annotations = set([a for a in self.file.by_type("IfcAnnotation") if not a.HasAssignments])
|
||||
|
||||
self.elements = [e for e in self.elements if not e.is_a("IfcFeatureElement")]
|
||||
if self.ifc_import_settings.is_coordinating:
|
||||
@@ -335,23 +344,7 @@ class IfcImporter:
|
||||
if self.ifc_import_settings.has_filter or offset or offset_limit < len(self.elements):
|
||||
self.element_types = set([ifcopenshell.util.element.get_type(e) for e in self.elements])
|
||||
else:
|
||||
if self.file.schema == "IFC2X3":
|
||||
self.element_types = set(
|
||||
self.file.by_type("IfcElementType")
|
||||
+ self.file.by_type("IfcDoorStyle")
|
||||
+ self.file.by_type("IfcWindowStyle")
|
||||
)
|
||||
elif self.file.schema == "IFC4":
|
||||
self.element_types = set(
|
||||
self.file.by_type("IfcElementType")
|
||||
+ self.file.by_type("IfcDoorStyle")
|
||||
+ self.file.by_type("IfcWindowStyle")
|
||||
+ self.file.by_type("IfcSpatialElementType")
|
||||
)
|
||||
else:
|
||||
self.element_types = set(
|
||||
self.file.by_type("IfcElementType") + self.file.by_type("IfcSpatialElementType")
|
||||
)
|
||||
self.element_types = set(self.file.by_type("IfcTypeProduct"))
|
||||
|
||||
if self.ifc_import_settings.has_filter and self.ifc_import_settings.should_filter_spatial_elements:
|
||||
self.spatial_elements = self.get_spatial_elements_filtered_by_elements(self.elements)
|
||||
@@ -388,7 +381,7 @@ class IfcImporter:
|
||||
representations = self.get_transformed_body_representations(element.Representation.Representations)
|
||||
|
||||
# Single swept disk solids (e.g. rebar) are better natively represented as beveled curves
|
||||
if self.is_native_swept_disk_solid(representations):
|
||||
if self.is_native_swept_disk_solid(element, representations):
|
||||
self.native_data[element.GlobalId] = {
|
||||
"representations": representations,
|
||||
"representation": self.get_body_representation(element.Representation.Representations),
|
||||
@@ -416,7 +409,11 @@ class IfcImporter:
|
||||
}
|
||||
return True
|
||||
|
||||
def is_native_swept_disk_solid(self, representations):
|
||||
def is_native_swept_disk_solid(self, element, representations):
|
||||
# detect BBIM Railings to represent them with meshes and not curves
|
||||
if tool.Pset.get_element_pset(element, "BBIM_Railing"):
|
||||
return False
|
||||
|
||||
for representation in representations:
|
||||
items = representation["raw"].Items or [] # Be forgiving of invalid IFCs because Revit :(
|
||||
if len(items) == 1 and items[0].is_a("IfcSweptDiskSolid"):
|
||||
@@ -695,28 +692,31 @@ class IfcImporter:
|
||||
|
||||
def create_generic_elements(self, elements):
|
||||
# Based on my experience in viewing BIM models, representations are prioritised as follows:
|
||||
# 1. 3D Body, 2. 2D Plans, 3. Point clouds, 4. No representation
|
||||
# If an element has a representation that doesn't follow 1, 2, or 3, it will not show by default.
|
||||
# 1. 3D Body, 2. 2D Body, 3. 2D Plans / annotations, 4. Point clouds, 5. No representation
|
||||
# If an element has a representation that doesn't follow 1, 2, 3, or 4, it will not show by default.
|
||||
# The user can load them later if they want to view them.
|
||||
products = self.create_products(elements)
|
||||
elements -= products
|
||||
products = self.create_curve_products(elements)
|
||||
products = self.create_products(elements, settings=self.settings_curve)
|
||||
elements -= products
|
||||
products = self.create_products(elements, settings=self.settings_2d)
|
||||
elements -= products
|
||||
products = self.create_pointclouds(elements)
|
||||
elements -= products
|
||||
for element in elements:
|
||||
self.create_product(element)
|
||||
|
||||
def create_products(self, products):
|
||||
def create_products(self, products, settings=None):
|
||||
if settings is None:
|
||||
settings = self.settings
|
||||
|
||||
results = set()
|
||||
if not products:
|
||||
return results
|
||||
if self.ifc_import_settings.should_use_cpu_multiprocessing:
|
||||
iterator = ifcopenshell.geom.iterator(
|
||||
self.settings, self.file, multiprocessing.cpu_count(), include=products
|
||||
)
|
||||
iterator = ifcopenshell.geom.iterator(settings, self.file, multiprocessing.cpu_count(), include=products)
|
||||
else:
|
||||
iterator = ifcopenshell.geom.iterator(self.settings, self.file, include=products)
|
||||
iterator = ifcopenshell.geom.iterator(settings, self.file, include=products)
|
||||
if self.ifc_import_settings.should_cache:
|
||||
cache = IfcStore.get_cache()
|
||||
if cache:
|
||||
@@ -783,10 +783,10 @@ class IfcImporter:
|
||||
self.structural_collection.children.link(self.structural_connection_collection)
|
||||
self.project["blender"].children.link(self.structural_collection)
|
||||
|
||||
self.create_curve_products(self.file.by_type("IfcStructuralCurveMember"))
|
||||
self.create_curve_products(self.file.by_type("IfcStructuralCurveConnection"))
|
||||
self.create_curve_products(self.file.by_type("IfcStructuralSurfaceMember"))
|
||||
self.create_curve_products(self.file.by_type("IfcStructuralSurfaceConnection"))
|
||||
self.create_products(self.file.by_type("IfcStructuralCurveMember"), settings=self.settings_2d)
|
||||
self.create_products(self.file.by_type("IfcStructuralCurveConnection"), settings=self.settings_2d)
|
||||
self.create_products(self.file.by_type("IfcStructuralSurfaceMember"), settings=self.settings_2d)
|
||||
self.create_products(self.file.by_type("IfcStructuralSurfaceConnection"), settings=self.settings_2d)
|
||||
self.create_structural_point_connections()
|
||||
|
||||
def create_structural_point_connections(self):
|
||||
@@ -874,40 +874,6 @@ class IfcImporter:
|
||||
self.link_element(product, obj)
|
||||
return product
|
||||
|
||||
def create_curve_products(self, products):
|
||||
results = set()
|
||||
if not products:
|
||||
return results
|
||||
if self.ifc_import_settings.should_use_cpu_multiprocessing:
|
||||
iterator = ifcopenshell.geom.iterator(
|
||||
self.settings_2d, self.file, multiprocessing.cpu_count(), include=products
|
||||
)
|
||||
else:
|
||||
iterator = ifcopenshell.geom.iterator(self.settings_2d, self.file, include=products)
|
||||
if self.ifc_import_settings.should_cache:
|
||||
cache = IfcStore.get_cache()
|
||||
if cache:
|
||||
iterator.set_cache(cache)
|
||||
valid_file = iterator.initialize()
|
||||
if not valid_file:
|
||||
return results
|
||||
checkpoint = time.time()
|
||||
total = 0
|
||||
while True:
|
||||
total += 1
|
||||
if total % 250 == 0:
|
||||
print("{} elements processed in {:.2f}s ...".format(total, time.time() - checkpoint))
|
||||
checkpoint = time.time()
|
||||
shape = iterator.get()
|
||||
if shape:
|
||||
product = self.file.by_id(shape.id)
|
||||
self.create_product(product, shape)
|
||||
results.add(product)
|
||||
if not iterator.next():
|
||||
break
|
||||
print("Done creating geometry")
|
||||
return results
|
||||
|
||||
def create_product(self, element, shape=None, mesh=None):
|
||||
if element is None:
|
||||
return
|
||||
@@ -921,9 +887,6 @@ class IfcImporter:
|
||||
|
||||
if mesh:
|
||||
pass
|
||||
elif element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
|
||||
mesh = self.create_camera(element, shape)
|
||||
tool.Loader.link_mesh(shape, mesh)
|
||||
elif element.is_a("IfcAnnotation") and self.is_curve_annotation(element) and shape:
|
||||
mesh = self.create_curve(element, shape)
|
||||
tool.Loader.link_mesh(shape, mesh)
|
||||
@@ -1348,14 +1311,20 @@ class IfcImporter:
|
||||
bpy.context.scene.unit_settings.length_unit = "FEET"
|
||||
elif unit.is_a("IfcNamedUnit") and unit.UnitType == "AREAUNIT":
|
||||
name = unit.Name if unit.is_a("IfcSIUnit") else unit.Name.lower()
|
||||
bpy.context.scene.BIMProperties.area_unit = "{}{}".format(
|
||||
unit.Prefix + "/" if hasattr(unit, "Prefix") and unit.Prefix else "", name
|
||||
)
|
||||
try:
|
||||
bpy.context.scene.BIMProperties.area_unit = "{}{}".format(
|
||||
unit.Prefix + "/" if hasattr(unit, "Prefix") and unit.Prefix else "", name
|
||||
)
|
||||
except: # Probably an invalid unit.
|
||||
bpy.context.scene.BIMProperties.area_unit = "SQUARE_METRE"
|
||||
elif unit.is_a("IfcNamedUnit") and unit.UnitType == "VOLUMEUNIT":
|
||||
name = unit.Name if unit.is_a("IfcSIUnit") else unit.Name.lower()
|
||||
bpy.context.scene.BIMProperties.volume_unit = "{}{}".format(
|
||||
unit.Prefix + "/" if hasattr(unit, "Prefix") and unit.Prefix else "", name
|
||||
)
|
||||
try:
|
||||
bpy.context.scene.BIMProperties.volume_unit = "{}{}".format(
|
||||
unit.Prefix + "/" if hasattr(unit, "Prefix") and unit.Prefix else "", name
|
||||
)
|
||||
except: # Probably an invalid unit.
|
||||
bpy.context.scene.BIMProperties.volume_unit = "CUBIC_METRE"
|
||||
|
||||
def create_project(self):
|
||||
project = self.file.by_type("IfcProject")[0]
|
||||
@@ -1746,14 +1715,8 @@ class IfcImporter:
|
||||
bpy.context.scene.collection.objects.link(obj)
|
||||
|
||||
def is_curve_annotation(self, element):
|
||||
return element.ObjectType in [
|
||||
"DIMENSION",
|
||||
"EQUAL_DIMENSION",
|
||||
"PLAN_LEVEL",
|
||||
"SECTION_LEVEL",
|
||||
"STAIR_ARROW",
|
||||
"TEXT_LEADER",
|
||||
]
|
||||
object_type = element.ObjectType
|
||||
return object_type in ANNOTATION_TYPES_DATA and ANNOTATION_TYPES_DATA[object_type][3] == "curve"
|
||||
|
||||
def get_drawing_group(self, element):
|
||||
for rel in element.HasAssignments or []:
|
||||
@@ -1834,54 +1797,6 @@ class IfcImporter:
|
||||
):
|
||||
return representation.Items[0].MappingTarget
|
||||
|
||||
def create_camera(self, element, shape):
|
||||
if hasattr(shape, "geometry"):
|
||||
geometry = shape.geometry
|
||||
else:
|
||||
geometry = shape
|
||||
|
||||
v = geometry.verts
|
||||
x = [v[i] for i in range(0, len(v), 3)]
|
||||
y = [v[i + 1] for i in range(0, len(v), 3)]
|
||||
z = [v[i + 2] for i in range(0, len(v), 3)]
|
||||
width = max(x) - min(x)
|
||||
height = max(y) - min(y)
|
||||
depth = max(z) - min(z)
|
||||
|
||||
camera = bpy.data.cameras.new(tool.Loader.get_mesh_name(geometry))
|
||||
camera.type = "ORTHO"
|
||||
camera.ortho_scale = width if width > height else height
|
||||
camera.clip_end = depth
|
||||
|
||||
if width > height:
|
||||
camera.BIMCameraProperties.raster_x = 1000
|
||||
camera.BIMCameraProperties.raster_y = round(1000 * (height / width))
|
||||
else:
|
||||
camera.BIMCameraProperties.raster_x = round(1000 * (width / height))
|
||||
camera.BIMCameraProperties.raster_y = 1000
|
||||
|
||||
psets = ifcopenshell.util.element.get_psets(element)
|
||||
pset = psets.get("EPset_Drawing")
|
||||
if pset:
|
||||
if "TargetView" in pset:
|
||||
camera.BIMCameraProperties.target_view = pset["TargetView"]
|
||||
if "Scale" in pset:
|
||||
valid_scales = [
|
||||
i[0] for i in get_diagram_scales(None, bpy.context) if pset["Scale"] == i[0].split("|")[-1]
|
||||
]
|
||||
if valid_scales:
|
||||
camera.BIMCameraProperties.diagram_scale = valid_scales[0]
|
||||
else:
|
||||
camera.BIMCameraProperties.diagram_scale = "CUSTOM"
|
||||
camera.BIMCameraProperties.custom_diagram_scale = pset["Scale"]
|
||||
if "HasUnderlay" in pset:
|
||||
camera.BIMCameraProperties.has_underlay = pset["HasUnderlay"]
|
||||
if "HasLinework" in pset:
|
||||
camera.BIMCameraProperties.has_linework = pset["HasLinework"]
|
||||
if "HasAnnotation" in pset:
|
||||
camera.BIMCameraProperties.has_annotation = pset["HasAnnotation"]
|
||||
return camera
|
||||
|
||||
def create_curve(self, element, shape):
|
||||
if hasattr(shape, "geometry"):
|
||||
geometry = shape.geometry
|
||||
|
||||
@@ -43,13 +43,17 @@ class BIM_OT_assign_object(bpy.types.Operator, Operator):
|
||||
related_object: bpy.props.IntProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
core.assign_object(
|
||||
tool.Ifc,
|
||||
tool.Aggregate,
|
||||
tool.Collector,
|
||||
relating_obj=tool.Ifc.get_object(tool.Ifc.get().by_id(self.relating_object)),
|
||||
related_obj=tool.Ifc.get_object(tool.Ifc.get().by_id(self.related_object)),
|
||||
)
|
||||
for obj in bpy.context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
core.assign_object(
|
||||
tool.Ifc,
|
||||
tool.Aggregate,
|
||||
tool.Collector,
|
||||
relating_obj=tool.Ifc.get_object(tool.Ifc.get().by_id(self.relating_object)),
|
||||
related_obj=obj,
|
||||
)
|
||||
|
||||
|
||||
class BIM_OT_unassign_object(bpy.types.Operator, Operator):
|
||||
|
||||
@@ -59,9 +59,13 @@ class MaterialAttributesData:
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.data = {"attributes": cls.attributes()}
|
||||
cls.data = {"ifc_definition_id": cls.ifc_definition_id(), "attributes": cls.attributes()}
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def ifc_definition_id(cls):
|
||||
return bpy.context.active_object.active_material.BIMObjectProperties.ifc_definition_id
|
||||
|
||||
@classmethod
|
||||
def attributes(cls):
|
||||
results = []
|
||||
|
||||
@@ -50,7 +50,22 @@ class EnableEditingAttributes(bpy.types.Operator):
|
||||
oprops = obj.BIMObjectProperties
|
||||
props = obj.BIMAttributeProperties
|
||||
props.attributes.clear()
|
||||
blenderbim.bim.helper.import_attributes2(tool.Ifc.get().by_id(oprops.ifc_definition_id), props.attributes)
|
||||
|
||||
def callback(name, prop, data):
|
||||
if name in ("RefLatitude", "RefLongitude"):
|
||||
new = props.attributes.add()
|
||||
new.name = name
|
||||
new.is_null = data[name] is None
|
||||
new.is_optional = True
|
||||
new.data_type = "string"
|
||||
new.ifc_class = data["type"]
|
||||
new.string_value = "" if new.is_null else json.dumps(data[name])
|
||||
blenderbim.bim.helper.add_attribute_description(new)
|
||||
new.description += " The degrees, minutes and seconds should follow this format : [12,34,56]"
|
||||
|
||||
blenderbim.bim.helper.import_attributes2(
|
||||
tool.Ifc.get().by_id(oprops.ifc_definition_id), props.attributes, callback=callback
|
||||
)
|
||||
props.is_editing_attributes = True
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -87,7 +102,19 @@ class EditAttributes(bpy.types.Operator, Operator):
|
||||
obj = bpy.data.materials.get(self.obj)
|
||||
props = obj.BIMAttributeProperties
|
||||
product = tool.Ifc.get_entity(obj)
|
||||
attributes = blenderbim.bim.helper.export_attributes(props.attributes)
|
||||
|
||||
def callback(attributes, prop):
|
||||
if prop.name in ("RefLatitude", "RefLongitude"):
|
||||
if prop.is_null:
|
||||
attributes[prop.name] = None
|
||||
else:
|
||||
try:
|
||||
attributes[prop.name] = json.loads(prop.string_value)
|
||||
except:
|
||||
attributes[prop.name] = None
|
||||
return True
|
||||
|
||||
attributes = blenderbim.bim.helper.export_attributes(props.attributes, callback=callback)
|
||||
ifcopenshell.api.run("attribute.edit_attributes", self.file, product=product, attributes=attributes)
|
||||
bpy.ops.bim.disable_editing_attributes(obj=obj.name, obj_type=self.obj_type)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -94,4 +94,10 @@ class BIM_PT_material_attributes(Panel):
|
||||
def draw(self, context):
|
||||
if not MaterialAttributesData.is_loaded:
|
||||
MaterialAttributesData.load()
|
||||
elif (
|
||||
context.active_object.active_material.BIMObjectProperties.ifc_definition_id
|
||||
!= MaterialAttributesData.data["ifc_definition_id"]
|
||||
):
|
||||
MaterialAttributesData.load()
|
||||
|
||||
draw_ui(context, self.layout, "Material", MaterialAttributesData.data["attributes"])
|
||||
|
||||
@@ -218,7 +218,7 @@ class EditBcfTopicName(bpy.types.Operator):
|
||||
props = context.scene.BCFProperties
|
||||
blender_topic = props.active_topic
|
||||
bcfxml = bcfstore.BcfStore.get_bcfxml()
|
||||
topic = bcfxml.topics[blender_topic.name]
|
||||
topic = bcfxml.topics[blender_topic.name].topic
|
||||
topic.title = blender_topic.title
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -233,7 +233,7 @@ class EditBcfTopic(bpy.types.Operator):
|
||||
blender_topic = props.active_topic
|
||||
bcfxml = bcfstore.BcfStore.get_bcfxml()
|
||||
|
||||
topic = bcfxml.topics[blender_topic.name]
|
||||
topic = bcfxml.topics[blender_topic.name].topic
|
||||
topic.title = blender_topic.title or None
|
||||
topic.priority = blender_topic.priority or None
|
||||
topic.due_date = blender_topic.due_date or None
|
||||
@@ -940,7 +940,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
|
||||
context.area.type = old
|
||||
for global_id in exception_global_ids:
|
||||
obj = IfcStore.get_element(global_id)
|
||||
if obj:
|
||||
if obj and bpy.context.view_layer.objects.get(obj.name):
|
||||
obj.hide_set(True)
|
||||
else:
|
||||
objs = []
|
||||
|
||||
@@ -20,28 +20,37 @@ import bpy
|
||||
from . import ui, operator, prop
|
||||
|
||||
classes = (
|
||||
operator.AddBoundary,
|
||||
operator.ColourByRelatedBuildingElement,
|
||||
operator.DisableEditingBoundary,
|
||||
operator.DisableEditingBoundaryGeometry,
|
||||
operator.EditBoundaryAttributes,
|
||||
operator.EditBoundaryGeometry,
|
||||
operator.EnableEditingBoundary,
|
||||
operator.EnableEditingBoundaryGeometry,
|
||||
operator.HideBoundaries,
|
||||
operator.LoadBoundary,
|
||||
operator.LoadProjectSpaceBoundaries,
|
||||
operator.LoadSpaceBoundaries,
|
||||
operator.LoadBoundary,
|
||||
operator.SelectRelatedElementBoundaries,
|
||||
operator.SelectProjectBoundaries,
|
||||
operator.SelectRelatedElementBoundaries,
|
||||
operator.SelectRelatedElementTypeBoundaries,
|
||||
operator.SelectSpaceBoundaries,
|
||||
operator.ShowBoundaries,
|
||||
operator.UpdateBoundaryGeometry,
|
||||
ui.BIM_PT_Boundary,
|
||||
ui.BIM_PT_SceneBoundaries,
|
||||
ui.BIM_PT_SpaceBoundaries,
|
||||
prop.BIMBoundaryProperties,
|
||||
prop.BIMObjectBoundaryProperties,
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
bpy.types.Object.bim_boundary_properties = bpy.props.PointerProperty(type=prop.BIMBoundaryProperties)
|
||||
bpy.types.Scene.BIMBoundaryProperties = bpy.props.PointerProperty(type=prop.BIMBoundaryProperties)
|
||||
bpy.types.Object.bim_boundary_properties = bpy.props.PointerProperty(type=prop.BIMObjectBoundaryProperties)
|
||||
|
||||
|
||||
def unregister():
|
||||
del bpy.types.Scene.BIMBoundaryProperties
|
||||
del bpy.types.Object.bim_boundary_properties
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2023 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on 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.
|
||||
#
|
||||
# BlenderBIM Add-on 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 BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import gpu
|
||||
import bmesh
|
||||
import blenderbim.tool as tool
|
||||
from bpy.types import SpaceView3D
|
||||
from mathutils import Vector
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
|
||||
|
||||
class BoundaryDecorator:
|
||||
installed = None
|
||||
|
||||
@classmethod
|
||||
def install(cls, context):
|
||||
if cls.installed:
|
||||
cls.uninstall()
|
||||
handler = cls()
|
||||
cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW")
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls):
|
||||
try:
|
||||
SpaceView3D.draw_handler_remove(cls.installed, "WINDOW")
|
||||
except ValueError:
|
||||
pass
|
||||
cls.installed = None
|
||||
|
||||
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 __call__(self, context):
|
||||
self.addon_prefs = context.preferences.addons["blenderbim"].preferences
|
||||
selected_elements_color = self.addon_prefs.decorator_color_selected
|
||||
unselected_elements_color = self.addon_prefs.decorator_color_unselected
|
||||
special_elements_color = self.addon_prefs.decorator_color_special
|
||||
|
||||
def transparent_color(color, alpha=0.1):
|
||||
color = [i for i in color]
|
||||
color[3] = alpha
|
||||
return color
|
||||
|
||||
gpu.state.point_size_set(6)
|
||||
gpu.state.blend_set("ALPHA")
|
||||
|
||||
self.line_shader = gpu.shader.from_builtin("3D_POLYLINE_UNIFORM_COLOR")
|
||||
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", 2.0)
|
||||
|
||||
# general shader
|
||||
self.shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
|
||||
|
||||
selected_vertices = []
|
||||
selected_edges = []
|
||||
selected_tris = []
|
||||
unselected_vertices = []
|
||||
unselected_edges = []
|
||||
unselected_tris = []
|
||||
|
||||
for boundary in context.scene.BIMBoundaryProperties.boundaries:
|
||||
obj = boundary.obj
|
||||
if not obj or not obj.data: # A boundary may not have data if it has no connection geometry
|
||||
continue
|
||||
|
||||
if obj.mode == "EDIT":
|
||||
continue # A profile decorator or something else is used here.
|
||||
else:
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(obj.data)
|
||||
obj.data.calc_loop_triangles()
|
||||
|
||||
if obj.select_get():
|
||||
offset = len(selected_vertices)
|
||||
selected_vertices.extend([tuple(obj.matrix_world @ v.co) for v in bm.verts])
|
||||
selected_edges.extend([tuple([v.index + offset for v in e.verts]) for e in bm.edges])
|
||||
selected_tris.extend([tuple([i + offset for i in t.vertices]) for t in obj.data.loop_triangles])
|
||||
else:
|
||||
offset = len(unselected_vertices)
|
||||
unselected_vertices.extend([tuple(obj.matrix_world @ v.co) for v in bm.verts])
|
||||
unselected_edges.extend([tuple([v.index + offset for v in e.verts]) for e in bm.edges])
|
||||
unselected_tris.extend([tuple([i + offset for i in t.vertices]) for t in obj.data.loop_triangles])
|
||||
|
||||
if obj.mode != "EDIT":
|
||||
bm.free()
|
||||
|
||||
if unselected_edges:
|
||||
self.draw_batch("LINES", unselected_vertices, special_elements_color, unselected_edges)
|
||||
self.draw_batch("TRIS", unselected_vertices, transparent_color(special_elements_color), unselected_tris)
|
||||
if selected_edges:
|
||||
self.draw_batch("LINES", selected_vertices, selected_elements_color, selected_edges)
|
||||
self.draw_batch("TRIS", selected_vertices, transparent_color(selected_elements_color), selected_tris)
|
||||
@@ -18,14 +18,23 @@
|
||||
|
||||
import bpy
|
||||
import bmesh
|
||||
import mathutils
|
||||
import logging
|
||||
import shapely
|
||||
import mathutils
|
||||
import numpy as np
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.util.placement
|
||||
import ifcopenshell.util.unit
|
||||
import ifcopenshell.util.shape
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.placement
|
||||
import ifcopenshell.util.representation
|
||||
import blenderbim.tool as tool
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
import blenderbim.bim.import_ifc as import_ifc
|
||||
from math import pi, inf
|
||||
from mathutils import Vector, Matrix
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.module.model.decorator import ProfileDecorator
|
||||
from blenderbim.bim.module.boundary.decorator import BoundaryDecorator
|
||||
|
||||
|
||||
def get_boundaries_collection(blender_space):
|
||||
@@ -38,6 +47,20 @@ def get_boundaries_collection(blender_space):
|
||||
return boundaries_collection
|
||||
|
||||
|
||||
def disable_editing_boundary_geometry(context):
|
||||
ProfileDecorator.uninstall()
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
|
||||
obj = context.active_object
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
|
||||
old_mesh = obj.data
|
||||
loader = Loader()
|
||||
obj.data = loader.create_mesh(element)
|
||||
tool.Geometry.delete_data(old_mesh)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class Loader:
|
||||
def __init__(self):
|
||||
self.ifc_file = None
|
||||
@@ -119,7 +142,7 @@ class Loader:
|
||||
|
||||
class LoadProjectSpaceBoundaries(bpy.types.Operator):
|
||||
bl_idname = "bim.load_project_space_boundaries"
|
||||
bl_label = "Load all project space boundaries"
|
||||
bl_label = "Load All Project Space Boundaries"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
@@ -131,7 +154,7 @@ class LoadProjectSpaceBoundaries(bpy.types.Operator):
|
||||
|
||||
class LoadBoundary(bpy.types.Operator):
|
||||
bl_idname = "bim.load_boundary"
|
||||
bl_label = "Load boundary"
|
||||
bl_label = "Load Boundary"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
boundary_id: bpy.props.IntProperty()
|
||||
|
||||
@@ -147,7 +170,7 @@ class LoadBoundary(bpy.types.Operator):
|
||||
|
||||
class LoadSpaceBoundaries(bpy.types.Operator):
|
||||
bl_idname = "bim.load_space_boundaries"
|
||||
bl_label = "Load selected space boundaries"
|
||||
bl_label = "Load Selected Space Boundaries"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
@@ -168,7 +191,7 @@ def get_element_boundaries(element):
|
||||
|
||||
class SelectRelatedElementBoundaries(bpy.types.Operator):
|
||||
bl_idname = "bim.select_related_element_boundaries"
|
||||
bl_label = "Select related element space boundaries"
|
||||
bl_label = "Select Related Element Space Boundaries"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
related_element: bpy.props.IntProperty()
|
||||
|
||||
@@ -185,7 +208,7 @@ class SelectRelatedElementBoundaries(bpy.types.Operator):
|
||||
|
||||
class SelectRelatedElementTypeBoundaries(bpy.types.Operator):
|
||||
bl_idname = "bim.select_related_element_type_boundaries"
|
||||
bl_label = "Select related element type space boundaries"
|
||||
bl_label = "Select Related Element Type Space Boundaries"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
related_element: bpy.props.IntProperty()
|
||||
|
||||
@@ -208,7 +231,7 @@ class SelectRelatedElementTypeBoundaries(bpy.types.Operator):
|
||||
|
||||
class SelectSpaceBoundaries(bpy.types.Operator):
|
||||
bl_idname = "bim.select_space_boundaries"
|
||||
bl_label = "Select all space boundaries"
|
||||
bl_label = "Select All Space Boundaries"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
@@ -224,7 +247,7 @@ class SelectSpaceBoundaries(bpy.types.Operator):
|
||||
|
||||
class SelectProjectBoundaries(bpy.types.Operator):
|
||||
bl_idname = "bim.select_project_space_boundaries"
|
||||
bl_label = "Select all project space boundaries"
|
||||
bl_label = "Select All Project Space Boundaries"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
@@ -260,7 +283,7 @@ def get_colour(ifc_boundary):
|
||||
|
||||
class ColourByRelatedBuildingElement(bpy.types.Operator):
|
||||
bl_idname = "bim.colour_by_related_building_element"
|
||||
bl_label = "Apply colour based on related building elements"
|
||||
bl_label = "Apply Colour Based on Related Building Elements"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
@@ -308,7 +331,7 @@ EDITABLE_ATTRIBUTES = {
|
||||
|
||||
class EnableEditingBoundary(bpy.types.Operator):
|
||||
bl_idname = "bim.enable_editing_boundary"
|
||||
bl_label = "Edit boundary relations"
|
||||
bl_label = "Edit Boundary Relations"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
@@ -327,7 +350,7 @@ class EnableEditingBoundary(bpy.types.Operator):
|
||||
|
||||
class DisableEditingBoundary(bpy.types.Operator):
|
||||
bl_idname = "bim.disable_editing_boundary"
|
||||
bl_label = "Disable editing boundary relations"
|
||||
bl_label = "Disable Editing Boundary Relations"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
@@ -340,7 +363,7 @@ class DisableEditingBoundary(bpy.types.Operator):
|
||||
|
||||
class EditBoundaryAttributes(bpy.types.Operator):
|
||||
bl_idname = "bim.edit_boundary_attributes"
|
||||
bl_label = "Disable editing boundary relations"
|
||||
bl_label = "Disable Editing Boundary Relations"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
@@ -361,7 +384,7 @@ class EditBoundaryAttributes(bpy.types.Operator):
|
||||
|
||||
class UpdateBoundaryGeometry(bpy.types.Operator):
|
||||
bl_idname = "bim.update_boundary_geometry"
|
||||
bl_label = "Update boundary geometry"
|
||||
bl_label = "Update Boundary Geometry"
|
||||
bl_description = """
|
||||
Update boundary connection geometry from mesh.
|
||||
Mesh must lie on a single plane. It should look like a face or a face with holes.
|
||||
@@ -376,3 +399,357 @@ class UpdateBoundaryGeometry(bpy.types.Operator):
|
||||
settings = tool.Boundary.get_assign_connection_geometry_settings(context.active_object)
|
||||
ifcopenshell.api.run("boundary.assign_connection_geometry", tool.Ifc.get(), **settings)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class EnableEditingBoundaryGeometry(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.enable_editing_boundary_geometry"
|
||||
bl_label = "Enable Editing Boundary Geometry"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.selected_objects
|
||||
|
||||
def _execute(self, context):
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
obj = context.active_object
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
|
||||
if element.ConnectionGeometry.is_a("IfcConnectionSurfaceGeometry"):
|
||||
surface = element.ConnectionGeometry.SurfaceOnRelatingElement
|
||||
tool.Model.import_surface(surface, obj)
|
||||
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
ProfileDecorator.install(context, exit_edit_mode_callback=lambda: disable_editing_boundary_geometry(context))
|
||||
if not bpy.app.background:
|
||||
bpy.ops.wm.tool_set_by_id(tool.Blender.get_viewport_context(), name="bim.cad_tool")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class EditBoundaryGeometry(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.edit_boundary_geometry"
|
||||
bl_label = "Edit Boundary Geometry"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
ProfileDecorator.uninstall()
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
|
||||
obj = context.active_object
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
|
||||
if element.ConnectionGeometry.is_a("IfcConnectionSurfaceGeometry"):
|
||||
surface = tool.Model.export_surface(obj)
|
||||
|
||||
if not surface:
|
||||
|
||||
def msg(self, context):
|
||||
self.layout.label(text="INVALID PROFILE")
|
||||
|
||||
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
|
||||
ProfileDecorator.install(
|
||||
context, exit_edit_mode_callback=lambda: disable_editing_boundary_geometry(context)
|
||||
)
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
return
|
||||
|
||||
old_surface = element.ConnectionGeometry.SurfaceOnRelatingElement
|
||||
for inverse in tool.Ifc.get().get_inverse(old_surface):
|
||||
ifcopenshell.util.element.replace_attribute(inverse, old_surface, surface)
|
||||
ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), old_surface)
|
||||
|
||||
old_mesh = obj.data
|
||||
loader = Loader()
|
||||
obj.data = loader.create_mesh(element)
|
||||
tool.Geometry.delete_data(old_mesh)
|
||||
|
||||
|
||||
class DisableEditingBoundaryGeometry(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.disable_editing_boundary_geometry"
|
||||
bl_label = "Disable Editing Boundary Geometry"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.selected_objects
|
||||
|
||||
def _execute(self, context):
|
||||
return disable_editing_boundary_geometry(context)
|
||||
|
||||
|
||||
class ShowBoundaries(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.show_boundaries"
|
||||
bl_label = "Show Boundaries"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
props = bpy.context.scene.BIMBoundaryProperties
|
||||
loader = Loader()
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not getattr(element, "BoundedBy", None):
|
||||
continue
|
||||
if tool.Ifc.is_moved(obj):
|
||||
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
for rel in element.BoundedBy or []:
|
||||
boundary_obj = loader.load_boundary(rel, obj)
|
||||
tool.Boundary.decorate_boundary(boundary_obj)
|
||||
BoundaryDecorator.install(bpy.context)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class HideBoundaries(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.hide_boundaries"
|
||||
bl_label = "Hide Boundaries"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
to_delete = set()
|
||||
spaces = set()
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
if element.is_a("IfcSpace"):
|
||||
spaces.add(element)
|
||||
elif element.is_a("IfcRelSpaceBoundary"):
|
||||
spaces.add(element.RelatingSpace)
|
||||
|
||||
for element in spaces:
|
||||
for boundary in element.BoundedBy or []:
|
||||
boundary_obj = tool.Ifc.get_object(boundary)
|
||||
if boundary_obj:
|
||||
to_delete.add(boundary_obj)
|
||||
for boundary_obj in to_delete:
|
||||
tool.Ifc.unlink(obj=boundary_obj)
|
||||
bpy.data.objects.remove(boundary_obj)
|
||||
context.scene.BIMBoundaryProperties.boundaries.clear()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_boundary"
|
||||
bl_label = "Add Boundary"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
relating_space = None
|
||||
related_building_element = None
|
||||
relating_space_obj = None
|
||||
related_building_element_obj = None
|
||||
|
||||
objs = context.selected_objects
|
||||
if len(objs) == 2:
|
||||
# The user may select two objects, a space and its related building element
|
||||
for obj in objs:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
if element.is_a("IfcSpace"):
|
||||
relating_space = element
|
||||
relating_space_obj = obj
|
||||
else:
|
||||
related_building_element = element
|
||||
related_building_element_obj = obj
|
||||
elif len(objs) == 1:
|
||||
# Optionally the user may select just the space, and the building element shall be auto-detected
|
||||
|
||||
def msg(self, context):
|
||||
self.layout.label(text="NO ACTIVE STOREY")
|
||||
|
||||
element = tool.Ifc.get_entity(objs[0])
|
||||
if element.is_a("IfcSpace"):
|
||||
relating_space = element
|
||||
relating_space_obj = objs[0]
|
||||
|
||||
target = bpy.context.scene.cursor.location
|
||||
|
||||
collection = context.view_layer.active_layer_collection.collection
|
||||
collection_obj = bpy.data.objects.get(collection.name)
|
||||
if not collection_obj:
|
||||
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
|
||||
return
|
||||
spatial_element = tool.Ifc.get_entity(collection_obj)
|
||||
if not spatial_element:
|
||||
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
|
||||
return
|
||||
|
||||
for subelement in ifcopenshell.util.element.get_decomposition(spatial_element):
|
||||
if not (subelement.is_a("IfcWall") or subelement.is_a("IfcSlab")):
|
||||
continue
|
||||
obj = tool.Ifc.get_object(subelement)
|
||||
if obj:
|
||||
raycast = obj.closest_point_on_mesh(obj.matrix_world.inverted() @ target, distance=0.1)
|
||||
if raycast[0]:
|
||||
related_building_element = subelement
|
||||
related_building_element_obj = obj
|
||||
break
|
||||
|
||||
if not relating_space or not related_building_element:
|
||||
return
|
||||
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(relating_space_obj.data)
|
||||
bmesh.ops.dissolve_limit(bm, angle_limit=pi * 2 / 360, verts=bm.verts, edges=bm.edges)
|
||||
|
||||
target_distance = inf
|
||||
target_face = None
|
||||
for face in bm.faces:
|
||||
centroid = relating_space_obj.matrix_world @ face.calc_center_median()
|
||||
raycast = related_building_element_obj.closest_point_on_mesh(
|
||||
related_building_element_obj.matrix_world.inverted() @ centroid, distance=1
|
||||
)
|
||||
if raycast[0]:
|
||||
distance = (related_building_element_obj.matrix_world @ raycast[1] - centroid).length
|
||||
if distance < target_distance:
|
||||
target_face = face
|
||||
target_distance = distance
|
||||
|
||||
if not target_face:
|
||||
return
|
||||
|
||||
parent_boundary = tool.Ifc.run("root.create_entity", ifc_class=context.scene.BIMModelProperties.boundary_class)
|
||||
|
||||
# Is this right? Or should I use loop?
|
||||
target_face_verts = [v.co.copy() for v in target_face.verts]
|
||||
target_face_matrix = self.get_face_matrix(*[v.copy() for v in target_face_verts[0:3]])
|
||||
target_face_matrix_i = target_face_matrix.inverted()
|
||||
|
||||
target_face_polygon = shapely.Polygon([tuple((target_face_matrix_i @ v).xy) for v in target_face_verts])
|
||||
|
||||
related_building_element_polygon = self.get_flattened_polygon(
|
||||
related_building_element, relating_space_obj, target_face_matrix_i
|
||||
)
|
||||
|
||||
gross_boundary_polygon = target_face_polygon.intersection(related_building_element_polygon)
|
||||
|
||||
if type(gross_boundary_polygon) == shapely.GeometryCollection:
|
||||
for geom in gross_boundary_polygon.geoms:
|
||||
if type(geom) == shapely.Polygon:
|
||||
gross_boundary_polygon = geom
|
||||
break
|
||||
|
||||
# The gross boundary polygon may not be a true gross boundary since it
|
||||
# may have openings already removed, such as in IFC4 Reference View. So
|
||||
# we cheat by using the exterior boundary to mean "gross". Later, we
|
||||
# can use this to check whether or not the opening is relevant to our
|
||||
# space.
|
||||
exterior_boundary_polygon = shapely.Polygon(gross_boundary_polygon.exterior.coords)
|
||||
|
||||
net_boundary_polygon = shapely.Polygon(gross_boundary_polygon)
|
||||
|
||||
inner_boundaries = []
|
||||
|
||||
for rel in getattr(related_building_element, "HasOpenings", []):
|
||||
opening = rel.RelatedOpeningElement
|
||||
filling = None
|
||||
if opening.HasFillings:
|
||||
filling = opening.HasFillings[0].RelatedBuildingElement
|
||||
|
||||
opening_polygon = self.get_flattened_polygon(opening, relating_space_obj, target_face_matrix_i)
|
||||
|
||||
net_boundary_polygon = net_boundary_polygon.difference(opening_polygon)
|
||||
# Only openings that are projected onto our exterior boundary are relevant.
|
||||
if opening_polygon.intersection(exterior_boundary_polygon).area == 0:
|
||||
continue
|
||||
|
||||
connection_geometry = self.create_connection_geometry_from_polygon(opening_polygon, target_face_matrix)
|
||||
boundary = tool.Ifc.run("root.create_entity", ifc_class=context.scene.BIMModelProperties.boundary_class)
|
||||
boundary.RelatingSpace = relating_space
|
||||
boundary.RelatedBuildingElement = filling or related_building_element
|
||||
boundary.ConnectionGeometry = connection_geometry
|
||||
boundary.PhysicalOrVirtualBoundary = "PHYSICAL" if filling else "VIRTUAL"
|
||||
boundary.InternalOrExternalBoundary = "INTERNAL"
|
||||
|
||||
if boundary.is_a("IfcRelSpaceBoundary2ndLevel"):
|
||||
boundary.ParentBoundary = parent_boundary
|
||||
|
||||
connection_geometry = self.create_connection_geometry_from_polygon(net_boundary_polygon, target_face_matrix)
|
||||
parent_boundary.RelatingSpace = relating_space
|
||||
parent_boundary.RelatedBuildingElement = related_building_element
|
||||
parent_boundary.ConnectionGeometry = connection_geometry
|
||||
parent_boundary.PhysicalOrVirtualBoundary = "PHYSICAL"
|
||||
parent_boundary.InternalOrExternalBoundary = "INTERNAL"
|
||||
|
||||
bpy.ops.bim.show_boundaries()
|
||||
obj = tool.Ifc.get_object(parent_boundary)
|
||||
obj.select_set(True)
|
||||
|
||||
def get_face_matrix(self, p1, p2, p3):
|
||||
edge1 = p2 - p1
|
||||
edge2 = p3 - p1
|
||||
normal = edge1.cross(edge2)
|
||||
z_axis = normal.normalized()
|
||||
x_axis = p2 - p1
|
||||
x_axis.normalize()
|
||||
y_axis = z_axis.cross(x_axis)
|
||||
|
||||
mat = Matrix()
|
||||
mat.col[0][:3] = x_axis
|
||||
mat.col[1][:3] = y_axis
|
||||
mat.col[2][:3] = z_axis
|
||||
mat.col[3][:3] = p1
|
||||
|
||||
return mat
|
||||
|
||||
def get_flattened_polygon(self, element, relating_space_obj, target_face_matrix_i):
|
||||
obj = tool.Ifc.get_object(element)
|
||||
if obj and tool.Ifc.is_moved(obj):
|
||||
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
||||
|
||||
space_matrix_i = relating_space_obj.matrix_world.inverted()
|
||||
|
||||
settings = ifcopenshell.geom.settings()
|
||||
if not element.is_a("IfcOpeningElement"):
|
||||
settings.set(settings.DISABLE_OPENING_SUBTRACTIONS, True)
|
||||
settings.set(settings.STRICT_TOLERANCE, True)
|
||||
# geometry = ifcopenshell.geom.create_shape(settings, body)
|
||||
shape = ifcopenshell.geom.create_shape(settings, element)
|
||||
m = shape.transformation.matrix.data
|
||||
mat = Matrix(([m[0], m[3], m[6], m[9]], [m[1], m[4], m[7], m[10]], [m[2], m[5], m[8], m[11]], [0, 0, 0, 1]))
|
||||
verts = [space_matrix_i @ mat @ Vector(v) for v in ifcopenshell.util.shape.get_vertices(shape.geometry)]
|
||||
faces = ifcopenshell.util.shape.get_faces(shape.geometry)
|
||||
polygons = []
|
||||
for face in faces:
|
||||
polygon = shapely.Polygon([tuple((target_face_matrix_i @ verts[vi]).xy) for vi in face])
|
||||
polygons.append(polygon)
|
||||
return shapely.ops.unary_union(polygons)
|
||||
|
||||
def create_connection_geometry_from_polygon(self, polygon, target_face_matrix):
|
||||
surface = self.export_surface(polygon, target_face_matrix)
|
||||
return tool.Ifc.get().createIfcConnectionSurfaceGeometry(surface)
|
||||
|
||||
def export_surface(self, polygon, target_face_matrix):
|
||||
x_axis = target_face_matrix.col[0][:3]
|
||||
z_axis = target_face_matrix.col[2][:3]
|
||||
p1 = target_face_matrix.col[3][:3]
|
||||
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
tool.Model.unit_scale = self.unit_scale
|
||||
|
||||
surface = tool.Ifc.get().createIfcCurveBoundedPlane()
|
||||
surface.BasisSurface = tool.Ifc.get().createIfcPlane(tool.Ifc.get().createIfcAxis2Placement3D(
|
||||
tool.Ifc.get().createIfcCartesianPoint([o / self.unit_scale for o in p1]),
|
||||
tool.Ifc.get().createIfcDirection([float(o) for o in z_axis]),
|
||||
tool.Ifc.get().createIfcDirection([float(o) for o in x_axis]),
|
||||
))
|
||||
|
||||
if tool.Ifc.get().schema != "IFC2X3":
|
||||
points = [tool.Model.convert_si_to_unit(list(co)) for co in polygon.exterior.coords]
|
||||
point_list = tool.Ifc.get().createIfcCartesianPointList2D(points)
|
||||
outer_boundary = tool.Ifc.get().createIfcIndexedPolyCurve(point_list, None, False)
|
||||
|
||||
inner_boundaries = []
|
||||
for interior in polygon.interiors:
|
||||
points = [tool.Model.convert_si_to_unit(list(co)) for co in interior.coords]
|
||||
point_list = tool.Ifc.get().createIfcCartesianPointList2D(points)
|
||||
inner_boundaries.append(tool.Ifc.get().createIfcIndexedPolyCurve(point_list, None, False))
|
||||
else:
|
||||
pass # TODO
|
||||
|
||||
surface.OuterBoundary = outer_boundary
|
||||
surface.InnerBoundaries = inner_boundaries
|
||||
|
||||
return surface
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
import bpy
|
||||
from bpy.types import PropertyGroup
|
||||
from blenderbim.bim.prop import ObjProperty
|
||||
from bpy.props import (
|
||||
PointerProperty,
|
||||
StringProperty,
|
||||
@@ -52,9 +53,13 @@ def element_filter(self, object):
|
||||
return False
|
||||
|
||||
|
||||
class BIMBoundaryProperties(PropertyGroup):
|
||||
class BIMObjectBoundaryProperties(PropertyGroup):
|
||||
is_editing: BoolProperty(name="Is Editing")
|
||||
relating_space: PointerProperty(name="RelatingSpace", type=bpy.types.Object, poll=space_filter)
|
||||
related_building_element: PointerProperty(name="RelatedBuildingElement", type=bpy.types.Object, poll=element_filter)
|
||||
parent_boundary: PointerProperty(name="ParentBoundary", type=bpy.types.Object, poll=boundary_filter)
|
||||
corresponding_boundary: PointerProperty(name="CorrespondingBoundary", type=bpy.types.Object, poll=boundary_filter)
|
||||
|
||||
|
||||
class BIMBoundaryProperties(PropertyGroup):
|
||||
boundaries: bpy.props.CollectionProperty(type=ObjProperty)
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
|
||||
import bpy
|
||||
from blenderbim.bim.module.model.data import AuthoringData
|
||||
from blenderbim.bim.module.model.root import ConstrTypeEntityNotFound
|
||||
from bpy.types import PropertyGroup
|
||||
from math import pi
|
||||
|
||||
@@ -32,3 +31,4 @@ 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)
|
||||
|
||||
@@ -46,7 +46,7 @@ class CadTool(WorkSpaceTool):
|
||||
("bim.cad_hotkey", {"type": "X", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_X")]}),
|
||||
)
|
||||
|
||||
def draw_settings(context, layout, tool):
|
||||
def draw_settings(context, layout, workspace_tool):
|
||||
obj = context.active_object
|
||||
if not obj or not obj.data:
|
||||
return
|
||||
@@ -54,14 +54,19 @@ class CadTool(WorkSpaceTool):
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_Q")
|
||||
if obj.BIMObjectProperties.ifc_definition_id:
|
||||
row.operator("bim.edit_extrusion_profile", text="Save Profile")
|
||||
row.operator("bim.align_view_to_profile", text="", icon="AXIS_FRONT")
|
||||
row.operator("bim.disable_editing_extrusion_profile", text="", icon="CANCEL")
|
||||
else:
|
||||
row.operator("bim.edit_arbitrary_profile", text="Save Profile")
|
||||
row.operator("bim.align_view_to_profile", text="", icon="AXIS_FRONT")
|
||||
row.operator("bim.disable_editing_arbitrary_profile", text="", icon="CANCEL")
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element:
|
||||
if element.is_a("IfcProfileDef"):
|
||||
row.operator("bim.edit_arbitrary_profile", text="Save Profile")
|
||||
row.operator("bim.align_view_to_profile", text="", icon="AXIS_FRONT")
|
||||
row.operator("bim.disable_editing_arbitrary_profile", text="", icon="CANCEL")
|
||||
elif element.is_a("IfcRelSpaceBoundary"):
|
||||
row.operator("bim.edit_boundary_geometry", text="Save Profile")
|
||||
row.operator("bim.disable_editing_boundary_geometry", text="", icon="CANCEL")
|
||||
else:
|
||||
row.operator("bim.edit_extrusion_profile", text="Save Profile")
|
||||
row.operator("bim.align_view_to_profile", text="", icon="AXIS_FRONT")
|
||||
row.operator("bim.disable_editing_extrusion_profile", text="", icon="CANCEL")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
@@ -156,7 +161,7 @@ class CadTool(WorkSpaceTool):
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_R")
|
||||
row.operator("bim.hotkey", text="Set gable roof angle").hotkey = "S_R"
|
||||
row.operator("bim.cad_hotkey", text="Set Gable Roof Angle").hotkey = "S_R"
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
@@ -229,6 +234,7 @@ 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 self.is_profile():
|
||||
@@ -254,28 +260,30 @@ class CadHotkey(bpy.types.Operator):
|
||||
bpy.ops.bim.cad_offset(distance=self.props.distance)
|
||||
|
||||
def hotkey_S_Q(self):
|
||||
if tool.Ifc.get_entity(bpy.context.active_object):
|
||||
if bpy.context.active_object.data.BIMMeshProperties.subshape_type == "PROFILE":
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
if bpy.context.active_object.data.BIMMeshProperties.subshape_type == "PROFILE":
|
||||
if element.is_a("IfcProfileDef"):
|
||||
bpy.ops.bim.edit_arbitrary_profile()
|
||||
elif element.is_a("IfcRelSpaceBoundary"):
|
||||
bpy.ops.bim.edit_boundary_geometry()
|
||||
else:
|
||||
bpy.ops.bim.edit_extrusion_profile()
|
||||
elif bpy.context.active_object.data.BIMMeshProperties.subshape_type == "AXIS":
|
||||
bpy.ops.bim.edit_extrusion_axis()
|
||||
elif bpy.context.active_object.data.BIMMeshProperties.subshape_type == "AXIS":
|
||||
bpy.ops.bim.edit_extrusion_axis()
|
||||
|
||||
elif (
|
||||
(RailingData.is_loaded or not RailingData.load())
|
||||
and RailingData.data["parameters"]
|
||||
and bpy.context.active_object.BIMRailingProperties.is_editing_path
|
||||
):
|
||||
bpy.ops.bim.finish_editing_railing_path()
|
||||
elif (
|
||||
(RailingData.is_loaded or not RailingData.load())
|
||||
and RailingData.data["parameters"]
|
||||
and bpy.context.active_object.BIMRailingProperties.is_editing_path
|
||||
):
|
||||
bpy.ops.bim.finish_editing_railing_path()
|
||||
|
||||
elif (
|
||||
(RoofData.is_loaded or not RoofData.load())
|
||||
and RoofData.data["parameters"]
|
||||
and bpy.context.active_object.BIMRoofProperties.is_editing_path
|
||||
):
|
||||
bpy.ops.bim.finish_editing_roof_path()
|
||||
|
||||
else:
|
||||
bpy.ops.bim.edit_arbitrary_profile()
|
||||
elif (
|
||||
(RoofData.is_loaded or not RoofData.load())
|
||||
and RoofData.data["parameters"]
|
||||
and bpy.context.active_object.BIMRoofProperties.is_editing_path
|
||||
):
|
||||
bpy.ops.bim.finish_editing_roof_path()
|
||||
|
||||
def hotkey_S_R(self):
|
||||
if self.is_profile():
|
||||
@@ -285,7 +293,9 @@ class CadHotkey(bpy.types.Operator):
|
||||
and RoofData.data["parameters"]
|
||||
and bpy.context.active_object.BIMRoofProperties.is_editing_path
|
||||
):
|
||||
bpy.ops.bim.set_gable_roof_edge_angle(angle=self.props.gable_roof_edge_angle)
|
||||
bpy.ops.bim.set_gable_roof_edge_angle(
|
||||
angle=self.props.gable_roof_edge_angle, separate_verts=self.props.gable_roof_separate_verts
|
||||
)
|
||||
|
||||
def hotkey_S_T(self):
|
||||
bpy.ops.bim.cad_mitre()
|
||||
|
||||
@@ -76,6 +76,7 @@ classes = (
|
||||
operator.LoadCostItemTaskQuantities,
|
||||
operator.LoadCostItemResourceQuantities,
|
||||
operator.ChangeParentCostItem,
|
||||
operator.CopyCostItem,
|
||||
prop.CostItem,
|
||||
prop.CostItemQuantity,
|
||||
prop.CostItemType,
|
||||
|
||||
@@ -97,10 +97,9 @@ class AddSummaryCostItem(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_label = "Add Cost Item"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Add a summary cost item"
|
||||
cost_schedule: bpy.props.IntProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
core.add_summary_cost_item(tool.Ifc, tool.Cost, cost_schedule=tool.Ifc.get().by_id(self.cost_schedule))
|
||||
core.add_summary_cost_item(tool.Ifc, tool.Cost, cost_schedule=tool.Cost.get_active_cost_schedule())
|
||||
|
||||
|
||||
class AddCostItem(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -114,6 +113,16 @@ class AddCostItem(bpy.types.Operator, tool.Ifc.Operator):
|
||||
core.add_cost_item(tool.Ifc, tool.Cost, cost_item=tool.Ifc.get().by_id(self.cost_item))
|
||||
|
||||
|
||||
class CopyCostItem(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.copy_cost_item"
|
||||
bl_label = "Copy Cost Item"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Copy a cost item"
|
||||
|
||||
def _execute(self, context):
|
||||
core.copy_cost_item(tool.Ifc, tool.Cost)
|
||||
|
||||
|
||||
class ExpandCostItem(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.expand_cost_item"
|
||||
bl_label = "Expand Cost Item"
|
||||
@@ -149,7 +158,7 @@ class ContractCostItem(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
class RemoveCostItem(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.remove_cost_item"
|
||||
bl_label = "Remove Cost item"
|
||||
bl_label = "Remove Cost Item"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
cost_item: bpy.props.IntProperty()
|
||||
|
||||
@@ -214,8 +223,9 @@ class UnassignCostItemType(bpy.types.Operator, tool.Ifc.Operator):
|
||||
core.unassign_cost_item_type(
|
||||
tool.Ifc,
|
||||
tool.Cost,
|
||||
self.cost_item,
|
||||
products=[tool.Ifc.get().by_id(self.related_object)] if self.related_object else [],
|
||||
tool.Spatial,
|
||||
cost_item=tool.Ifc.get().by_id(self.cost_item),
|
||||
product_types=[tool.Ifc.get().by_id(self.related_object)] if self.related_object else [],
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -491,7 +501,7 @@ class AddCostColumn(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
name: bpy.props.StringProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
def execute(self, context):
|
||||
core.add_cost_column(tool.Cost, self.name)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -619,10 +629,12 @@ class ExportCostSchedules(bpy.types.Operator):
|
||||
bl_label = "Export Cost Schedule"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Export a cost schedule to a CSV, XSLX OR ODS file"
|
||||
cost_schedule: bpy.props.IntProperty()
|
||||
format: bpy.props.EnumProperty("Format", items=(("CSV", "CSV", ""), ("XLSX", "XLSX", ""), ("ODS", "ODS", "")))
|
||||
|
||||
def execute(self, context):
|
||||
core.export_cost_schedules(tool.Cost, format=self.format)
|
||||
cost_schedule = tool.Ifc.get().by_id(self.cost_schedule) if self.cost_schedule else None
|
||||
core.export_cost_schedules(tool.Cost, format=self.format, cost_schedule=cost_schedule)
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
@@ -667,7 +679,9 @@ class LoadProductCostItems(bpy.types.Operator):
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
core.load_product_cost_items(tool.Cost, product=tool.Ifc.get().by_id(context.active_object.BIMObjectProperties.ifc_definition_id))
|
||||
core.load_product_cost_items(
|
||||
tool.Cost, product=tool.Ifc.get().by_id(context.active_object.BIMObjectProperties.ifc_definition_id)
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ class CostItemType(PropertyGroup):
|
||||
|
||||
def update_cost_item_parent(self, context):
|
||||
cost_item = tool.Cost.get_highlighted_cost_item()
|
||||
tool.Cost.toggle_cost_item_parent(cost_item=cost_item)
|
||||
tool.Cost.toggle_cost_item_parent_change(cost_item=cost_item)
|
||||
|
||||
|
||||
def update_active_cost_item_elements(self, context):
|
||||
@@ -213,3 +213,4 @@ class BIMCostProperties(PropertyGroup):
|
||||
name="Show Nested Tasks", default=False, update=update_active_cost_item_resources
|
||||
)
|
||||
change_cost_item_parent: BoolProperty(name="Change Cost Item Parent", default=False, update=update_cost_item_parent)
|
||||
show_cost_item_operators: BoolProperty(name="Show Cost Item Operators", default=False)
|
||||
|
||||
@@ -43,43 +43,54 @@ class BIM_PT_cost_schedules(Panel):
|
||||
|
||||
self.props = context.scene.BIMCostProperties
|
||||
row = self.layout.row()
|
||||
if CostSchedulesData.data["total_cost_schedules"]:
|
||||
row.label(text=f"{CostSchedulesData.data['total_cost_schedules']} Cost Schedules Found", icon="TEXT")
|
||||
row.operator("bim.export_cost_schedules", text="Export as spreadsheet", icon="EXPORT")
|
||||
else:
|
||||
row.label(text="No Cost Schedules Found found.", icon="COMMUNITY")
|
||||
|
||||
row = self.layout.row()
|
||||
row.prop(self.props, "cost_schedule_predefined_types")
|
||||
row.operator("bim.add_cost_schedule", icon="ADD", text="Add new")
|
||||
if not self.props.active_cost_schedule_id:
|
||||
if CostSchedulesData.data["total_cost_schedules"]:
|
||||
row.label(text=f"{CostSchedulesData.data['total_cost_schedules']} Cost Schedules Found", icon="TEXT")
|
||||
row.operator("bim.export_cost_schedules", text="Export as spreadsheet", icon="EXPORT")
|
||||
else:
|
||||
row.label(text="No Cost Schedules Found found.", icon="COMMUNITY")
|
||||
row = self.layout.row()
|
||||
row.prop(self.props, "cost_schedule_predefined_types")
|
||||
row.operator("bim.add_cost_schedule", icon="ADD", text="Add")
|
||||
|
||||
for schedule in CostSchedulesData.data["schedules"]:
|
||||
self.draw_cost_schedule_ui(schedule)
|
||||
|
||||
def draw_cost_schedule_ui(self, cost_schedule):
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=cost_schedule["name"], icon="LINENUMBERS_ON")
|
||||
|
||||
if self.props.active_cost_schedule_id and self.props.active_cost_schedule_id == cost_schedule["id"]:
|
||||
op = row.operator("bim.select_cost_schedule_products", icon="RESTRICT_SELECT_OFF", text="Assigned")
|
||||
row.label(text="Currently editing: {}".format(cost_schedule["name"]), icon="LINENUMBERS_ON")
|
||||
grid = self.layout.grid_flow(columns=2, even_columns=True)
|
||||
col = grid.column()
|
||||
row1 = col.row(align=True)
|
||||
row1.alignment = "LEFT"
|
||||
row1.label(text="Schedule tools")
|
||||
row1 = col.row(align=True)
|
||||
row1.alignment = "RIGHT"
|
||||
row1.operator("bim.export_cost_schedules", text="Export", icon="EXPORT").cost_schedule = cost_schedule["id"]
|
||||
row2 = col.row(align=True)
|
||||
row2.alignment = "RIGHT"
|
||||
op = row2.operator("bim.select_cost_schedule_products", icon="RESTRICT_SELECT_OFF", text="Assigned")
|
||||
op.cost_schedule = cost_schedule["id"]
|
||||
row.operator("bim.select_unassigned_products", icon="RESTRICT_SELECT_OFF", text="Unassigned")
|
||||
row2.operator("bim.select_unassigned_products", icon="RESTRICT_SELECT_OFF", text="Unassigned")
|
||||
|
||||
row.prop(self.props, "should_show_column_ui", text="", icon="SHORTDISPLAY")
|
||||
col = grid.column()
|
||||
row1 = col.row(align=True)
|
||||
row1.alignment = "LEFT"
|
||||
row1.label(text="Settings")
|
||||
row1 = col.row(align=True)
|
||||
row1.alignment = "RIGHT"
|
||||
row1.prop(self.props, "should_show_column_ui", text="Schedule Columns", icon="SHORTDISPLAY")
|
||||
if self.props.is_editing == "COST_SCHEDULE_ATTRIBUTES":
|
||||
row.operator("bim.edit_cost_schedule", text="", icon="CHECKMARK")
|
||||
elif self.props.is_editing == "COST_ITEMS":
|
||||
row.operator("bim.add_summary_cost_item", text="", icon="ADD").cost_schedule = cost_schedule["id"]
|
||||
row.operator("bim.disable_editing_cost_schedule", text="", icon="CANCEL")
|
||||
elif self.props.active_cost_schedule_id:
|
||||
row.operator("bim.remove_cost_schedule", text="", icon="X").cost_schedule = cost_schedule["id"]
|
||||
row.operator("bim.disable_editing_cost_schedule", text="Disable Editing", icon="CANCEL")
|
||||
else:
|
||||
row.label(text=cost_schedule["name"], icon="LINENUMBERS_ON")
|
||||
row.operator("bim.enable_editing_cost_items", text="", icon="OUTLINER").cost_schedule = cost_schedule["id"]
|
||||
row.operator(
|
||||
"bim.enable_editing_cost_schedule_attributes", text="", icon="GREASEPENCIL"
|
||||
).cost_schedule = cost_schedule["id"]
|
||||
row.operator("bim.remove_cost_schedule", text="", icon="X").cost_schedule = cost_schedule["id"]
|
||||
|
||||
if self.props.active_cost_schedule_id == cost_schedule["id"]:
|
||||
if self.props.is_editing == "COST_SCHEDULE_ATTRIBUTES":
|
||||
self.draw_editable_cost_schedule_ui()
|
||||
@@ -101,30 +112,39 @@ class BIM_PT_cost_schedules(Panel):
|
||||
row = self.layout.row(align=True)
|
||||
row.alignment = "RIGHT"
|
||||
ifc_definition_id = None
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Cost Item Tools")
|
||||
row = self.layout.row(align=True)
|
||||
row.alignment = "RIGHT"
|
||||
row.operator("bim.add_summary_cost_item", text="Add Summary Cost", icon="ADD")
|
||||
row.operator("bim.expand_all_tasks", text="Expand All")
|
||||
row.operator("bim.contract_all_tasks", text="Contract All")
|
||||
row = self.layout.row(align=True)
|
||||
row.alignment = "RIGHT"
|
||||
if self.props.cost_items and self.props.active_cost_item_index < len(self.props.cost_items):
|
||||
ifc_definition_id = self.props.cost_items[self.props.active_cost_item_index].ifc_definition_id
|
||||
if ifc_definition_id:
|
||||
row.prop(self.props, "change_cost_item_parent", text="", icon="LINKED")
|
||||
row.prop(self.props, "enable_reorder", text="", icon="SORTALPHA")
|
||||
if not CostSchedulesData.data["is_editing_rates"]:
|
||||
op = row.operator("bim.enable_editing_cost_item_quantities", text="", icon="PROPERTIES")
|
||||
row.prop(self.props, "show_cost_item_operators", text="Edit", icon="DOWNARROW_HLT")
|
||||
row.operator("bim.add_cost_item", text="Add", icon="ADD").cost_item = ifc_definition_id
|
||||
row.operator("bim.copy_cost_item", text="Copy", icon="ADD")
|
||||
row.operator("bim.remove_cost_item", text="Delete", icon="X").cost_item = ifc_definition_id
|
||||
if self.props.show_cost_item_operators:
|
||||
row = self.layout.row(align=True)
|
||||
row.alignment = "RIGHT"
|
||||
row.prop(self.props, "change_cost_item_parent", text="", icon="LINKED")
|
||||
row.prop(self.props, "enable_reorder", text="", icon="SORTALPHA")
|
||||
if not CostSchedulesData.data["is_editing_rates"]:
|
||||
op = row.operator("bim.enable_editing_cost_item_quantities", text="", icon="PROPERTIES")
|
||||
op.cost_item = ifc_definition_id
|
||||
op = row.operator("bim.enable_editing_cost_item_values", text="", icon="DISC")
|
||||
op.cost_item = ifc_definition_id
|
||||
|
||||
op = row.operator("bim.enable_editing_cost_item_values", text="", icon="DISC")
|
||||
op.cost_item = ifc_definition_id
|
||||
|
||||
row.operator("bim.add_cost_item", text="", icon="ADD").cost_item = ifc_definition_id
|
||||
|
||||
if self.props.active_cost_item_id == ifc_definition_id:
|
||||
if self.props.cost_item_editing_type == "ATTRIBUTES":
|
||||
row.operator("bim.edit_cost_item", text="", icon="CHECKMARK")
|
||||
row.operator("bim.disable_editing_cost_item", text="", icon="CANCEL")
|
||||
else:
|
||||
op = row.operator("bim.enable_editing_cost_item_attributes", text="", icon="GREASEPENCIL")
|
||||
op.cost_item = ifc_definition_id
|
||||
|
||||
row.operator("bim.remove_cost_item", text="", icon="X").cost_item = ifc_definition_id
|
||||
|
||||
if self.props.active_cost_item_id == ifc_definition_id:
|
||||
if self.props.cost_item_editing_type == "ATTRIBUTES":
|
||||
row.operator("bim.edit_cost_item", text="", icon="CHECKMARK")
|
||||
row.operator("bim.disable_editing_cost_item", text="", icon="CANCEL")
|
||||
else:
|
||||
op = row.operator("bim.enable_editing_cost_item_attributes", text="", icon="GREASEPENCIL")
|
||||
op.cost_item = ifc_definition_id
|
||||
self.layout.template_list(
|
||||
"BIM_UL_cost_items",
|
||||
"",
|
||||
@@ -594,7 +614,7 @@ class BIM_UL_cost_items_trait:
|
||||
layout.label(text=text)
|
||||
|
||||
def draw_uom_column(self, layout, cost_item):
|
||||
layout.label(text=cost_item["UnitBasisUnitSymbol"] or "?" if cost_item["UnitBasisValueComponent"] else "-")
|
||||
layout.label(text=cost_item["UnitBasisUnitSymbol"] or "-" if cost_item["UnitBasisValueComponent"] else "-")
|
||||
|
||||
def draw_order_operator(self, row, ifc_definition_id, cost_item):
|
||||
if cost_item["NestingIndex"] is not None:
|
||||
@@ -608,7 +628,14 @@ class BIM_UL_cost_items_trait:
|
||||
op.new_index = cost_item["NestingIndex"] - 1
|
||||
|
||||
def draw_total_quantity_column(self, layout, cost_item):
|
||||
layout.label(text="{0:.2f}".format(cost_item["TotalCostQuantity"]) + f" {cost_item['UnitSymbol'] or '?'}")
|
||||
layout.label(text="{0:.2f}".format(cost_item["TotalCostQuantity"]) + f" {cost_item['UnitSymbol'] or '-'}")
|
||||
# if cost_item["DerivedTotalCostQuantity"] not in [None, 0]:
|
||||
# layout.label(text="{0:.2f}".format(cost_item["DerivedTotalCostQuantity"]) + f" {cost_item['DerivedUnitSymbol'] or '-'}")
|
||||
# else:
|
||||
# if cost_item["TotalCostQuantity"] == 0:
|
||||
# layout.label(text="-")
|
||||
# else:
|
||||
# layout.label(text="{0:.2f}".format(cost_item["TotalCostQuantity"]) + f" {cost_item['UnitSymbol'] or '-'}")
|
||||
|
||||
|
||||
class BIM_UL_cost_items(BIM_UL_cost_items_trait, UIList):
|
||||
@@ -691,9 +718,7 @@ class BIM_PT_Costing_Tools(Panel):
|
||||
def draw(self, context):
|
||||
self.props = context.scene.BIMCostProperties
|
||||
row = self.layout.row()
|
||||
row.operator(
|
||||
"bim.load_product_cost_items", icon="FILE_REFRESH"
|
||||
)
|
||||
row.operator("bim.load_product_cost_items", icon="FILE_REFRESH")
|
||||
row = self.layout.row()
|
||||
row.template_list(
|
||||
"BIM_UL_product_cost_items",
|
||||
|
||||
@@ -31,6 +31,7 @@ from blenderbim.bim.handler import purge_module_data
|
||||
class AddCsvAttribute(bpy.types.Operator):
|
||||
bl_idname = "bim.add_csv_attribute"
|
||||
bl_label = "Add CSV Attribute"
|
||||
bl_description = "Add a new IFC Attribute to the CSV export"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
@@ -52,6 +53,7 @@ class RemoveCsvAttribute(bpy.types.Operator):
|
||||
class RemoveAllCsvAttributes(bpy.types.Operator):
|
||||
bl_idname = "bim.remove_all_csv_attributes"
|
||||
bl_label = "Remove all CSV Attributes"
|
||||
bl_description = "Remove all IFC Attributes from the CSV export"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
@@ -61,20 +63,25 @@ class RemoveAllCsvAttributes(bpy.types.Operator):
|
||||
|
||||
class ImportCsvAttributes(bpy.types.Operator):
|
||||
bl_idname = "bim.import_csv_attributes"
|
||||
bl_label = "Import CSV Attributes"
|
||||
bl_label = "Import CSV Template"
|
||||
bl_description = "Import a json template for CSV export"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
|
||||
def execute(self, context):
|
||||
csv_attributes = context.scene.CsvProperties.csv_attributes
|
||||
csv_attributes.clear()
|
||||
csv_props = context.scene.CsvProperties
|
||||
csv_json = json.load(open(self.filepath))
|
||||
i = 0
|
||||
for attribute in csv_json:
|
||||
csv_attributes.add()
|
||||
csv_attributes[i].name = attribute
|
||||
i += 1
|
||||
|
||||
expression = csv_json.get("expression", "")
|
||||
if expression:
|
||||
csv_props.ifc_selector = expression
|
||||
|
||||
attributes = csv_json.get("attributes", [])
|
||||
if attributes:
|
||||
csv_props.csv_attributes.clear()
|
||||
for attribute in attributes:
|
||||
csv_props.csv_attributes.add().name = attribute
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -85,19 +92,30 @@ class ImportCsvAttributes(bpy.types.Operator):
|
||||
|
||||
class ExportCsvAttributes(bpy.types.Operator):
|
||||
bl_idname = "bim.export_csv_attributes"
|
||||
bl_label = "Export CSV Attributes"
|
||||
bl_label = "Export CSV Template"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Save a json template for CSV export"
|
||||
filename_ext = ".json"
|
||||
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
|
||||
def execute(self, context):
|
||||
csv_attributes = []
|
||||
with open(self.filepath, "w") as outfile:
|
||||
for attribute in context.scene.CsvProperties.csv_attributes:
|
||||
csv_attributes.append(attribute.name)
|
||||
csv_props = context.scene.CsvProperties
|
||||
|
||||
json.dump(csv_attributes, outfile)
|
||||
csv_template = {}
|
||||
expression = csv_props.ifc_selector
|
||||
if expression:
|
||||
csv_template["expression"] = expression
|
||||
|
||||
csv_attributes = []
|
||||
for attribute in csv_props.csv_attributes:
|
||||
attribute_name = attribute.name
|
||||
csv_attributes.append(attribute_name)
|
||||
if csv_attributes:
|
||||
csv_template["attributes"] = csv_attributes
|
||||
|
||||
with open(self.filepath, "w") as outfile:
|
||||
json.dump(csv_template, outfile)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -49,27 +49,18 @@ class BIM_PT_ifccsv(Panel):
|
||||
row.operator("bim.eyedrop_ifccsv", icon="EYEDROPPER", text="")
|
||||
layout.separator()
|
||||
|
||||
row = layout.row()
|
||||
split = row.split(factor=0.7)
|
||||
c = split.column()
|
||||
c.operator("bim.add_csv_attribute")
|
||||
c = split.column()
|
||||
c.operator("bim.import_csv_attributes", icon="IMPORT", text="Load Template")
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.add_csv_attribute", icon="ADD")
|
||||
row.operator("bim.remove_all_csv_attributes", icon="CANCEL")
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.import_csv_attributes", icon="IMPORT")
|
||||
row.operator("bim.export_csv_attributes", icon="EXPORT")
|
||||
|
||||
for index, attribute in enumerate(props.csv_attributes):
|
||||
row = layout.row(align=True)
|
||||
row.prop(attribute, "name", text="")
|
||||
row.operator("bim.remove_csv_attribute", icon="X", text="").index = index
|
||||
|
||||
if props.csv_attributes:
|
||||
row = layout.row()
|
||||
row.label()
|
||||
row.operator("bim.remove_all_csv_attributes", icon="CANCEL", text="")
|
||||
row = layout.row()
|
||||
row.operator("bim.export_csv_attributes", icon="EXPORT", text="Create Template")
|
||||
|
||||
layout.separator()
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(props, "csv_delimiter")
|
||||
|
||||
@@ -78,7 +69,7 @@ class BIM_PT_ifccsv(Panel):
|
||||
row.prop(props, "csv_custom_delimiter")
|
||||
|
||||
row = layout.row()
|
||||
split = row.split(factor=0.7)
|
||||
split = row.split(factor=0.5)
|
||||
c = split.column()
|
||||
c.operator("bim.export_ifccsv", icon="EXPORT")
|
||||
c = split.column()
|
||||
|
||||
@@ -17,11 +17,12 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
from . import ui, prop, operator, handler, gizmos
|
||||
from . import ui, prop, operator, handler, gizmos, workspace
|
||||
|
||||
classes = (
|
||||
operator.ActivateDrawing,
|
||||
operator.ActivateDrawingStyle,
|
||||
operator.ActivateView,
|
||||
operator.ActivateModel,
|
||||
operator.AddAnnotation,
|
||||
operator.AddDrawing,
|
||||
operator.AddDrawingStyle,
|
||||
@@ -30,6 +31,7 @@ classes = (
|
||||
operator.AddSchedule,
|
||||
operator.AddScheduleToSheet,
|
||||
operator.AddSheet,
|
||||
operator.AddTextLiteral,
|
||||
operator.BuildSchedule,
|
||||
operator.CleanWireframes,
|
||||
operator.ContractSheet,
|
||||
@@ -40,32 +42,34 @@ classes = (
|
||||
operator.DisableEditingSchedules,
|
||||
operator.DisableEditingSheets,
|
||||
operator.DisableEditingText,
|
||||
operator.AddTextLiteral,
|
||||
operator.RemoveTextLiteral,
|
||||
operator.DuplicateDrawing,
|
||||
operator.EditAssignedProduct,
|
||||
operator.EditSheet,
|
||||
operator.EditText,
|
||||
operator.EditTextPopup,
|
||||
operator.EditVectorStyle,
|
||||
operator.EnableEditingAssignedProduct,
|
||||
operator.EnableEditingText,
|
||||
operator.ExpandSheet,
|
||||
operator.LoadDrawings,
|
||||
operator.LoadSchedules,
|
||||
operator.LoadSheets,
|
||||
operator.OpenDrawing,
|
||||
operator.OpenSchedule,
|
||||
operator.OpenSheet,
|
||||
operator.OpenView,
|
||||
operator.RemoveDrawing,
|
||||
operator.RemoveDrawingFromSheet,
|
||||
operator.RemoveDrawingStyle,
|
||||
operator.RemoveDrawingStyleAttribute,
|
||||
operator.RemoveSchedule,
|
||||
operator.ReloadDrawingStyles,
|
||||
operator.RemoveSheet,
|
||||
operator.RemoveTextLiteral,
|
||||
operator.SelectAllDrawings,
|
||||
operator.ResizeText,
|
||||
operator.SaveDrawingStyle,
|
||||
operator.SelectDocIfcFile,
|
||||
operator.SaveDrawingStylesData,
|
||||
operator.SelectAssignedProduct,
|
||||
operator.SelectDocIfcFile,
|
||||
prop.Variable,
|
||||
prop.Drawing,
|
||||
prop.Schedule,
|
||||
@@ -76,6 +80,7 @@ classes = (
|
||||
prop.Literal,
|
||||
prop.BIMTextProperties,
|
||||
prop.BIMAssignedProductProperties,
|
||||
prop.BIMAnnotationProperties,
|
||||
ui.BIM_PT_camera,
|
||||
ui.BIM_PT_drawing_underlay,
|
||||
ui.BIM_PT_annotation_utilities,
|
||||
@@ -91,11 +96,15 @@ classes = (
|
||||
gizmos.DimensionLabelGizmo,
|
||||
gizmos.ExtrusionGuidesGizmo,
|
||||
gizmos.ExtrusionWidget,
|
||||
workspace.Hotkey,
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
if not bpy.app.background:
|
||||
bpy.utils.register_tool(workspace.AnnotationTool, after={"bim.bim_tool"}, separator=True, group=True)
|
||||
bpy.types.Scene.DocProperties = bpy.props.PointerProperty(type=prop.DocProperties)
|
||||
bpy.types.Scene.BIMAnnotationProperties = bpy.props.PointerProperty(type=prop.BIMAnnotationProperties)
|
||||
bpy.types.Camera.BIMCameraProperties = bpy.props.PointerProperty(type=prop.BIMCameraProperties)
|
||||
bpy.types.Object.BIMAssignedProductProperties = bpy.props.PointerProperty(type=prop.BIMAssignedProductProperties)
|
||||
bpy.types.Object.BIMTextProperties = bpy.props.PointerProperty(type=prop.BIMTextProperties)
|
||||
@@ -105,7 +114,10 @@ def register():
|
||||
|
||||
|
||||
def unregister():
|
||||
if not bpy.app.background:
|
||||
bpy.utils.unregister_tool(workspace.AnnotationTool)
|
||||
del bpy.types.Scene.DocProperties
|
||||
del bpy.types.Scene.BIMAnnotationProperties
|
||||
del bpy.types.Camera.BIMCameraProperties
|
||||
del bpy.types.Object.BIMAssignedProductProperties
|
||||
del bpy.types.Object.BIMTextProperties
|
||||
|
||||
@@ -77,66 +77,35 @@ class Annotator:
|
||||
co1, co2, _, _ = Annotator.get_placeholder_coords()
|
||||
co1 = obj.matrix_world.inverted() @ co1
|
||||
co2 = obj.matrix_world.inverted() @ co2
|
||||
|
||||
if isinstance(obj.data, bpy.types.Mesh):
|
||||
obj.data.vertices.add(2)
|
||||
obj.data.vertices[-2].co = co1
|
||||
obj.data.vertices[-1].co = co2
|
||||
obj.data.edges.add(1)
|
||||
obj.data.edges[-1].vertices = (obj.data.vertices[-2].index, obj.data.vertices[-1].index)
|
||||
|
||||
if isinstance(obj.data, bpy.types.Curve):
|
||||
polyline = obj.data.splines.new("POLY")
|
||||
polyline.points.add(1)
|
||||
polyline.points[-2].co = list(co1) + [1]
|
||||
polyline.points[-1].co = list(co2) + [1]
|
||||
|
||||
return obj
|
||||
|
||||
@staticmethod
|
||||
def add_plane_to_annotation(obj):
|
||||
co1, co2, co3, co4 = Annotator.get_placeholder_coords()
|
||||
co1 = obj.matrix_world.inverted() @ co1 # bot left
|
||||
co2 = obj.matrix_world.inverted() @ co2 # top left
|
||||
co3 = obj.matrix_world.inverted() @ co3 # bot right
|
||||
co4 = obj.matrix_world.inverted() @ co4 # top right
|
||||
# default order = bot left, top left, bot right, top right
|
||||
# therefore we redefine the order
|
||||
face_verts = [0, 2, 3, 1]
|
||||
|
||||
obj.data.vertices.add(4)
|
||||
v1 = obj.data.vertices[-4]
|
||||
v2 = obj.data.vertices[-3]
|
||||
v3 = obj.data.vertices[-2]
|
||||
v4 = obj.data.vertices[-1]
|
||||
v1.co = co4
|
||||
v2.co = co2
|
||||
v3.co = co1
|
||||
v4.co = co3
|
||||
verts_world_space = Annotator.get_placeholder_coords()
|
||||
verts_local = [obj.matrix_world.inverted() @ v for v in verts_world_space]
|
||||
bm = tool.Blender.get_bmesh_for_mesh(obj.data, clean=True)
|
||||
new_verts = [bm.verts.new(v) for v in verts_local]
|
||||
|
||||
obj.data.edges.add(4)
|
||||
e1 = obj.data.edges[-4]
|
||||
e2 = obj.data.edges[-3]
|
||||
e3 = obj.data.edges[-2]
|
||||
e4 = obj.data.edges[-1]
|
||||
e1.vertices = (v1.index, v2.index)
|
||||
e2.vertices = (v2.index, v3.index)
|
||||
e3.vertices = (v3.index, v4.index)
|
||||
e4.vertices = (v4.index, v1.index)
|
||||
|
||||
obj.data.loops.add(4)
|
||||
l1 = obj.data.loops[-4]
|
||||
l2 = obj.data.loops[-3]
|
||||
l3 = obj.data.loops[-2]
|
||||
l4 = obj.data.loops[-1]
|
||||
l1.vertex_index = v1.index
|
||||
l1.edge_index = e1.index
|
||||
l2.vertex_index = v2.index
|
||||
l2.edge_index = e2.index
|
||||
l3.vertex_index = v3.index
|
||||
l3.edge_index = e3.index
|
||||
l4.vertex_index = v4.index
|
||||
l4.edge_index = e4.index
|
||||
|
||||
obj.data.polygons.add(1)
|
||||
p1 = obj.data.polygons[-1]
|
||||
p1.vertices = (v1.index, v2.index, v3.index, v4.index)
|
||||
p1.loop_start = l1.index
|
||||
p1.loop_total = 4
|
||||
bm.faces.new([new_verts[i] for i in face_verts])
|
||||
tool.Blender.apply_bmesh(obj.data, bm, obj)
|
||||
return obj
|
||||
|
||||
@staticmethod
|
||||
@@ -144,15 +113,15 @@ class Annotator:
|
||||
camera = tool.Ifc.get_object(drawing)
|
||||
co1, _, _, _ = Annotator.get_placeholder_coords(camera)
|
||||
matrix_world = camera.matrix_world.copy()
|
||||
matrix_world[0][3] = co1.x
|
||||
matrix_world[1][3] = co1.y
|
||||
matrix_world[2][3] = co1.z
|
||||
matrix_world.translation = co1
|
||||
collection = camera.users_collection[0]
|
||||
|
||||
if object_type == "TEXT":
|
||||
obj = bpy.data.objects.new(object_type, None)
|
||||
obj.matrix_world = matrix_world
|
||||
collection.objects.link(obj)
|
||||
return obj
|
||||
|
||||
elif object_type in ("TEXT_LEADER", "SECTION_LEVEL"):
|
||||
data = bpy.data.curves.new(object_type, type="CURVE")
|
||||
data.dimensions = "3D"
|
||||
@@ -161,17 +130,20 @@ class Annotator:
|
||||
obj.matrix_world = matrix_world
|
||||
collection.objects.link(obj)
|
||||
return obj
|
||||
|
||||
if object_type != "ANGLE":
|
||||
for obj in collection.objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element and element.ObjectType == object_type and obj.type == object_type.upper():
|
||||
return obj
|
||||
|
||||
if data_type == "mesh":
|
||||
data = bpy.data.meshes.new(object_type)
|
||||
elif data_type == "curve":
|
||||
data = bpy.data.curves.new(object_type, type="CURVE")
|
||||
data.dimensions = "3D"
|
||||
data.resolution_u = 2
|
||||
|
||||
obj = bpy.data.objects.new(object_type, data)
|
||||
obj.matrix_world = matrix_world
|
||||
collection.objects.link(obj)
|
||||
@@ -182,14 +154,13 @@ class Annotator:
|
||||
if not camera:
|
||||
camera = bpy.context.scene.camera
|
||||
z_offset = camera.matrix_world.to_quaternion() @ Vector((0, 0, -1))
|
||||
if bpy.context.scene.render.resolution_x > bpy.context.scene.render.resolution_y:
|
||||
y = (
|
||||
camera.data.ortho_scale
|
||||
* (bpy.context.scene.render.resolution_y / bpy.context.scene.render.resolution_x)
|
||||
/ 4
|
||||
)
|
||||
else:
|
||||
y = camera.data.ortho_scale / 4
|
||||
y = camera.data.ortho_scale / 4
|
||||
|
||||
res_x = bpy.context.scene.render.resolution_x
|
||||
res_y = bpy.context.scene.render.resolution_y
|
||||
if res_x > res_y:
|
||||
y *= res_y / res_x
|
||||
|
||||
y_offset = camera.matrix_world.to_quaternion() @ Vector((0, y, 0))
|
||||
x_offset = camera.matrix_world.to_quaternion() @ Vector((y / 2, 0, 0))
|
||||
return (
|
||||
|
||||
@@ -16,10 +16,12 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import bpy
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.representation
|
||||
import blenderbim.tool as tool
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def refresh():
|
||||
@@ -27,7 +29,10 @@ def refresh():
|
||||
SheetsData.is_loaded = False
|
||||
SchedulesData.is_loaded = False
|
||||
DrawingsData.is_loaded = False
|
||||
AnnotationData.is_loaded = False
|
||||
DecoratorData.data = {}
|
||||
DecoratorData.cut_cache = {}
|
||||
DecoratorData.layerset_cache = {}
|
||||
|
||||
|
||||
class ProductAssignmentsData:
|
||||
@@ -56,12 +61,36 @@ class SheetsData:
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.data = {"total_sheets": cls.total_sheets()}
|
||||
cls.data = {
|
||||
"has_saved_ifc": cls.has_saved_ifc(),
|
||||
"total_sheets": cls.total_sheets(),
|
||||
"titleblocks": cls.titleblocks(),
|
||||
}
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def has_saved_ifc(cls):
|
||||
return os.path.isfile(tool.Ifc.get_path())
|
||||
|
||||
@classmethod
|
||||
def total_sheets(cls):
|
||||
return len([d for d in tool.Ifc.get().by_type("IfcDocumentInformation") if d.Scope == "DOCUMENTATION"])
|
||||
return len([d for d in tool.Ifc.get().by_type("IfcDocumentInformation") if d.Scope == "SHEET"])
|
||||
|
||||
@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]
|
||||
|
||||
if tool.Ifc.get():
|
||||
project = tool.Ifc.get().by_type("IfcProject")[0]
|
||||
titleblocks_dir = ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "TitleblocksDir")
|
||||
if not titleblocks_dir:
|
||||
titleblocks_dir = bpy.context.scene.DocProperties.titleblocks_dir
|
||||
titleblocks_dir = tool.Ifc.resolve_uri(titleblocks_dir)
|
||||
if os.path.exists(titleblocks_dir):
|
||||
files.extend([str(f.stem) for f in Path(titleblocks_dir).glob("*.svg")])
|
||||
|
||||
return [(f, f, "") for f in sorted(list(set(files)))]
|
||||
|
||||
|
||||
class DrawingsData:
|
||||
@@ -70,9 +99,18 @@ class DrawingsData:
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.data = {"total_drawings": cls.total_drawings(), "location_hint": cls.location_hint()}
|
||||
cls.data = {
|
||||
"has_saved_ifc": cls.has_saved_ifc(),
|
||||
"total_drawings": cls.total_drawings(),
|
||||
"location_hint": cls.location_hint(),
|
||||
"active_drawing_pset_data": cls.active_drawing_pset_data(),
|
||||
}
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def has_saved_ifc(cls):
|
||||
return os.path.isfile(tool.Ifc.get_path())
|
||||
|
||||
@classmethod
|
||||
def total_drawings(cls):
|
||||
return len([e for e in tool.Ifc.get().by_type("IfcAnnotation") if e.ObjectType == "DRAWING"])
|
||||
@@ -87,6 +125,15 @@ class DrawingsData:
|
||||
return results
|
||||
return [(h.upper(), h, "") for h in ["North", "South", "East", "West"]]
|
||||
|
||||
@classmethod
|
||||
def active_drawing_pset_data(cls):
|
||||
ifc_file = tool.Ifc.get()
|
||||
drawing_id = bpy.context.scene.DocProperties.active_drawing_id
|
||||
if drawing_id == 0:
|
||||
return {}
|
||||
drawing = ifc_file.by_id(bpy.context.scene.DocProperties.active_drawing_id)
|
||||
return ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing")
|
||||
|
||||
|
||||
class SchedulesData:
|
||||
data = {}
|
||||
@@ -94,9 +141,13 @@ class SchedulesData:
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.data = {"total_schedules": cls.total_schedules()}
|
||||
cls.data = {"has_saved_ifc": cls.has_saved_ifc(), "total_schedules": cls.total_schedules()}
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def has_saved_ifc(cls):
|
||||
return os.path.isfile(tool.Ifc.get_path())
|
||||
|
||||
@classmethod
|
||||
def total_schedules(cls):
|
||||
return len([d for d in tool.Ifc.get().by_type("IfcDocumentInformation") if d.Scope == "SCHEDULE"])
|
||||
@@ -114,6 +165,8 @@ FONT_SIZES = {
|
||||
class DecoratorData:
|
||||
# stores 1 type of data per object
|
||||
data = {}
|
||||
cut_cache = {}
|
||||
layerset_cache = {}
|
||||
|
||||
# used by Ifc Annotations with ObjectType = "BATTING"
|
||||
@classmethod
|
||||
@@ -171,7 +224,7 @@ class DecoratorData:
|
||||
cls.data[obj.name] = display_data
|
||||
return display_data
|
||||
|
||||
# used by Ifc Annotations with ObjectType = "TEXT"
|
||||
# used by Ifc Annotations with ObjectType = "TEXT" / "TEXT_LEADER"
|
||||
@classmethod
|
||||
def get_ifc_text_data(cls, obj):
|
||||
"""returns font size in mm for current ifc text object"""
|
||||
@@ -180,23 +233,26 @@ class DecoratorData:
|
||||
return result
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not element.is_a("IfcAnnotation") or element.ObjectType not in ["TEXT", "TEXT_LEADER"]:
|
||||
if not element or not tool.Drawing.is_annotation_object_type(element, ["TEXT", "TEXT_LEADER"]):
|
||||
return None
|
||||
|
||||
props = obj.BIMTextProperties
|
||||
# getting font size
|
||||
classes = ifcopenshell.util.element.get_pset(element, "EPset_Annotation", "Classes")
|
||||
pset_data = ifcopenshell.util.element.get_pset(element, "EPset_Annotation") or {}
|
||||
# use `regular` as default
|
||||
if classes:
|
||||
classes_split = classes.split()
|
||||
# prioritize smaller font sizes just like in svg
|
||||
font_size_type = next(
|
||||
(font_size_type for font_size_type in FONT_SIZES if font_size_type in classes_split), "regular"
|
||||
)
|
||||
else:
|
||||
font_size_type = "regular"
|
||||
|
||||
# get font size
|
||||
classes = pset_data.get("Classes", None) or "regular"
|
||||
classes_split = classes.split()
|
||||
# prioritize smaller font sizes just like in svg
|
||||
font_size_type = next(
|
||||
(font_size_type for font_size_type in FONT_SIZES if font_size_type in classes_split), "regular"
|
||||
)
|
||||
font_size = FONT_SIZES[font_size_type]
|
||||
|
||||
# get symbol
|
||||
symbol = pset_data.get("Symbol", None)
|
||||
|
||||
# other attributes
|
||||
props_literals = props.literals
|
||||
props_literals_n = len(props.literals)
|
||||
@@ -214,19 +270,20 @@ class DecoratorData:
|
||||
|
||||
literals_data.append(literal_data)
|
||||
|
||||
text_data = {"Literals": literals_data, "FontSize": font_size}
|
||||
text_data = {"Literals": literals_data, "FontSize": font_size, "Symbol": symbol}
|
||||
cls.data[obj.name] = text_data
|
||||
return text_data
|
||||
|
||||
# used by Ifc Annotations with ObjectType = "DIMENSION"
|
||||
# used by Ifc Annotations with ObjectType = "DIMENSION" / "DIAMETER"
|
||||
@classmethod
|
||||
def get_dimension_style(cls, obj):
|
||||
def get_dimension_data(cls, obj):
|
||||
result = cls.data.get(obj.name, None)
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not element.is_a("IfcAnnotation") or element.ObjectType != "DIMENSION":
|
||||
supported_object_types = ("DIMENSION", "DIAMETER")
|
||||
if not element or not element.is_a("IfcAnnotation") or element.ObjectType not in supported_object_types:
|
||||
return None
|
||||
|
||||
dimension_style = "arrow"
|
||||
@@ -234,5 +291,43 @@ class DecoratorData:
|
||||
if classes and "oblique" in classes.lower().split():
|
||||
dimension_style = "oblique"
|
||||
|
||||
cls.data[obj.name] = dimension_style
|
||||
return dimension_style
|
||||
pset_data = ifcopenshell.util.element.get_pset(element, "BBIM_Dimension") or {}
|
||||
show_description_only = pset_data.get("ShowDescriptionOnly", False)
|
||||
suppress_zero_inches = pset_data.get("SuppressZeroInches", False)
|
||||
text_prefix = pset_data.get("TextPrefix", "")
|
||||
text_suffix = pset_data.get("TextSuffix", "")
|
||||
|
||||
dimension_data = {
|
||||
"dimension_style": dimension_style,
|
||||
"show_description_only": show_description_only,
|
||||
"suppress_zero_inches": suppress_zero_inches,
|
||||
"text_prefix": text_prefix,
|
||||
"text_suffix": text_suffix,
|
||||
}
|
||||
cls.data[obj.name] = dimension_data
|
||||
return dimension_data
|
||||
|
||||
|
||||
class AnnotationData:
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.is_loaded = True
|
||||
cls.props = bpy.context.scene.BIMAnnotationProperties
|
||||
cls.data["relating_types"] = cls.get_relating_types()
|
||||
|
||||
@classmethod
|
||||
def get_relating_types(cls):
|
||||
object_type = cls.props.object_type
|
||||
relating_types = []
|
||||
for relating_type in tool.Ifc.get().by_type("IfcTypeProduct"):
|
||||
if tool.Drawing.is_annotation_object_type(relating_type, object_type):
|
||||
relating_types.append(relating_type)
|
||||
|
||||
enum_items = [(str(e.id()), e.Name or "Unnamed", e.Description or "") for e in relating_types]
|
||||
|
||||
# item to create anootations without relating types
|
||||
enum_items.insert(0, ("0", "-", ""))
|
||||
return enum_items
|
||||
|
||||
@@ -18,13 +18,12 @@
|
||||
|
||||
import bpy
|
||||
import blf
|
||||
import math
|
||||
import gpu, bgl
|
||||
import gpu
|
||||
from bpy import types
|
||||
from mathutils import Vector, Matrix
|
||||
from mathutils import Vector
|
||||
from mathutils import geometry
|
||||
from bpy_extras import view3d_utils
|
||||
from blenderbim.bim.module.drawing.shaders import DotsGizmoShader, ExtrusionGuidesShader, BaseLinesShader
|
||||
from blenderbim.bim.module.drawing.shaders import DotsGizmoShader, ExtrusionGuidesShader
|
||||
from ifcopenshell.util.unit import si_conversions
|
||||
|
||||
|
||||
@@ -252,11 +251,12 @@ X3DISC = (
|
||||
class CustomGizmo:
|
||||
# FIXME: highliting/selection doesnt work
|
||||
def draw_very_custom_shape(self, ctx, custom_shape, select_id=None):
|
||||
# similar to draw_custom_shape
|
||||
shape, batch, shader = custom_shape
|
||||
# create shader and batch
|
||||
shader_wrapper, batch = custom_shape
|
||||
shader = shader_wrapper.get_shader()
|
||||
|
||||
# setup params
|
||||
shader.bind()
|
||||
|
||||
if select_id is not None:
|
||||
gpu.select.load_id(select_id)
|
||||
else:
|
||||
@@ -265,15 +265,19 @@ class CustomGizmo:
|
||||
else:
|
||||
color = (*self.color, self.alpha)
|
||||
shader.uniform_float("color", color)
|
||||
shape.glenable()
|
||||
shader_wrapper.glenable()
|
||||
shader_wrapper.uniform_region(ctx)
|
||||
|
||||
shape.uniform_region(ctx)
|
||||
# shader.uniform_float('modelMatrix', self.matrix_world)
|
||||
# using `with` block to make sure matrix multiplication
|
||||
# won't affect other shaders
|
||||
with gpu.matrix.push_pop():
|
||||
gpu.matrix.multiply_matrix(self.matrix_world)
|
||||
batch.draw()
|
||||
# using matrix_world seems to be unaffected by matrix_offset
|
||||
# therefore we use basis @ offset
|
||||
matrix = self.matrix_basis @ self.matrix_offset
|
||||
gpu.matrix.multiply_matrix(matrix)
|
||||
batch.draw(shader)
|
||||
|
||||
bgl.glDisable(bgl.GL_BLEND)
|
||||
gpu.state.blend_set("NONE")
|
||||
|
||||
|
||||
class OffsetHandle:
|
||||
@@ -361,6 +365,7 @@ class UglyDotGizmo(OffsetHandle, types.Gizmo):
|
||||
self.draw_custom_shape(self.custom_shape, select_id=select_id)
|
||||
|
||||
|
||||
# TODO: dead code?
|
||||
class DotGizmo(CustomGizmo, OffsetHandle, types.Gizmo):
|
||||
"""Single dot viewport-aligned"""
|
||||
|
||||
@@ -390,10 +395,6 @@ class DotGizmo(CustomGizmo, OffsetHandle, types.Gizmo):
|
||||
self.refresh()
|
||||
self.draw_very_custom_shape(ctx, self.custom_shape, select_id=select_id)
|
||||
|
||||
# doesn't get called
|
||||
# def test_select(self, ctx, location):
|
||||
# pass
|
||||
|
||||
|
||||
class ExtrusionGuidesGizmo(CustomGizmo, types.Gizmo):
|
||||
"""Extrusion guides
|
||||
@@ -408,19 +409,25 @@ class ExtrusionGuidesGizmo(CustomGizmo, types.Gizmo):
|
||||
__slots__ = ("scale_value", "custom_shape")
|
||||
|
||||
def setup(self):
|
||||
shader = ExtrusionGuidesShader()
|
||||
self.custom_shape = shader, shader.batch(pos=((0, 0, 0), (0, 0, 1))), shader.prog
|
||||
self.use_draw_scale = False
|
||||
|
||||
def refresh(self):
|
||||
depth = self.target_get_value("depth") / self.scale_value
|
||||
self.matrix_offset.col[2][2] = depth # z-scaled
|
||||
"""setup `custom_shape`"""
|
||||
shader_wrapper = ExtrusionGuidesShader()
|
||||
verts = [Vector((0, 0, 0)), Vector((0, 0, 1))]
|
||||
verts, edges = shader_wrapper.process_geometry(verts)
|
||||
self.custom_shape = shader_wrapper, shader_wrapper.batch(
|
||||
pos=verts,
|
||||
indices=edges,
|
||||
)
|
||||
|
||||
def draw(self, ctx):
|
||||
self.refresh()
|
||||
self.draw_very_custom_shape(ctx, self.custom_shape)
|
||||
|
||||
def refresh(self):
|
||||
depth = self.target_get_value("depth") / self.scale_value
|
||||
self.matrix_offset.col[2][2] = depth # z-scaled
|
||||
|
||||
|
||||
# TODO: dead code?
|
||||
class DimensionLabelGizmo(types.Gizmo):
|
||||
"""Text label for a dimension"""
|
||||
|
||||
@@ -487,6 +494,7 @@ class ExtrusionWidget(types.GizmoGroup):
|
||||
theme = ctx.preferences.themes[0].user_interface
|
||||
scale_value = self.get_scale_value(ctx.scene.unit_settings.system, ctx.scene.unit_settings.length_unit)
|
||||
|
||||
# setup handle
|
||||
gz = self.handle = self.gizmos.new("BIM_GT_uglydot_3d")
|
||||
gz.matrix_basis = basis
|
||||
gz.scale_basis = 0.1
|
||||
@@ -497,10 +505,11 @@ class ExtrusionWidget(types.GizmoGroup):
|
||||
gz.target_set_prop("offset", prop, "value")
|
||||
gz.scale_value = scale_value
|
||||
|
||||
# setup guides
|
||||
gz = self.guides = self.gizmos.new("BIM_GT_extrusion_guides")
|
||||
gz.matrix_basis = basis
|
||||
gz.color = gz.color_highlight = tuple(theme.gizmo_secondary)
|
||||
gz.alpha = gz.alpha_highlight = 0.5
|
||||
gz.alpha = gz.alpha_highlight = 0.75
|
||||
gz.use_draw_modal = True
|
||||
gz.target_set_prop("depth", prop, "value")
|
||||
gz.scale_value = scale_value
|
||||
|
||||
@@ -23,8 +23,7 @@ from bpy.app.handlers import persistent
|
||||
|
||||
@persistent
|
||||
def toggleDecorationsOnLoad(*args):
|
||||
toggle = bpy.context.scene.DocProperties.should_draw_decorations
|
||||
if toggle:
|
||||
if bpy.context.scene.DocProperties.should_draw_decorations:
|
||||
decoration.DecorationsHandler.install(bpy.context)
|
||||
else:
|
||||
decoration.DecorationsHandler.uninstall()
|
||||
|
||||
@@ -120,9 +120,9 @@ class BoundingBox:
|
||||
|
||||
# This function stolen from https://github.com/kevancress/MeasureIt_ARCH/blob/dcf607ce0896aa2284463c6b4ae9cd023fc54cbe/measureit_arch_baseclass.py
|
||||
# MeasureIt-ARCH is GPL-v3
|
||||
# In the future I will need to rewrite this to allow the user to have custom
|
||||
# settings for each annotation object, not read from Blender.
|
||||
def format_distance(value, isArea=False, hide_units=True):
|
||||
def format_distance(
|
||||
value, isArea=False, hide_units=True, precision=None, decimal_places=None, suppress_zero_inches=False
|
||||
):
|
||||
s_code = "\u00b2" # Superscript two THIS IS LEGACY (but being kept for when Area Measurements are re-implimented)
|
||||
|
||||
# Get Scene Unit Settings
|
||||
@@ -131,7 +131,7 @@ def format_distance(value, isArea=False, hide_units=True):
|
||||
unit_length = bpy.context.scene.unit_settings.length_unit
|
||||
|
||||
toInches = 39.3700787401574887
|
||||
inPerFoot = 11.999
|
||||
inPerFoot = 11.9999
|
||||
|
||||
if isArea:
|
||||
toInches = 1550
|
||||
@@ -141,8 +141,7 @@ def format_distance(value, isArea=False, hide_units=True):
|
||||
|
||||
# Imperial Formatting
|
||||
if unit_system == "IMPERIAL":
|
||||
precision = bpy.context.scene.BIMProperties.imperial_precision
|
||||
if precision == "NONE":
|
||||
if not precision:
|
||||
precision = 256
|
||||
elif precision == "1":
|
||||
precision = 1
|
||||
@@ -155,13 +154,14 @@ def format_distance(value, isArea=False, hide_units=True):
|
||||
# Separate ft and inches
|
||||
# Unless Inches are the specified Length Unit
|
||||
if unit_length != "INCHES":
|
||||
feet = math.floor(decInches / inPerFoot)
|
||||
feet = int(decInches / inPerFoot) # remove decimal
|
||||
decInches -= feet * inPerFoot
|
||||
else:
|
||||
feet = 0
|
||||
|
||||
# Separate Fractional Inches
|
||||
inches = math.floor(decInches)
|
||||
decInches = abs(decInches) # ignore the sign for inches
|
||||
inches = math.floor(decInches) # remove decimal
|
||||
if inches != 0:
|
||||
frac = round(base * (decInches - inches))
|
||||
else:
|
||||
@@ -186,44 +186,50 @@ def format_distance(value, isArea=False, hide_units=True):
|
||||
inches = 0
|
||||
|
||||
if not isArea:
|
||||
add_inches = bool(inches) or not suppress_zero_inches
|
||||
tx_dist = ""
|
||||
if feet:
|
||||
tx_dist += str(feet) + "'"
|
||||
if feet and inches:
|
||||
if feet and add_inches:
|
||||
tx_dist += " - "
|
||||
if inches:
|
||||
if add_inches:
|
||||
tx_dist += str(inches)
|
||||
if inches and frac:
|
||||
if add_inches and frac:
|
||||
tx_dist += " "
|
||||
if frac:
|
||||
tx_dist += str(frac) + "/" + str(base)
|
||||
if inches or frac:
|
||||
if add_inches or frac:
|
||||
tx_dist += '"'
|
||||
else:
|
||||
tx_dist = str("%1.3f" % (value * toInches / inPerFoot)) + " sq. ft."
|
||||
|
||||
# METRIC FORMATTING
|
||||
elif unit_system == "METRIC":
|
||||
precision = bpy.context.scene.BIMProperties.metric_precision
|
||||
if precision != 0:
|
||||
if precision:
|
||||
value = precision * round(float(value) / precision)
|
||||
|
||||
if decimal_places:
|
||||
fmt = "%1." + str(decimal_places) + "f"
|
||||
|
||||
# Meters
|
||||
if unit_length == "METERS":
|
||||
fmt = "%1.3f"
|
||||
if not decimal_places:
|
||||
fmt = "%1.3f"
|
||||
if hide_units is False:
|
||||
fmt += " m"
|
||||
tx_dist = fmt % value
|
||||
# Centimeters
|
||||
elif unit_length == "CENTIMETERS":
|
||||
fmt = "%1.1f"
|
||||
if not decimal_places:
|
||||
fmt = "%1.1f"
|
||||
if hide_units is False:
|
||||
fmt += " cm"
|
||||
d_cm = value * (100)
|
||||
tx_dist = fmt % d_cm
|
||||
# Millimeters
|
||||
elif unit_length == "MILLIMETERS":
|
||||
fmt = "%1.0f"
|
||||
if not decimal_places:
|
||||
fmt = "%1.0f"
|
||||
if hide_units is False:
|
||||
fmt += " mm"
|
||||
d_mm = value * (1000)
|
||||
@@ -231,20 +237,21 @@ def format_distance(value, isArea=False, hide_units=True):
|
||||
|
||||
# Otherwise Use Adaptive Units
|
||||
else:
|
||||
if round(value, 2) >= 1.0:
|
||||
if round(value, 2) >= 1.0 and not decimal_places:
|
||||
fmt = "%1.3f"
|
||||
if hide_units is False:
|
||||
fmt += " m"
|
||||
tx_dist = fmt % value
|
||||
else:
|
||||
if round(value, 2) >= 0.01:
|
||||
if round(value, 2) >= 0.01 and not decimal_places:
|
||||
fmt = "%1.1f"
|
||||
if hide_units is False:
|
||||
fmt += " cm"
|
||||
d_cm = value * (100)
|
||||
tx_dist = fmt % d_cm
|
||||
else:
|
||||
fmt = "%1.0f"
|
||||
if not decimal_places:
|
||||
fmt = "%1.0f"
|
||||
if hide_units is False:
|
||||
fmt += " mm"
|
||||
d_mm = value * (1000)
|
||||
|
||||
@@ -25,7 +25,7 @@ import blenderbim.tool as tool
|
||||
import blenderbim.core.drawing as core
|
||||
import blenderbim.bim.module.drawing.annotation as annotation
|
||||
import blenderbim.bim.module.drawing.decoration as decoration
|
||||
from blenderbim.bim.module.drawing.data import DrawingsData, DecoratorData
|
||||
from blenderbim.bim.module.drawing.data import DrawingsData, DecoratorData, SheetsData, AnnotationData
|
||||
from blenderbim.bim.module.drawing.data import refresh as refresh_drawing_data
|
||||
from pathlib import Path
|
||||
from blenderbim.bim.prop import Attribute, StrProperty
|
||||
@@ -44,20 +44,14 @@ from bpy.props import (
|
||||
|
||||
|
||||
diagram_scales_enum = []
|
||||
titleblocks_enum = []
|
||||
sheets_enum = []
|
||||
vector_styles_enum = []
|
||||
|
||||
|
||||
def purge():
|
||||
global diagram_scales_enum
|
||||
global titleblocks_enum
|
||||
global sheets_enum
|
||||
global vector_styles_enum
|
||||
diagram_scales_enum = []
|
||||
titleblocks_enum = []
|
||||
sheets_enum = []
|
||||
vector_styles_enum = []
|
||||
|
||||
|
||||
def update_target_view(self, context):
|
||||
@@ -86,7 +80,7 @@ def update_diagram_scale(self, context):
|
||||
)
|
||||
except:
|
||||
return
|
||||
pset = ifcopenshell.util.element.get_psets(element).get("EPset_Drawing")
|
||||
pset = ifcopenshell.util.element.get_pset(element, "EPset_Drawing")
|
||||
if pset:
|
||||
pset = tool.Ifc.get().by_id(pset["id"])
|
||||
else:
|
||||
@@ -162,6 +156,21 @@ def update_drawing_name(self, context):
|
||||
core.update_drawing_name(tool.Ifc, tool.Drawing, drawing=drawing, name=self.name)
|
||||
|
||||
|
||||
def get_drawing_style_name(self):
|
||||
"""needed to make `set_drawing_style_name` work"""
|
||||
return self.get("name", "")
|
||||
|
||||
|
||||
def set_drawing_style_name(self, new_value):
|
||||
"""ensure the name is unique"""
|
||||
scene = bpy.context.scene
|
||||
drawing_styles = [s.name for s in scene.DocProperties.drawing_styles if s.name != self.name]
|
||||
new_value = tool.Blender.ensure_unique_name(new_value, drawing_styles)
|
||||
old_value = self.name
|
||||
self["name"] = new_value
|
||||
bpy.ops.bim.save_drawing_styles_data(rename_style=True, rename_style_from=old_value, rename_style_to=new_value)
|
||||
|
||||
|
||||
def update_schedule_name(self, context):
|
||||
schedule = tool.Ifc.get().by_id(self.ifc_definition_id)
|
||||
core.update_schedule_name(tool.Ifc, tool.Drawing, schedule=schedule, name=self.name)
|
||||
@@ -169,6 +178,10 @@ def update_schedule_name(self, context):
|
||||
|
||||
def update_has_underlay(self, context):
|
||||
update_layer(self, context, "HasUnderlay", self.has_underlay)
|
||||
# making sure that camera is active
|
||||
if self.has_underlay and (context.active_object and context.active_object.data == self.id_data):
|
||||
bpy.ops.bim.reload_drawing_styles()
|
||||
bpy.ops.bim.activate_drawing_style()
|
||||
|
||||
|
||||
def update_has_linework(self, context):
|
||||
@@ -191,20 +204,14 @@ def update_layer(self, context, name, value):
|
||||
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={name: value})
|
||||
|
||||
|
||||
def getTitleblocks(self, context):
|
||||
global titleblocks_enum
|
||||
if len(titleblocks_enum) < 1:
|
||||
titleblocks_enum.clear()
|
||||
files = Path(os.path.join(context.scene.BIMProperties.data_dir, "templates", "titleblocks")).glob("*.svg")
|
||||
files = sorted([str(f.stem) for f in files])
|
||||
titleblocks_enum.extend([(f, f, "") for f in files])
|
||||
return titleblocks_enum
|
||||
def get_titleblocks(self, context):
|
||||
if not SheetsData.is_loaded:
|
||||
SheetsData.load()
|
||||
return SheetsData.data["titleblocks"]
|
||||
|
||||
|
||||
def refreshTitleblocks(self, context):
|
||||
global titleblocks_enum
|
||||
titleblocks_enum.clear()
|
||||
getTitleblocks(self, context)
|
||||
def update_titleblocks(self, context):
|
||||
SheetsData.data["titleblocks"] = SheetsData.titleblocks()
|
||||
|
||||
|
||||
def toggleDecorations(self, context):
|
||||
@@ -213,6 +220,9 @@ def toggleDecorations(self, context):
|
||||
# TODO: design a proper text variable templating renderer
|
||||
collection = context.scene.camera.users_collection[0]
|
||||
for obj in collection.objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not tool.Drawing.is_annotation_object_type(element, ["TEXT", "TEXT_LEADER"]):
|
||||
continue
|
||||
tool.Drawing.update_text_value(obj)
|
||||
refresh_drawing_data()
|
||||
decoration.DecorationsHandler.install(context)
|
||||
@@ -220,16 +230,6 @@ def toggleDecorations(self, context):
|
||||
decoration.DecorationsHandler.uninstall()
|
||||
|
||||
|
||||
def getVectorStyles(self, context):
|
||||
global vector_styles_enum
|
||||
if len(vector_styles_enum) < 1:
|
||||
sheets_enum.clear()
|
||||
for filename in Path(os.path.join(context.scene.BIMProperties.data_dir, "styles")).glob("*.css"):
|
||||
f = str(filename.stem)
|
||||
vector_styles_enum.append((f, f, ""))
|
||||
return vector_styles_enum
|
||||
|
||||
|
||||
class Variable(PropertyGroup):
|
||||
name: StringProperty(name="Name")
|
||||
prop_key: StringProperty(name="Property Key")
|
||||
@@ -239,6 +239,7 @@ class Drawing(PropertyGroup):
|
||||
ifc_definition_id: IntProperty(name="IFC Definition ID")
|
||||
name: StringProperty(name="Name", update=update_drawing_name)
|
||||
target_view: StringProperty(name="Target View")
|
||||
is_selected: BoolProperty(name="Is Selected", default=True)
|
||||
|
||||
|
||||
class Schedule(PropertyGroup):
|
||||
@@ -248,27 +249,17 @@ class Schedule(PropertyGroup):
|
||||
|
||||
|
||||
class Sheet(PropertyGroup):
|
||||
def set_name(self, new):
|
||||
old = self.get("name")
|
||||
path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "sheets")
|
||||
if old and os.path.isfile(os.path.join(path, old + ".svg")):
|
||||
os.rename(os.path.join(path, old + ".svg"), os.path.join(path, new + ".svg"))
|
||||
self["name"] = new
|
||||
|
||||
def get_name(self):
|
||||
return self.get("name")
|
||||
|
||||
ifc_definition_id: IntProperty(name="IFC Definition ID")
|
||||
identification: StringProperty(name="Identification")
|
||||
name: StringProperty(name="Name", get=get_name, set=set_name)
|
||||
name: StringProperty(name="Name")
|
||||
is_sheet: BoolProperty(name="Is Sheet", default=False)
|
||||
reference_type: StringProperty(name="Reference Type")
|
||||
is_expanded: BoolProperty(name="Is Expanded", default=False)
|
||||
|
||||
|
||||
class DrawingStyle(PropertyGroup):
|
||||
name: StringProperty(name="Name")
|
||||
raster_style: StringProperty(name="Raster Style")
|
||||
name: StringProperty(name="Name", get=get_drawing_style_name, set=set_drawing_style_name)
|
||||
raster_style: StringProperty(name="Raster Style", default="{}")
|
||||
render_type: EnumProperty(
|
||||
items=[
|
||||
("NONE", "None", ""),
|
||||
@@ -278,7 +269,6 @@ class DrawingStyle(PropertyGroup):
|
||||
name="Render Type",
|
||||
default="VIEWPORT",
|
||||
)
|
||||
vector_style: EnumProperty(items=getVectorStyles, name="Vector Style")
|
||||
include_query: StringProperty(name="Include Query")
|
||||
exclude_query: StringProperty(name="Exclude Query")
|
||||
attributes: CollectionProperty(name="Attributes", type=StrProperty)
|
||||
@@ -337,19 +327,34 @@ class DocProperties(PropertyGroup):
|
||||
current_drawing_index: IntProperty(name="Current Drawing Index")
|
||||
schedules: CollectionProperty(name="Schedules", type=Schedule)
|
||||
active_schedule_index: IntProperty(name="Active Schedule Index")
|
||||
titleblock: EnumProperty(items=getTitleblocks, name="Titleblock", update=refreshTitleblocks)
|
||||
titleblock: EnumProperty(items=get_titleblocks, name="Titleblock", update=update_titleblocks)
|
||||
is_editing_sheets: BoolProperty(name="Is Editing Sheets", default=False)
|
||||
sheets: CollectionProperty(name="Sheets", type=Sheet)
|
||||
active_sheet_index: IntProperty(name="Active Sheet Index")
|
||||
ifc_files: CollectionProperty(name="IFCs", type=StrProperty)
|
||||
drawing_styles: CollectionProperty(name="Drawing Styles", type=DrawingStyle)
|
||||
should_draw_decorations: BoolProperty(name="Should Draw Decorations", update=toggleDecorations)
|
||||
decorations_colour: FloatVectorProperty(
|
||||
name="Decorations Colour", subtype="COLOR", default=(1, 1, 1, 1), min=0.0, max=1.0, size=4
|
||||
sheets_dir: StringProperty(default=os.path.join("sheets") + os.path.sep, name="Default Sheets Directory")
|
||||
layouts_dir: StringProperty(default=os.path.join("layouts") + os.path.sep, name="Default Layouts Directory")
|
||||
titleblocks_dir: StringProperty(
|
||||
default=os.path.join("layouts", "titleblocks") + os.path.sep, name="Default Titleblocks Directory"
|
||||
)
|
||||
drawings_dir: StringProperty(default=os.path.join("drawings") + os.path.sep, name="Default Drawings Directory")
|
||||
stylesheet_path: StringProperty(
|
||||
default=os.path.join("drawings", "assets", "default.css"), name="Default Stylesheet"
|
||||
)
|
||||
markers_path: StringProperty(default=os.path.join("drawings", "assets", "markers.svg"), name="Default Markers")
|
||||
symbols_path: StringProperty(default=os.path.join("drawings", "assets", "symbols.svg"), name="Default Symbols")
|
||||
patterns_path: StringProperty(default=os.path.join("drawings", "assets", "patterns.svg"), name="Default Patterns")
|
||||
shadingstyles_path: StringProperty(
|
||||
default=os.path.join("drawings", "assets", "shading_styles.json"), name="Default Shading Styles"
|
||||
)
|
||||
shadingstyle_default: StringProperty(default="Blender Default", name="Default Shading Style")
|
||||
|
||||
|
||||
class BIMCameraProperties(PropertyGroup):
|
||||
calculate_shapely_surfaces: BoolProperty(name="Calculate Shapely Surfaces", default=False)
|
||||
calculate_svgfill_surfaces: BoolProperty(name="Calculate SVGFill Surfaces", default=False)
|
||||
has_underlay: BoolProperty(name="Underlay", default=False, update=update_has_underlay)
|
||||
has_linework: BoolProperty(name="Linework", default=True, update=update_has_linework)
|
||||
has_annotation: BoolProperty(name="Annotation", default=True, update=update_has_annotation)
|
||||
@@ -357,6 +362,8 @@ class BIMCameraProperties(PropertyGroup):
|
||||
view_name: StringProperty(name="View Name")
|
||||
diagram_scale: EnumProperty(items=get_diagram_scales, name="Drawing Scale", update=update_diagram_scale)
|
||||
custom_diagram_scale: StringProperty(name="Custom Scale", update=update_diagram_scale)
|
||||
custom_diagram_scale_input1: StringProperty(name="Custom Scale Input 1", update=update_diagram_scale)
|
||||
custom_diagram_scale_input2: StringProperty(name="Custom Scale Input 2", update=update_diagram_scale)
|
||||
raster_x: IntProperty(name="Raster X", default=1000)
|
||||
raster_y: IntProperty(name="Raster Y", default=1000)
|
||||
is_nts: BoolProperty(name="Is NTS")
|
||||
@@ -471,3 +478,57 @@ class BIMTextProperties(PropertyGroup):
|
||||
class BIMAssignedProductProperties(PropertyGroup):
|
||||
is_editing_product: BoolProperty(name="Is Editing Product", default=False)
|
||||
relating_product: PointerProperty(name="Relating Product", type=bpy.types.Object)
|
||||
|
||||
|
||||
# ObjectType: annotation_name, description, icon, data_type
|
||||
# fmt: off
|
||||
ANNOTATION_TYPES_DATA = {
|
||||
"DIMENSION": ("Dimension", "Add dimensions annotation.\nMeasurement values can be hidden through ShowDescriptionOnly property\nof BBIM_Dimension property set", "FIXED_SIZE", "curve"),
|
||||
"ANGLE": ("Angle", "", "DRIVER_ROTATIONAL_DIFFERENCE", "curve"),
|
||||
"RADIUS": ("Radius", "", "FORWARD", "curve"),
|
||||
"DIAMETER": ("Diameter", "Add diameter annotation.\nMeasurement values can be hidden through ShowDescriptionOnly property\nof BBIM_Dimension property set", "ARROW_LEFTRIGHT", "curve"),
|
||||
"TEXT": ("Text", "", "SMALL_CAPS", "empty"),
|
||||
"TEXT_LEADER": ("Leader", "", "TRACKING_BACKWARDS", "curve"),
|
||||
"STAIR_ARROW": ("Stair Arrow", "Add stair arrow annotation.\nIf you have IfcStairFlight object selected, it will be used as a reference for the annotation", "SCREEN_BACK", "curve"),
|
||||
"HIDDEN_LINE": ("Hidden", "", "CON_TRACKTO", "mesh"),
|
||||
"PLAN_LEVEL": ("Level (Plan)", "", "SORTBYEXT", "curve"),
|
||||
"SECTION_LEVEL": ("Level (Section)", "", "TRIA_DOWN", "curve"),
|
||||
"BREAKLINE": ("Breakline", "", "FCURVE", "mesh"),
|
||||
"LINEWORK": ("Line", "", "MESH_MONKEY", "mesh"),
|
||||
"BATTING": ("Batting", "Add batting annotation.\nThickness could be changed through Thickness property of BBIM_Batting property set", "FORCE_FORCE", "mesh"),
|
||||
"FILL_AREA": ("Fill Area", "", "NODE_TEXTURE", "mesh"),
|
||||
"FALL": ("Fall", "", "SORT_ASC", "curve"),
|
||||
}
|
||||
# fmt: on
|
||||
|
||||
annotation_classes = [(x, *ANNOTATION_TYPES_DATA[x][:3], i) for i, x in enumerate(ANNOTATION_TYPES_DATA)]
|
||||
|
||||
|
||||
def get_annotation_data_prop(prop_name):
|
||||
def function(self, context):
|
||||
if not AnnotationData.is_loaded:
|
||||
AnnotationData.load()
|
||||
return AnnotationData.data[prop_name]
|
||||
|
||||
return function
|
||||
|
||||
|
||||
def update_annotation_object_type(self, context):
|
||||
self.relating_type_id = "0"
|
||||
# changing enum doesn't trigger refresh by itself
|
||||
AnnotationData.is_loaded = False
|
||||
|
||||
|
||||
class BIMAnnotationProperties(PropertyGroup):
|
||||
object_type: bpy.props.EnumProperty(
|
||||
name="Annotation Object Type", items=annotation_classes, default="TEXT", update=update_annotation_object_type
|
||||
)
|
||||
relating_type_id: bpy.props.EnumProperty(
|
||||
name="Relating Annotation Type", items=get_annotation_data_prop("relating_types")
|
||||
)
|
||||
create_representation_for_type: bpy.props.BoolProperty(
|
||||
name="Create Representation For Type",
|
||||
default=False,
|
||||
description='Whether "Add type" should define a representation for the type \n'
|
||||
"or allow occurences to have their own",
|
||||
)
|
||||
|
||||
@@ -16,12 +16,41 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from blenderbim.bim.module.drawing.svgwriter import SvgWriter
|
||||
import svgwrite
|
||||
|
||||
from odf.opendocument import load
|
||||
from odf.opendocument import load as load_ods
|
||||
from odf.table import Table, TableRow, TableColumn, TableCell
|
||||
from odf.text import P
|
||||
from odf.style import Style
|
||||
from textwrap import wrap
|
||||
from pathlib import Path
|
||||
import string
|
||||
|
||||
FONT_SIZE = 4.13
|
||||
FONT_WIDTH = lambda size: size * 0.45
|
||||
FONT_SIZE_PT = 12
|
||||
FONT_FAMILY = "OpenGost Type B TT"
|
||||
DEBUG = False
|
||||
|
||||
|
||||
def col2num(col):
|
||||
"""convert letter column index to number:
|
||||
`"A" -> 1`, `"AA" -> 27``
|
||||
"""
|
||||
num = 0
|
||||
for c in col:
|
||||
if c in string.ascii_letters:
|
||||
num = num * 26 + (ord(c.upper()) - ord("A")) + 1
|
||||
return num
|
||||
|
||||
|
||||
def a1_to_rc(cell):
|
||||
"""convert cell index from A1 format to RC: `"A1" -> (0,0)`"""
|
||||
column_letter = cell.strip(string.digits)
|
||||
col_number = col2num(column_letter) - 1
|
||||
row_number = int(cell[len(column_letter) :]) - 1
|
||||
return row_number, col_number
|
||||
|
||||
|
||||
class Scheduler:
|
||||
@@ -33,69 +62,328 @@ class Scheduler:
|
||||
)
|
||||
self.padding = 1
|
||||
self.margin = 1
|
||||
doc = load(infile)
|
||||
doc = load_ods(infile)
|
||||
|
||||
# useful for debugging ods
|
||||
if DEBUG:
|
||||
import xml.dom.minidom
|
||||
|
||||
path = Path(infile)
|
||||
dom = xml.dom.minidom.parseString(doc.xml())
|
||||
pretty_xml = dom.toprettyxml()
|
||||
|
||||
with open(path.with_suffix(".xml"), "w") as fo:
|
||||
fo.write(pretty_xml)
|
||||
|
||||
styles = {}
|
||||
for style in doc.getElementsByType(Style):
|
||||
name = style.getAttribute("name")
|
||||
if not style.firstChild:
|
||||
for cell_style in doc.getElementsByType(Style):
|
||||
name = cell_style.getAttribute("name")
|
||||
styles[name] = {}
|
||||
|
||||
# NOTE: there are also styles that inherit from parent styles that we do not process atm
|
||||
if not cell_style.firstChild:
|
||||
continue
|
||||
styles[name] = {key[1]: value for key, value in style.firstChild.attributes.items()}
|
||||
|
||||
if cell_style.firstChild.tagName in ["style:table-column-properties", "style:table-row-properties"]:
|
||||
style_children = [cell_style.firstChild]
|
||||
else:
|
||||
# for style:table-cell-properties we need to collect also text and paragraph properties
|
||||
style_children = cell_style.childNodes
|
||||
|
||||
for child in style_children:
|
||||
child_params = {key[1]: value for key, value in child.attributes.items()}
|
||||
styles[name].update(child_params)
|
||||
|
||||
table = doc.getElementsByType(Table)[0]
|
||||
|
||||
# related styles stored as a list of tuples:
|
||||
# [(child, parent), ...]
|
||||
related_styles = []
|
||||
|
||||
# collect columns width
|
||||
column_widths = []
|
||||
column_styles = []
|
||||
for col in table.getElementsByType(TableColumn):
|
||||
style_name = col.getAttribute("stylename")
|
||||
repeat = col.getAttribute("numbercolumnsrepeated")
|
||||
repeat = int(repeat) if repeat else 1
|
||||
for i in range(0, repeat):
|
||||
col_repeat = col.getAttribute("numbercolumnsrepeated")
|
||||
col_repeat = int(col_repeat) if col_repeat else 1
|
||||
for i in range(col_repeat):
|
||||
if not style_name or "column-width" not in styles[style_name]:
|
||||
column_widths.append(50)
|
||||
column_width = 50
|
||||
else:
|
||||
column_widths.append(self.convert_to_mm(styles[style_name]["column-width"]))
|
||||
column_width = self.convert_to_mm(styles[style_name]["column-width"])
|
||||
column_styles.append(style_name)
|
||||
column_widths.append(column_width)
|
||||
cell_style = col.getAttribute("defaultcellstylename")
|
||||
if cell_style:
|
||||
related_styles.append((style_name, cell_style))
|
||||
|
||||
# collect rows height
|
||||
row_heights = []
|
||||
# TODO: never used yet because unsure about priority for row styles
|
||||
# over column styles or vice versa
|
||||
row_styles = []
|
||||
|
||||
for col in table.getElementsByType(TableRow):
|
||||
style_name = col.getAttribute("stylename")
|
||||
row_repeat = col.getAttribute("numberrowsrepeated")
|
||||
row_repeat = int(row_repeat) if row_repeat else 1
|
||||
for i in range(row_repeat):
|
||||
if not style_name or "row-height" not in styles[style_name]:
|
||||
row_height = 6
|
||||
else:
|
||||
row_height = self.convert_to_mm(styles[style_name]["row-height"])
|
||||
row_styles.append(style_name)
|
||||
row_heights.append(row_height)
|
||||
cell_style = col.getAttribute("defaultcellstylename")
|
||||
if cell_style:
|
||||
related_styles.append((style_name, cell_style))
|
||||
|
||||
while len(related_styles) > 0:
|
||||
# unzip related styles to children and parents
|
||||
children, parents = zip(*related_styles)
|
||||
independent_styles = set(parents) - set(children)
|
||||
for relation in related_styles[:]:
|
||||
child, parent = relation
|
||||
if parent in independent_styles:
|
||||
child_style = styles[child]
|
||||
styles[child] = styles[parent] | child_style
|
||||
related_styles.remove(relation)
|
||||
|
||||
# TODO: multiple print ranges? 😔
|
||||
print_range = table.getAttribute("printranges")
|
||||
if print_range:
|
||||
min_rc, max_rc = [a1_to_rc(cell.rsplit(".", 1)[1]) for cell in print_range.split(":")]
|
||||
else:
|
||||
# fallback if print range is not defined
|
||||
n_rows = len(row_heights)
|
||||
n_cols = len(column_widths)
|
||||
n_cells = len(row_heights) * len(column_widths)
|
||||
cells_limit = 10000
|
||||
if n_cells >= cells_limit:
|
||||
raise Exception(
|
||||
f"You were about to build a very big table with number of cells more than {cells_limit}.\n"
|
||||
f"In fact it is {n_rows} rows x {n_cols} cols = {n_cells} cells \n"
|
||||
"and the operation was stopped to prevent system freeze.\n"
|
||||
"Please define print range in .ods file to proceede\n"
|
||||
"(needed to make sure printed table will have reasonable size)."
|
||||
)
|
||||
|
||||
min_rc, max_rc = (0, 0), (1048576, 16384)
|
||||
min_row, min_col = min_rc
|
||||
max_row, max_col = max_rc
|
||||
|
||||
# draw table
|
||||
y = self.margin
|
||||
for tri, tr in enumerate(table.getElementsByType(TableRow)):
|
||||
x = self.margin
|
||||
height = 6
|
||||
tdi = 0
|
||||
for td in tr.getElementsByType(TableCell):
|
||||
repeat = td.getAttribute("numbercolumnsrepeated")
|
||||
repeat = int(repeat) if repeat else 1
|
||||
for i in range(0, repeat):
|
||||
width = column_widths[tdi]
|
||||
self.svg.add(
|
||||
self.svg.rect(
|
||||
insert=(x, y),
|
||||
size=(width, height),
|
||||
style="fill: #ffffff; stroke-width:.125; stroke: #000000;",
|
||||
tri = 0
|
||||
stop_iterating_over_rows = False
|
||||
# TODO: row spans support?
|
||||
for tr in table.getElementsByType(TableRow):
|
||||
if stop_iterating_over_rows:
|
||||
break
|
||||
|
||||
row_repeat = tr.getAttribute("numberrowsrepeated")
|
||||
row_repeat = int(row_repeat) if row_repeat else 1
|
||||
|
||||
for i_row_repeat in range(row_repeat):
|
||||
if tri < min_row:
|
||||
tri += 1
|
||||
continue
|
||||
elif tri > max_row:
|
||||
stop_iterating_over_rows = True
|
||||
break
|
||||
|
||||
x = self.margin
|
||||
height = row_heights[tri]
|
||||
tdi = 0
|
||||
stop_iterating_over_columns = False
|
||||
|
||||
for td in tr.getElementsByType(TableCell):
|
||||
if stop_iterating_over_columns:
|
||||
break
|
||||
|
||||
column_span = td.getAttribute("numbercolumnsspanned")
|
||||
column_span = int(column_span) if column_span else 1
|
||||
|
||||
col_repeat = td.getAttribute("numbercolumnsrepeated")
|
||||
col_repeat = int(col_repeat) if col_repeat else 1
|
||||
|
||||
# figuring text alignment
|
||||
cell_style = self.get_style(td.getAttribute("stylename"), styles)
|
||||
|
||||
# drawing cells and text
|
||||
for i_col_repeat in range(col_repeat):
|
||||
start_tdi = tdi
|
||||
end_tdi = tdi + int(column_span) - 1
|
||||
# if the entire span is beyond print range => continue
|
||||
# if only part then keeping that part
|
||||
if start_tdi < min_col:
|
||||
if end_tdi < min_col:
|
||||
tdi += column_span
|
||||
continue
|
||||
else:
|
||||
start_tdi = min_col
|
||||
|
||||
# stop if start column is beyond print range
|
||||
if start_tdi > max_col:
|
||||
stop_iterating_over_columns = True
|
||||
break
|
||||
|
||||
# making sure last column won't go beyond the print range
|
||||
if end_tdi > max_col:
|
||||
end_tdi = max_col
|
||||
|
||||
width = sum(column_widths[start_tdi : end_tdi + 1])
|
||||
|
||||
self.svg.add(
|
||||
self.svg.rect(
|
||||
insert=(x, y),
|
||||
size=(width, height),
|
||||
style="fill: #ffffff; stroke-width:.125; stroke: #000000;",
|
||||
)
|
||||
)
|
||||
)
|
||||
value = td.getElementsByType(P)
|
||||
if value:
|
||||
self.add_text(value[0], x + self.padding, y + self.padding)
|
||||
x += width
|
||||
tdi += 1
|
||||
y += height
|
||||
total_width = sum(column_widths) + (self.margin * 2)
|
||||
p_tags = td.getElementsByType(P)
|
||||
|
||||
col_style = self.get_style(column_styles[tdi], styles)
|
||||
final_cell_style = cell_style or col_style
|
||||
box_alignment = self.get_box_alignment(final_cell_style)
|
||||
wrap_text = final_cell_style.get("wrap-option", None) == "wrap"
|
||||
bold_text = final_cell_style.get("font-weight", None) == "bold"
|
||||
italic_text = final_cell_style.get("font-style", None) == "italic"
|
||||
# NOTE: very naive since we're scaling text proportionally
|
||||
font_size = (
|
||||
float(final_cell_style.get("font-size", f"{FONT_SIZE_PT}pt")[:-2])
|
||||
/ FONT_SIZE_PT
|
||||
* FONT_SIZE
|
||||
)
|
||||
|
||||
if p_tags:
|
||||
# figuring text position based on alignment
|
||||
text_position = [0.0, 0.0]
|
||||
if box_alignment.endswith("left"):
|
||||
text_position[0] = x + self.padding
|
||||
elif box_alignment.endswith("middle") or box_alignment == "center":
|
||||
text_position[0] = x + width / 2
|
||||
elif box_alignment.endswith("right"):
|
||||
text_position[0] = x + width - self.padding
|
||||
|
||||
if box_alignment.startswith("top"):
|
||||
text_position[1] = y + self.padding
|
||||
elif box_alignment.startswith("middle") or box_alignment == "center":
|
||||
text_position[1] = y + height / 2
|
||||
elif box_alignment.startswith("bottom"):
|
||||
text_position[1] = y + height - self.padding
|
||||
|
||||
self.add_text(
|
||||
p_tags,
|
||||
*text_position,
|
||||
font_size=font_size,
|
||||
box_alignment=box_alignment,
|
||||
wrap_text=wrap_text,
|
||||
cell_width=width,
|
||||
bold=bold_text,
|
||||
italic=italic_text,
|
||||
)
|
||||
x += width
|
||||
tdi += column_span
|
||||
|
||||
tri += 1
|
||||
y += height
|
||||
|
||||
total_width = x + self.margin
|
||||
total_height = y + self.margin
|
||||
self.svg["width"] = "{}mm".format(total_width)
|
||||
self.svg["height"] = "{}mm".format(y)
|
||||
self.svg["viewBox"] = "0 0 {} {}".format(total_width, y)
|
||||
self.svg["height"] = "{}mm".format(total_height)
|
||||
self.svg["viewBox"] = "0 0 {} {}".format(total_width, total_height)
|
||||
self.svg.save(pretty=True)
|
||||
|
||||
def add_text(self, text, x, y):
|
||||
self.svg.add(
|
||||
self.svg.text(
|
||||
str(text).upper(),
|
||||
insert=tuple((x, y)),
|
||||
**{
|
||||
"font-size": 4.13,
|
||||
"font-family": "OpenGost Type B TT",
|
||||
"text-anchor": "start",
|
||||
"alignment-baseline": "baseline",
|
||||
"dominant-baseline": "hanging",
|
||||
}
|
||||
)
|
||||
)
|
||||
def get_style(self, style_name, styles):
|
||||
style = styles[style_name] if style_name else {}
|
||||
return style
|
||||
|
||||
def get_box_alignment(self, style):
|
||||
if style and "vertical-align" in style and style["vertical-align"] != "automatic":
|
||||
vertical_align = style["vertical-align"]
|
||||
else:
|
||||
vertical_align = "bottom"
|
||||
|
||||
alignment_translation = {
|
||||
"center": "middle",
|
||||
"end": "right",
|
||||
"start": "left",
|
||||
}
|
||||
|
||||
if style and "text-align" in style and style["text-align"] != "automatic":
|
||||
horizontal_align = style["text-align"]
|
||||
horizontal_align = alignment_translation.get(horizontal_align, horizontal_align)
|
||||
else:
|
||||
horizontal_align = "left"
|
||||
|
||||
if vertical_align == "middle" and horizontal_align == "middle":
|
||||
box_alignment = "center"
|
||||
else:
|
||||
box_alignment = f"{vertical_align}-{horizontal_align}"
|
||||
|
||||
return box_alignment
|
||||
|
||||
def add_text(
|
||||
self,
|
||||
p_tags,
|
||||
x,
|
||||
y,
|
||||
font_size,
|
||||
box_alignment="bottom-left",
|
||||
wrap_text=False,
|
||||
cell_width=100,
|
||||
bold=False,
|
||||
italic=False,
|
||||
):
|
||||
"""
|
||||
Adds text to svg.
|
||||
|
||||
Args:
|
||||
p_tags: list of cell's P tags from odt file
|
||||
box_alignment: alignment of text in box
|
||||
wrap_text: if True, text will be wrapped to fit in cell
|
||||
cell_width: width of cell, used for wrapping text
|
||||
"""
|
||||
text_lines = [str(p).upper() for p in p_tags]
|
||||
box_alignment_params = SvgWriter.get_box_alignment_parameters(box_alignment)
|
||||
text_params = {
|
||||
"font-size": font_size,
|
||||
"font-family": FONT_FAMILY,
|
||||
}
|
||||
if bold:
|
||||
text_params["font-weight"] = "bold"
|
||||
if italic:
|
||||
text_params["font-style"] = "italic"
|
||||
|
||||
if len(text_lines) == 1 and not wrap_text:
|
||||
text_params.update(box_alignment_params)
|
||||
text_tag = self.svg.text(text_lines[0], insert=(x, y), **(text_params))
|
||||
self.svg.add(text_tag)
|
||||
return
|
||||
|
||||
text_tag = self.svg.text("", **(text_params | {"font-size": "0"} | box_alignment_params))
|
||||
|
||||
# TODO: should be done in less naive way
|
||||
# without using magic number for FONT_WIDTH
|
||||
# currently it might not work for all fonts and font sizes
|
||||
if wrap_text:
|
||||
wrapped_lines = []
|
||||
for line in text_lines:
|
||||
wrapped_line = wrap(line, width=int(cell_width // FONT_WIDTH(font_size)), break_long_words=False)
|
||||
wrapped_lines.extend(wrapped_line)
|
||||
else:
|
||||
wrapped_lines = text_lines
|
||||
|
||||
for line_number, text_line in enumerate(wrapped_lines[::-1]):
|
||||
# position has to be inserted at tspan to avoid x offset between tspans
|
||||
tspan = self.svg.tspan(text_line, insert=(x, y), **text_params)
|
||||
# doing it here and not in tspan constructor because constructor adds unnecessary spaces
|
||||
tspan.update({"dy": f"-{line_number}em"})
|
||||
text_tag.add(tspan)
|
||||
self.svg.add(text_tag)
|
||||
|
||||
def convert_to_mm(self, value):
|
||||
# XSL is what defines the units of measurements in ODF
|
||||
|
||||
@@ -16,10 +16,200 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bgl
|
||||
from mathutils import Matrix
|
||||
from gpu.types import GPUShader
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
import gpu
|
||||
from mathutils import Vector
|
||||
|
||||
|
||||
# NOTES:
|
||||
# Since Metal doesn't support geometry shaders we stick to builtin shaders
|
||||
# and generate all geometry data in python before passing it to the shader.
|
||||
# This way was considered to be the most reliable atm.
|
||||
# More: https://blender.stackexchange.com/questions/291674/migrating-geometry-shaders-to-metal
|
||||
#
|
||||
# BGL deprecation:
|
||||
# since `bgl` is deprecated, creating smoothing lines became tricky
|
||||
# Notes for creating shaders with smoothed lines:
|
||||
# in geom shader - use triangle_strip, DEFAULT_SETUP, do_edge_verts or do_vertex to emit vertices
|
||||
# in frag shader - use lineWidth uniform, smoothline flaot in, smoothing shader code from base shader
|
||||
# mind the vertex limit since emitting vertices for smoothed lines produces twice as much vertices
|
||||
|
||||
|
||||
BASE_DEF_GLSL = """
|
||||
#define PI 3.141592653589793
|
||||
#define MAX_POINTS 64
|
||||
#define CIRCLE_SEGS 12
|
||||
#define SMOOTH_WIDTH 1.0
|
||||
#define lineSmooth true
|
||||
"""
|
||||
|
||||
BASE_LIB_GLSL = """
|
||||
// TODO: redefine as macor instead
|
||||
uniform vec2 winsize;
|
||||
uniform float lineWidth;
|
||||
#define half_winsize (winsize / 2)
|
||||
// convert camera to window
|
||||
#define C2W(v) vec4(v.x * half_winsize.x / v.w, v.y * half_winsize.y / v.w, v.z / v.w, 1)
|
||||
// convert window to camera
|
||||
#define W2C(v) vec4(v.x * v.w / half_winsize.x, v.y * v.w / half_winsize.y, v.z * v.w, 1)
|
||||
|
||||
void emitSegment(vec4 p0, vec4 p1) {
|
||||
gl_Position = p0;
|
||||
EmitVertex();
|
||||
gl_Position = p1;
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
|
||||
void emitTriangle(vec4 p1, vec4 p2, vec4 p3) {
|
||||
gl_Position = p1;
|
||||
EmitVertex();
|
||||
gl_Position = p2;
|
||||
EmitVertex();
|
||||
gl_Position = p3;
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
|
||||
#define matCLIP2WIN() vec4(winsize.x/2, winsize.y/2, 1, 1)
|
||||
#define CLIP2WIN(v) (clip2win * (v) / (v).w)
|
||||
#define matWIN2CLIP() vec4(2/winsize.x, 2/winsize.y, 1, 1)
|
||||
#define WIN2CLIP(v) (win2clip * (v) * (v).w)
|
||||
#define DEFAULT_SETUP() vec4 clip2win = matCLIP2WIN(), win2clip = matWIN2CLIP(); vec2 EDGE_DIR
|
||||
|
||||
out float smoothline;
|
||||
|
||||
void arrow_head(in vec4 dir, in float size, in float angle, out vec4 head[3]) {
|
||||
vec4 nose = dir * size;
|
||||
float c = cos(angle), s = sin(angle);
|
||||
head[0] = nose;
|
||||
head[1] = vec4(mat2(c, -s, +s, c) * nose.xy, 0, 0);
|
||||
head[2] = vec4(mat2(c, +s, -s, c) * nose.xy, 0, 0);
|
||||
}
|
||||
|
||||
void circle_head(in float size, out vec4 head[CIRCLE_SEGS]) {
|
||||
float angle_d = PI * 2 / CIRCLE_SEGS;
|
||||
for(int i = 0; i<CIRCLE_SEGS; i++) {
|
||||
float angle = angle_d * i;
|
||||
head[i] = vec4(cos(angle), sin(angle), 0, 0) * size;
|
||||
}
|
||||
}
|
||||
|
||||
bool check_counterclockwise(in vec4 A, in vec4 B, in vec4 C) {
|
||||
return (C.y-A.y) * (B.x-A.x) > (B.y-A.y) * (C.x-A.x);
|
||||
}
|
||||
|
||||
void angle_circle_head(
|
||||
in vec4 circle_start, in float circle_angle,
|
||||
in bool counterclockwise,
|
||||
out vec4 head[CIRCLE_SEGS+1], out float angle_segs) {
|
||||
|
||||
// 1 added to CIRCLE_SEGS because we're number of vertices
|
||||
// for n segments is n+1
|
||||
|
||||
float angle_d;
|
||||
angle_d = PI * 2 / CIRCLE_SEGS; // 30d
|
||||
// need to bottom clamp it to 1, otherwise it causes Blender crash at extruding the curve
|
||||
angle_segs = max(1, ceil(circle_angle / angle_d));
|
||||
angle_d = circle_angle / angle_segs;
|
||||
|
||||
for(int i = 0; i < (angle_segs + 1); i++) {
|
||||
float angle = angle_d * i;
|
||||
if (counterclockwise) {
|
||||
head[i] = vec4(
|
||||
circle_start.x * cos(-angle) + circle_start.y * sin(-angle),
|
||||
circle_start.x * -sin(-angle) + circle_start.y * cos(-angle),
|
||||
0, 0
|
||||
);
|
||||
} else {
|
||||
head[i] = vec4(
|
||||
circle_start.x * cos(angle) + circle_start.y * sin(angle),
|
||||
circle_start.x * -sin(angle) + circle_start.y * cos(angle),
|
||||
0, 0
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void cross_head(in vec4 dir, in float size, out vec4 head[3]) {
|
||||
vec4 nose = dir * size;
|
||||
float c = cos(PI/2), s = sin(PI/2);
|
||||
head[0] = nose;
|
||||
head[1] = vec4(mat2(c, -s, +s, c) * nose.xy, 0, 0);
|
||||
head[2] = vec4(mat2(c, +s, -s, c) * nose.xy, 0, 0);
|
||||
}
|
||||
|
||||
// do_vertex_util is custom EmitVertex method to support line smoothing
|
||||
// `e` - edge xy direction in win/clip space (directions are invariant in both spaces)
|
||||
#define do_vertex(pos, e) (do_vertex_util(pos, vec2(-(e).y, (e).x) / winsize.xy))
|
||||
#define do_vertex_win(pos, e) ( do_vertex( WIN2CLIP( pos ), e ) )
|
||||
|
||||
// if vertex is shared by two segments of the line still need to emit it twice
|
||||
// to avoid smoothing artifacts
|
||||
// don't forget to initialize `vec2 EDGE_DIR` for macro to work
|
||||
// `pos0` / `pos1` - vertex position in clip space
|
||||
#define do_edge_verts(pos0, pos1) ( EDGE_DIR = normalize( ( (pos1) - (pos0) ).xy ), do_vertex( pos0, EDGE_DIR ), do_vertex( pos1, EDGE_DIR) )
|
||||
#define do_edge_verts_win(pos0, pos1) ( do_edge_verts( WIN2CLIP(pos0), WIN2CLIP(pos1) ) )
|
||||
void do_vertex_util(vec4 pos, vec2 ofs)
|
||||
{
|
||||
float final_line_width = lineWidth + SMOOTH_WIDTH * float(lineSmooth);
|
||||
ofs *= final_line_width;
|
||||
|
||||
smoothline = final_line_width * 0.5;
|
||||
gl_Position = pos;
|
||||
gl_Position.xy += ofs * pos.w;
|
||||
EmitVertex();
|
||||
|
||||
smoothline = -final_line_width * 0.5;
|
||||
gl_Position = pos;
|
||||
gl_Position.xy -= ofs * pos.w;
|
||||
EmitVertex();
|
||||
}
|
||||
|
||||
// geometry utils
|
||||
void triangle_head(in vec4 side, in vec4 dir, in float length, in float width, in float radius, out vec4 head[5]) {
|
||||
// TODO: radius is unnecessary?
|
||||
head[0] = side * -radius;
|
||||
head[1] = side * length * -.5;
|
||||
head[2] = dir * width;
|
||||
head[3] = side * length * .5;
|
||||
head[4] = side * radius;
|
||||
}
|
||||
|
||||
void do_triangle_head(vec4 pos_w, vec4 head[5]) {
|
||||
DEFAULT_SETUP();
|
||||
do_edge_verts_win(pos_w + head[0], pos_w + head[1]);
|
||||
do_edge_verts_win(pos_w + head[1], pos_w + head[2]);
|
||||
do_edge_verts_win(pos_w + head[2], pos_w + head[3]);
|
||||
do_edge_verts_win(pos_w + head[3], pos_w + head[4]);
|
||||
}
|
||||
|
||||
void do_circle_head(vec4 pos_w, vec4 head[CIRCLE_SEGS]) {
|
||||
DEFAULT_SETUP();
|
||||
for(int i=0; i<CIRCLE_SEGS-1; i++) {
|
||||
do_edge_verts_win(pos_w + head[i], pos_w + head[i+1]);
|
||||
EmitVertex();
|
||||
}
|
||||
do_edge_verts_win( pos_w + head[CIRCLE_SEGS-1], pos_w + head[0] );
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def add_verts_sequence(verts, start_i, output_verts, output_edges, closed=False):
|
||||
"""Add sequence of verts to output lists, returns next vertex index"""
|
||||
for i, v in enumerate(verts[:-1], start_i):
|
||||
output_verts.append(v)
|
||||
output_edges.append((i, i + 1))
|
||||
output_verts.append(verts[-1])
|
||||
if closed:
|
||||
output_edges.append((i + 1, start_i))
|
||||
return i + 2
|
||||
|
||||
|
||||
def add_offsets(v, offsets):
|
||||
"""returns list of verts with offsets added"""
|
||||
return [v + offset for offset in offsets]
|
||||
|
||||
|
||||
class BaseShader:
|
||||
@@ -28,7 +218,7 @@ class BaseShader:
|
||||
|
||||
The Geometry shader works in clipping coords (aftre projecting before division and window scaling).
|
||||
To make window-scale geometry, vectors should be calculated in window space and than back-projected to clipping spae.
|
||||
Provide `winSize` uniform vector with halfsize of window, and use W2C and C2W macros.
|
||||
Provide `half_winsize` uniform vector with halfsize of window, and use W2C and C2W macros.
|
||||
|
||||
Replace glsl code in derived classes.
|
||||
Beware of unused attributes and uniforms: glsl compiler will optimize them out and blender fail with an exception.
|
||||
@@ -43,9 +233,7 @@ class BaseShader:
|
||||
|
||||
TYPE = None # should be LINES|POINTS|etc
|
||||
|
||||
DEF_GLSL = """
|
||||
#define PI 3.141592653589793
|
||||
"""
|
||||
DEF_GLSL = BASE_DEF_GLSL
|
||||
|
||||
VERT_GLSL = """
|
||||
uniform mat4 viewMatrix;
|
||||
@@ -63,84 +251,86 @@ class BaseShader:
|
||||
"""
|
||||
|
||||
# prepended to geom_glsl
|
||||
LIB_GLSL = """
|
||||
uniform vec2 winSize;
|
||||
// convert camera to window
|
||||
#define C2W(v) vec4(v.x * winSize.x / v.w, v.y * winSize.y / v.w, v.z / v.w, 1)
|
||||
// convert window to camera
|
||||
#define W2C(v) vec4(v.x * v.w / winSize.x, v.y * v.w / winSize.y, v.z * v.w, 1)
|
||||
|
||||
void emitSegment(vec4 p0, vec4 p1) {
|
||||
gl_Position = p0;
|
||||
EmitVertex();
|
||||
gl_Position = p1;
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
|
||||
void emitTriangle(vec4 p1, vec4 p2, vec4 p3) {
|
||||
gl_Position = p1;
|
||||
EmitVertex();
|
||||
gl_Position = p2;
|
||||
EmitVertex();
|
||||
gl_Position = p3;
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
"""
|
||||
LIB_GLSL = BASE_LIB_GLSL
|
||||
|
||||
GEOM_GLSL = """
|
||||
uniform float viewportDrawingScale;
|
||||
|
||||
layout(lines) in;
|
||||
layout(triangle_strip, max_vertices = 4) out;
|
||||
|
||||
void main() {
|
||||
// default setup for macro to work
|
||||
vec4 clip2win = matCLIP2WIN();
|
||||
vec4 win2clip = matWIN2CLIP();
|
||||
vec2 EDGE_DIR;
|
||||
|
||||
vec4 p0 = gl_in[0].gl_Position, p1 = gl_in[1].gl_Position;
|
||||
do_edge_verts(p0, p1);
|
||||
EndPrimitive();
|
||||
}
|
||||
"""
|
||||
|
||||
FRAG_GLSL = """
|
||||
uniform vec4 color;
|
||||
uniform float lineWidth;
|
||||
|
||||
in float smoothline;
|
||||
out vec4 fragColor;
|
||||
|
||||
void main() {
|
||||
vec2 co = gl_FragCoord.xy;
|
||||
|
||||
fragColor = color;
|
||||
if (lineSmooth) {
|
||||
fragColor.a *= clamp((lineWidth + SMOOTH_WIDTH) * 0.5 - abs(smoothline), 0.0, 1.0);
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
# NB: libcode arg doesn't work
|
||||
self.prog = GPUShader(
|
||||
vertexcode=self.VERT_GLSL,
|
||||
fragcode=self.FRAG_GLSL,
|
||||
geocode=self.LIB_GLSL + self.GEOM_GLSL,
|
||||
defines=self.DEF_GLSL,
|
||||
)
|
||||
# 3D_POLYLINE_UNIFORM_COLOR is good for smoothed lines since `bgl.enable(GL_LINE_SMOOTH)` is deprecated
|
||||
self.line_shader = gpu.shader.from_builtin("3D_POLYLINE_UNIFORM_COLOR")
|
||||
self.base_shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
|
||||
|
||||
def get_shader(self):
|
||||
"""Returns shader for this type"""
|
||||
return self.line_shader if self.TYPE == "LINES" else self.base_shader
|
||||
|
||||
def batch(self, indices=None, **data):
|
||||
"""Returns automatic GPUBatch filled with provided parameters"""
|
||||
batch = batch_for_shader(self.prog, self.TYPE, data, indices=indices)
|
||||
batch.program_set(self.prog)
|
||||
shader = self.get_shader()
|
||||
batch = batch_for_shader(shader, self.TYPE, data, indices=indices)
|
||||
return batch
|
||||
|
||||
def bind(self):
|
||||
self.prog.bind()
|
||||
"""need to bind shader before changing it's uniforms"""
|
||||
shader = self.get_shader()
|
||||
shader.bind()
|
||||
return shader
|
||||
|
||||
def glenable(self):
|
||||
bgl.glEnable(bgl.GL_BLEND)
|
||||
bgl.glBlendFunc(bgl.GL_SRC_ALPHA, bgl.GL_ONE_MINUS_SRC_ALPHA)
|
||||
bgl.glBlendEquation(bgl.GL_FUNC_ADD)
|
||||
|
||||
# bgl.glEnable(bgl.GL_DEPTH_TEST)
|
||||
# bgl.glDepthFunc(bgl.GL_LEQUAL)
|
||||
# bgl.glDepthMask(True)
|
||||
gpu.state.blend_set("ALPHA")
|
||||
gpu.state.depth_test_set("LESS_EQUAL")
|
||||
|
||||
def uniform_region(self, ctx):
|
||||
shader = self.bind()
|
||||
|
||||
region = ctx.region
|
||||
region3d = ctx.region_data
|
||||
try:
|
||||
self.prog.uniform_float("viewMatrix", region3d.perspective_matrix)
|
||||
except ValueError: # unused uniform
|
||||
pass
|
||||
try:
|
||||
self.prog.uniform_float("winSize", (region.width / 2, region.height / 2))
|
||||
except ValueError: # unused uniform
|
||||
pass
|
||||
|
||||
uniform_floats = {
|
||||
"ModelViewProjectionMatrix": region3d.perspective_matrix,
|
||||
# POLYLINE_UNIFORM_COLOR specific uniforms
|
||||
"viewportSize": (region.width, region.height),
|
||||
"lineWidth": 2.5,
|
||||
}
|
||||
|
||||
for name, value in uniform_floats.items():
|
||||
shader.uniform_float(name, value)
|
||||
|
||||
|
||||
# TODO: add smoothing if this shader is going to be used
|
||||
# TODO: dead code?
|
||||
class BaseLinesShader(BaseShader):
|
||||
"""Draws line segments with gaps around vertices at endpoints"""
|
||||
|
||||
@@ -185,7 +375,6 @@ class BaseLinesShader(BaseShader):
|
||||
|
||||
def glenable(self):
|
||||
super().glenable()
|
||||
bgl.glEnable(bgl.GL_LINE_SMOOTH)
|
||||
|
||||
|
||||
class GizmoShader(BaseShader):
|
||||
@@ -207,6 +396,7 @@ class GizmoShader(BaseShader):
|
||||
"""
|
||||
|
||||
|
||||
# TODO: add smoothing if this shader is going to be used
|
||||
class DotsGizmoShader(GizmoShader):
|
||||
"""Draws circles of radius 1 around points"""
|
||||
|
||||
@@ -263,35 +453,24 @@ class ExtrusionGuidesShader(GizmoShader):
|
||||
|
||||
TYPE = "LINES"
|
||||
|
||||
DEF_GLSL = (
|
||||
BaseShader.DEF_GLSL
|
||||
+ """
|
||||
#define CROSS_SIZE .5
|
||||
"""
|
||||
)
|
||||
def process_geometry(self, verts):
|
||||
CROSS_SIZE = 0.5
|
||||
|
||||
GEOM_GLSL = """
|
||||
uniform mat4 ModelViewProjectionMatrix;
|
||||
p0, p1 = verts
|
||||
bx = Vector((1, 0, 0)) * CROSS_SIZE
|
||||
by = Vector((0, 1, 0)) * CROSS_SIZE
|
||||
|
||||
layout(lines) in;
|
||||
layout(line_strip, max_vertices=10) out;
|
||||
output_verts = []
|
||||
output_edges = []
|
||||
out_kwargs = {
|
||||
"output_verts": output_verts,
|
||||
"output_edges": output_edges,
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec4 p0 = gl_in[0].gl_Position, p1 = gl_in[1].gl_Position;
|
||||
vec4 p0w = C2W(p0), p1w = C2W(p1);
|
||||
start_i = 0
|
||||
start_i = add_verts_sequence(add_offsets(p0, [-bx, bx]), start_i, **out_kwargs)
|
||||
start_i = add_verts_sequence(add_offsets(p0, [-by, by]), start_i, **out_kwargs)
|
||||
start_i = add_verts_sequence(add_offsets(p1, [-bx, bx]), start_i, **out_kwargs)
|
||||
start_i = add_verts_sequence(add_offsets(p1, [-by, by]), start_i, **out_kwargs)
|
||||
|
||||
emitSegment(p0, p1);
|
||||
|
||||
vec4 bx = ModelViewProjectionMatrix[0] * CROSS_SIZE;
|
||||
vec4 by = ModelViewProjectionMatrix[1] * CROSS_SIZE;
|
||||
|
||||
emitSegment(p0 - bx, p0 + bx);
|
||||
emitSegment(p0 - by, p0 + by);
|
||||
emitSegment(p1 - bx, p1 + bx);
|
||||
emitSegment(p1 - by, p1 + by);
|
||||
}
|
||||
"""
|
||||
|
||||
def glenable(self):
|
||||
super().glenable()
|
||||
bgl.glEnable(bgl.GL_LINE_SMOOTH)
|
||||
return output_verts, output_edges
|
||||
|
||||
@@ -17,15 +17,20 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import bpy
|
||||
import uuid
|
||||
import shutil
|
||||
import ntpath
|
||||
import pystache
|
||||
import urllib.parse
|
||||
import xml.etree.ElementTree as ET
|
||||
import blenderbim.tool as tool
|
||||
import ifcopenshell.util.geolocation
|
||||
from shutil import copy
|
||||
from xml.dom import minidom
|
||||
from mathutils import Vector
|
||||
|
||||
VIEW_TITLE_OFFSET_Y = 5
|
||||
DEFAULT_POSITION = Vector((30, 30))
|
||||
|
||||
|
||||
class SheetBuilder:
|
||||
@@ -33,22 +38,29 @@ class SheetBuilder:
|
||||
self.data_dir = None
|
||||
self.scale = "NTS"
|
||||
|
||||
def create(self, sheet_path, titleblock_name):
|
||||
def create(self, layout_path, titleblock_name):
|
||||
root = ET.Element("svg")
|
||||
root.attrib["xmlns"] = "http://www.w3.org/2000/svg"
|
||||
root.attrib["xmlns:xlink"] = "http://www.w3.org/1999/xlink"
|
||||
root.attrib["id"] = "root"
|
||||
root.attrib["version"] = "1.1"
|
||||
|
||||
view_root = ET.parse(
|
||||
os.path.join(self.data_dir, "templates", "titleblocks", titleblock_name + ".svg")
|
||||
).getroot()
|
||||
sheet_dir = os.path.dirname(layout_path)
|
||||
ootb_titleblock_path = os.path.join(self.data_dir, "templates", "titleblocks", titleblock_name + ".svg")
|
||||
titleblock_path = tool.Ifc.resolve_uri(tool.Drawing.get_default_titleblock_path(titleblock_name))
|
||||
|
||||
os.makedirs(sheet_dir, exist_ok=True)
|
||||
os.makedirs(os.path.dirname(titleblock_path), exist_ok=True)
|
||||
if not os.path.exists(titleblock_path):
|
||||
shutil.copy(ootb_titleblock_path, titleblock_path)
|
||||
|
||||
view_root = ET.parse(titleblock_path).getroot()
|
||||
view_width = self.convert_to_mm(view_root.attrib.get("width"))
|
||||
view_height = self.convert_to_mm(view_root.attrib.get("height"))
|
||||
view = ET.SubElement(root, "g")
|
||||
view.attrib["data-type"] = "titleblock"
|
||||
titleblock = ET.SubElement(view, "image")
|
||||
titleblock.attrib["xlink:href"] = "../templates/titleblocks/" + titleblock_name + ".svg"
|
||||
titleblock.attrib["xlink:href"] = os.path.relpath(titleblock_path, sheet_dir)
|
||||
titleblock.attrib["x"] = "0"
|
||||
titleblock.attrib["y"] = "0"
|
||||
titleblock.attrib["width"] = str(view_width)
|
||||
@@ -58,26 +70,25 @@ class SheetBuilder:
|
||||
root.attrib["height"] = "{}mm".format(view_height)
|
||||
root.attrib["viewBox"] = "0 0 {} {}".format(view_width, view_height)
|
||||
|
||||
with open(sheet_path, "w") as f:
|
||||
with open(layout_path, "w") as f:
|
||||
f.write(minidom.parseString(ET.tostring(root)).toprettyxml(indent=" "))
|
||||
|
||||
def add_drawing(self, reference, drawing, sheet):
|
||||
filename = drawing.Name
|
||||
sheet_name = os.path.splitext(os.path.basename(tool.Drawing.get_document_uri(sheet)))[0]
|
||||
sheet_dir = os.path.join(self.data_dir, "sheets")
|
||||
drawing_dir = os.path.join(self.data_dir, "diagrams")
|
||||
sheet_path = os.path.join(sheet_dir, sheet_name + ".svg")
|
||||
drawing_path = os.path.join(drawing_dir, filename + ".svg")
|
||||
underlay_path = os.path.join(drawing_dir, filename + "-underlay.png")
|
||||
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
|
||||
layout_dir = os.path.dirname(layout_path)
|
||||
|
||||
if not os.path.isfile(sheet_path):
|
||||
drawing_path = tool.Drawing.get_document_uri(tool.Drawing.get_drawing_reference(drawing))
|
||||
underlay_path = os.path.splitext(drawing_path)[0] + "-underlay.png"
|
||||
|
||||
if not os.path.exists(layout_path) or not os.path.exists(drawing_path):
|
||||
raise FileNotFoundError
|
||||
|
||||
ET.register_namespace("", "http://www.w3.org/2000/svg")
|
||||
ET.register_namespace("xlink", "http://www.w3.org/1999/xlink")
|
||||
|
||||
sheet_tree = ET.parse(sheet_path)
|
||||
sheet_root = sheet_tree.getroot()
|
||||
layout_tree = ET.parse(layout_path)
|
||||
layout_root = layout_tree.getroot()
|
||||
|
||||
view_tree = ET.parse(drawing_path)
|
||||
view_root = view_tree.getroot()
|
||||
@@ -85,103 +96,161 @@ class SheetBuilder:
|
||||
# The view is placed into a group with a background image element.
|
||||
# Although the foreground SVG already has a background, it is duplicated
|
||||
# here to accommodate browsers which do not nest images.
|
||||
view = ET.SubElement(sheet_root, "g")
|
||||
view = ET.SubElement(layout_root, "g")
|
||||
view.attrib["data-type"] = "drawing"
|
||||
view.attrib["data-id"] = str(reference.id())
|
||||
view.attrib["data-drawing"] = drawing.GlobalId
|
||||
view_width = self.convert_to_mm(view_root.attrib.get("width"))
|
||||
view_height = self.convert_to_mm(view_root.attrib.get("height"))
|
||||
|
||||
# add background
|
||||
if os.path.isfile(underlay_path):
|
||||
background = ET.SubElement(view, "image")
|
||||
background.attrib["data-type"] = "background"
|
||||
background.attrib["xlink:href"] = os.path.relpath(underlay_path, sheet_dir)
|
||||
background.attrib["x"] = "30"
|
||||
background.attrib["y"] = "30"
|
||||
background.attrib["xlink:href"] = os.path.relpath(underlay_path, layout_dir)
|
||||
background.attrib["x"] = str(DEFAULT_POSITION.x)
|
||||
background.attrib["y"] = str(DEFAULT_POSITION.y)
|
||||
background.attrib["width"] = str(view_width)
|
||||
background.attrib["height"] = str(view_height)
|
||||
|
||||
# add foreground
|
||||
if os.path.isfile(drawing_path):
|
||||
foreground = ET.SubElement(view, "image")
|
||||
foreground.attrib["data-type"] = "foreground"
|
||||
foreground.attrib["xlink:href"] = os.path.relpath(drawing_path, sheet_dir)
|
||||
foreground.attrib["x"] = "30"
|
||||
foreground.attrib["y"] = "30"
|
||||
foreground.attrib["xlink:href"] = os.path.relpath(drawing_path, layout_dir)
|
||||
foreground.attrib["x"] = str(DEFAULT_POSITION.x)
|
||||
foreground.attrib["y"] = str(DEFAULT_POSITION.y)
|
||||
foreground.attrib["width"] = str(view_width)
|
||||
foreground.attrib["height"] = str(view_height)
|
||||
|
||||
self.add_view_title(30, view_height + 35, view)
|
||||
sheet_tree.write(sheet_path)
|
||||
self.add_view_title(
|
||||
DEFAULT_POSITION.x, view_height + DEFAULT_POSITION.y + VIEW_TITLE_OFFSET_Y, view, layout_dir
|
||||
)
|
||||
layout_tree.write(layout_path)
|
||||
|
||||
def update_sheet_drawing_sizes(self, sheet):
|
||||
ET.register_namespace("", "http://www.w3.org/2000/svg")
|
||||
SVG = "{http://www.w3.org/2000/svg}"
|
||||
|
||||
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
|
||||
layout_tree = ET.parse(layout_path)
|
||||
layout_root = layout_tree.getroot()
|
||||
ifc_file = tool.Ifc.get()
|
||||
|
||||
# iterate over all drawings in the sheet
|
||||
drawings_views = layout_root.findall(f'{SVG}g[@data-type="drawing"]')
|
||||
|
||||
for drawing_view in drawings_views:
|
||||
# find drawing in ifc file to get the drawing dimensions
|
||||
drawing = ifc_file.by_guid(drawing_view.attrib.get("data-drawing"))
|
||||
drawing_path = tool.Drawing.get_document_uri(tool.Drawing.get_drawing_reference(drawing))
|
||||
drawing_tree = ET.parse(drawing_path)
|
||||
drawing_root = drawing_tree.getroot()
|
||||
view_width = self.convert_to_mm(drawing_root.attrib.get("width"))
|
||||
view_height = self.convert_to_mm(drawing_root.attrib.get("height"))
|
||||
|
||||
foreground = drawing_view.find(f'.//{SVG}image[@data-type="foreground"]')
|
||||
height = float(foreground.attrib["height"])
|
||||
width = float(foreground.attrib["width"])
|
||||
readjust = Vector((width - view_width, height - view_height)) / 2
|
||||
|
||||
for image in drawing_view.findall(f"{SVG}image"):
|
||||
x = float(image.attrib["x"])
|
||||
y = float(image.attrib["y"])
|
||||
if image.attrib["data-type"] == "view-title":
|
||||
image.attrib["x"] = str(x - readjust.x)
|
||||
image.attrib["y"] = str(y - readjust.y)
|
||||
else:
|
||||
image.attrib["x"] = str(x + readjust.x)
|
||||
image.attrib["y"] = str(y + readjust.y)
|
||||
image.attrib["width"] = str(view_width)
|
||||
image.attrib["height"] = str(view_height)
|
||||
|
||||
layout_tree.write(layout_path)
|
||||
|
||||
def remove_drawing(self, reference, sheet):
|
||||
ET.register_namespace("", "http://www.w3.org/2000/svg")
|
||||
|
||||
sheet_path = tool.Drawing.get_document_uri(sheet)
|
||||
sheet_tree = ET.parse(sheet_path)
|
||||
sheet_root = sheet_tree.getroot()
|
||||
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
|
||||
layout_tree = ET.parse(layout_path)
|
||||
layout_root = layout_tree.getroot()
|
||||
|
||||
for g in sheet_root.findall("{http://www.w3.org/2000/svg}g"):
|
||||
for g in layout_root.findall("{http://www.w3.org/2000/svg}g"):
|
||||
if g.attrib.get("data-id") == str(reference.id()):
|
||||
sheet_root.remove(g)
|
||||
layout_root.remove(g)
|
||||
break
|
||||
|
||||
sheet_tree.write(sheet_path)
|
||||
layout_tree.write(layout_path)
|
||||
|
||||
def add_schedule(self, reference, schedule, sheet):
|
||||
view_path = tool.Drawing.get_document_uri(schedule)
|
||||
view_path = tool.Drawing.get_path_with_ext(tool.Drawing.get_document_uri(schedule), "svg")
|
||||
if not os.path.exists(view_path):
|
||||
tool.Drawing.create_svg_schedule(schedule)
|
||||
schedule_name = os.path.splitext(os.path.basename(view_path))[0]
|
||||
sheet_path = tool.Drawing.get_document_uri(sheet)
|
||||
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
|
||||
layout_dir = os.path.dirname(layout_path)
|
||||
|
||||
ET.register_namespace("", "http://www.w3.org/2000/svg")
|
||||
ET.register_namespace("xlink", "http://www.w3.org/1999/xlink")
|
||||
|
||||
sheet_tree = ET.parse(sheet_path)
|
||||
sheet_root = sheet_tree.getroot()
|
||||
layout_tree = ET.parse(layout_path)
|
||||
layout_root = layout_tree.getroot()
|
||||
|
||||
view_tree = ET.parse(view_path)
|
||||
view_root = view_tree.getroot()
|
||||
view_width = self.convert_to_mm(view_root.attrib.get("width"))
|
||||
view_height = self.convert_to_mm(view_root.attrib.get("height"))
|
||||
|
||||
view = ET.SubElement(sheet_root, "g")
|
||||
view = ET.SubElement(layout_root, "g")
|
||||
view.attrib["data-type"] = "schedule"
|
||||
view.attrib["data-id"] = str(reference.id())
|
||||
view.attrib["data-schedule"] = str(schedule.id())
|
||||
|
||||
foreground = ET.SubElement(view, "image")
|
||||
foreground.attrib["data-type"] = "table"
|
||||
foreground.attrib["xlink:href"] = "../schedules/{}.svg".format(schedule_name)
|
||||
foreground.attrib["x"] = "30"
|
||||
foreground.attrib["y"] = "30"
|
||||
foreground.attrib["xlink:href"] = os.path.relpath(view_path, layout_dir)
|
||||
foreground.attrib["x"] = str(DEFAULT_POSITION.x)
|
||||
foreground.attrib["y"] = str(DEFAULT_POSITION.y)
|
||||
foreground.attrib["width"] = str(view_width)
|
||||
foreground.attrib["height"] = str(view_height)
|
||||
|
||||
self.add_view_title(30, view_height + 35, view)
|
||||
sheet_tree.write(sheet_path)
|
||||
self.add_view_title(
|
||||
DEFAULT_POSITION.x, view_height + DEFAULT_POSITION.y + VIEW_TITLE_OFFSET_Y, view, layout_dir
|
||||
)
|
||||
layout_tree.write(layout_path)
|
||||
|
||||
def add_view_title(self, x, y, parent):
|
||||
title_tree = ET.parse(os.path.join(self.data_dir, "templates", "view-title.svg"))
|
||||
def add_view_title(self, x, y, parent, layout_dir):
|
||||
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")
|
||||
shutil.copy(ootb_title, title_path)
|
||||
|
||||
title_tree = ET.parse(title_path)
|
||||
title_root = title_tree.getroot()
|
||||
title = ET.SubElement(parent, "image")
|
||||
title.attrib["data-type"] = "view-title"
|
||||
title.attrib["xlink:href"] = "../templates/view-title.svg"
|
||||
title.attrib["xlink:href"] = os.path.relpath(title_path, layout_dir)
|
||||
title.attrib["x"] = str(x)
|
||||
title.attrib["y"] = str(y)
|
||||
title.attrib["width"] = str(self.convert_to_mm(title_root.attrib.get("width")))
|
||||
title.attrib["height"] = str(self.convert_to_mm(title_root.attrib.get("height")))
|
||||
|
||||
def build(self, sheet):
|
||||
sheet_name = os.path.splitext(os.path.basename(tool.Drawing.get_document_uri(sheet)))[0]
|
||||
os.makedirs(os.path.join(self.data_dir, "build", sheet_name), exist_ok=True)
|
||||
self.references = {"SHEET": None, "RASTER": []}
|
||||
|
||||
sheet_path = os.path.join(self.data_dir, "sheets", f"{sheet_name}.svg")
|
||||
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
|
||||
self.layout_dir = os.path.dirname(layout_path)
|
||||
|
||||
sheet_path = tool.Ifc.resolve_uri(tool.Drawing.get_default_sheet_path(sheet[0], sheet.Name))
|
||||
self.sheets_dir = os.path.dirname(sheet_path)
|
||||
|
||||
os.makedirs(self.sheets_dir, exist_ok=True)
|
||||
|
||||
ET.register_namespace("", "http://www.w3.org/2000/svg")
|
||||
ET.register_namespace("xlink", "http://www.w3.org/1999/xlink")
|
||||
|
||||
tree = ET.parse(sheet_path)
|
||||
tree = ET.parse(layout_path)
|
||||
root = tree.getroot()
|
||||
|
||||
self.defs = ET.Element("defs")
|
||||
@@ -189,11 +258,15 @@ class SheetBuilder:
|
||||
|
||||
self.build_titleblock(root, sheet)
|
||||
self.build_drawings(root, sheet)
|
||||
self.build_schedules(root)
|
||||
self.build_schedules(root, sheet)
|
||||
|
||||
with open(os.path.join(self.data_dir, "build", sheet_name, f"{sheet_name}.svg"), "wb") as output:
|
||||
with open(sheet_path, "wb") as output:
|
||||
tree.write(output)
|
||||
|
||||
self.references["SHEET"] = sheet_path
|
||||
|
||||
return self.references
|
||||
|
||||
def build_titleblock(self, root, sheet):
|
||||
titleblock = root.findall('{http://www.w3.org/2000/svg}g[@data-type="titleblock"]')[0]
|
||||
image = titleblock.findall("{http://www.w3.org/2000/svg}image")[0]
|
||||
@@ -208,8 +281,6 @@ class SheetBuilder:
|
||||
titleblock.remove(image)
|
||||
|
||||
def build_drawings(self, root, sheet):
|
||||
sheet_name = os.path.splitext(os.path.basename(tool.Drawing.get_document_uri(sheet)))[0]
|
||||
|
||||
for view in root.findall('{http://www.w3.org/2000/svg}g[@data-type="drawing"]'):
|
||||
reference = tool.Ifc.get().by_id(int(view.attrib["data-id"]))
|
||||
drawing = tool.Ifc.get().by_id(view.attrib["data-drawing"])
|
||||
@@ -232,12 +303,15 @@ class SheetBuilder:
|
||||
view.append(self.parse_embedded_svg(foreground, {}))
|
||||
|
||||
if background is not None:
|
||||
background_path = os.path.join(self.data_dir, "sheets", self.get_href(background))
|
||||
copy(background_path, os.path.join(self.data_dir, "build", sheet_name))
|
||||
background_path = os.path.join(self.layout_dir, self.get_href(background))
|
||||
raster_path = os.path.join(self.sheets_dir, os.path.basename(background_path))
|
||||
shutil.copy(background_path, raster_path)
|
||||
self.references["RASTER"].append(raster_path)
|
||||
|
||||
if view_title is not None:
|
||||
foreground_path = self.get_href(foreground)
|
||||
data = reference.get_info()
|
||||
data.update({"Sheet" + k: v for k, v in sheet.get_info().items()})
|
||||
if not data["Name"]:
|
||||
data["Name"] = ntpath.basename(foreground_path)[0:-4]
|
||||
data["Scale"] = tool.Drawing.get_drawing_human_scale(drawing)
|
||||
@@ -246,7 +320,7 @@ class SheetBuilder:
|
||||
for image in images:
|
||||
view.remove(image)
|
||||
|
||||
def build_schedules(self, root):
|
||||
def build_schedules(self, root, sheet):
|
||||
for view in root.findall('{http://www.w3.org/2000/svg}g[@data-type="schedule"]'):
|
||||
reference = tool.Ifc.get().by_id(int(view.attrib["data-id"]))
|
||||
schedule = tool.Ifc.get().by_id(int(view.attrib["data-schedule"]))
|
||||
@@ -268,6 +342,7 @@ class SheetBuilder:
|
||||
if view_title is not None:
|
||||
path = self.get_href(table)
|
||||
data = reference.get_info()
|
||||
data.update({"Sheet" + k: v for k, v in sheet.get_info().items()})
|
||||
if not data["Name"]:
|
||||
data["Name"] = schedule.Name or "Unnamed"
|
||||
view.append(self.parse_embedded_svg(view_title, data))
|
||||
@@ -298,7 +373,7 @@ class SheetBuilder:
|
||||
self.defs.append(clip_path)
|
||||
|
||||
svg_path = self.get_href(image)
|
||||
with open(os.path.join(self.data_dir, "sheets", svg_path), "r") as template:
|
||||
with open(os.path.join(self.layout_dir, svg_path), "r") as template:
|
||||
embedded = ET.fromstring(pystache.render(template.read(), data))
|
||||
# viewBox should not be nested
|
||||
embedded.attrib["viewBox"] = ""
|
||||
@@ -314,6 +389,39 @@ class SheetBuilder:
|
||||
group.append(child)
|
||||
return group
|
||||
|
||||
def change_titleblock(self, sheet, titleblock_name):
|
||||
ootb_titleblock_path = os.path.join(self.data_dir, "templates", "titleblocks", titleblock_name + ".svg")
|
||||
titleblock_path = tool.Drawing.get_default_titleblock_path(titleblock_name)
|
||||
sheet_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
|
||||
sheet_dir = os.path.dirname(sheet_path)
|
||||
|
||||
os.makedirs(sheet_dir, exist_ok=True)
|
||||
os.makedirs(os.path.dirname(titleblock_path), exist_ok=True)
|
||||
if not os.path.exists(titleblock_path):
|
||||
shutil.copy(ootb_titleblock_path, titleblock_path)
|
||||
|
||||
ET.register_namespace("", "http://www.w3.org/2000/svg")
|
||||
ET.register_namespace("xlink", "http://www.w3.org/1999/xlink")
|
||||
|
||||
view_root = ET.parse(ootb_titleblock_path).getroot()
|
||||
view_width = self.convert_to_mm(view_root.attrib.get("width"))
|
||||
view_height = self.convert_to_mm(view_root.attrib.get("height"))
|
||||
|
||||
sheet_tree = ET.parse(sheet_path)
|
||||
root = sheet_tree.getroot()
|
||||
|
||||
titleblock = sheet_tree.findall('{http://www.w3.org/2000/svg}g[@data-type="titleblock"]')[0]
|
||||
image = titleblock.findall("{http://www.w3.org/2000/svg}image[@{http://www.w3.org/1999/xlink}href]")[0]
|
||||
image.attrib["{http://www.w3.org/1999/xlink}href"] = os.path.relpath(titleblock_path, sheet_dir)
|
||||
image.attrib["width"] = str(view_width)
|
||||
image.attrib["height"] = str(view_height)
|
||||
|
||||
root.attrib["width"] = "{}mm".format(view_width)
|
||||
root.attrib["height"] = "{}mm".format(view_height)
|
||||
root.attrib["viewBox"] = "0 0 {} {}".format(view_width, view_height)
|
||||
|
||||
sheet_tree.write(sheet_path)
|
||||
|
||||
def convert_to_mm(self, value):
|
||||
# CSS is what defines these possibilities
|
||||
# https://www.w3.org/TR/SVG/refs.html#ref-css-values-3
|
||||
|
||||
@@ -21,6 +21,7 @@ import re
|
||||
import bpy
|
||||
import math
|
||||
import bmesh
|
||||
import shutil
|
||||
import pystache
|
||||
import mathutils
|
||||
import xml.etree.ElementTree as ET
|
||||
@@ -65,10 +66,9 @@ class SvgWriter:
|
||||
self.scale = 1 / 100 # 1:100
|
||||
self.camera_width = None
|
||||
self.camera_height = None
|
||||
self.related_paths = None
|
||||
self.resource_paths = {}
|
||||
|
||||
def create_blank_svg(self, output_path):
|
||||
self.define_related_paths() # making sure all paths are defined
|
||||
self.calculate_scale()
|
||||
self.svg = svgwrite.Drawing(
|
||||
output_path,
|
||||
@@ -85,35 +85,24 @@ class SvgWriter:
|
||||
self.svg.save(pretty=True)
|
||||
|
||||
def draw_underlay(self, image):
|
||||
self.svg.add(
|
||||
self.svg.image(
|
||||
os.path.join("..", "diagrams", os.path.basename(image)),
|
||||
width=self.width,
|
||||
height=self.height,
|
||||
)
|
||||
)
|
||||
self.svg.add(self.svg.image(os.path.basename(image), width=self.width, height=self.height))
|
||||
return self
|
||||
|
||||
def define_related_paths(self, **related_paths):
|
||||
if not self.related_paths:
|
||||
self.related_paths = {}
|
||||
|
||||
if not related_paths:
|
||||
related_paths = {
|
||||
"Stylesheet": os.path.join(self.data_dir, "styles", f"default.css"),
|
||||
"Markers": os.path.join(self.data_dir, "templates", "markers.svg"),
|
||||
"Symbols": os.path.join(self.data_dir, "templates", "symbols.svg"),
|
||||
"Patterns": os.path.join(self.data_dir, "templates", "patterns.svg"),
|
||||
}
|
||||
for path_name in list(related_paths.keys()):
|
||||
if path_name in self.related_paths:
|
||||
del related_paths[path_name]
|
||||
|
||||
for path_name in related_paths:
|
||||
uri = related_paths[path_name]
|
||||
custom_path = tool.Ifc.resolve_uri(uri)
|
||||
if custom_path:
|
||||
self.related_paths[path_name] = custom_path
|
||||
def setup_drawing_resource_paths(self, element):
|
||||
pset = ifcopenshell.util.element.get_pset(element, "EPset_Drawing")
|
||||
for resource in ("Stylesheet", "Markers", "Symbols", "Patterns"):
|
||||
resource_path = pset.get(resource)
|
||||
if not resource_path:
|
||||
self.resource_paths[resource] = None
|
||||
continue
|
||||
resource_path = tool.Ifc.resolve_uri(resource_path)
|
||||
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)
|
||||
if os.path.exists(ootb_resource):
|
||||
shutil.copy(ootb_resource, resource_path)
|
||||
self.resource_paths[resource] = resource_path
|
||||
|
||||
def define_boilerplate(self):
|
||||
self.add_stylesheet()
|
||||
@@ -132,33 +121,43 @@ class SvgWriter:
|
||||
self.height = self.raw_height * self.svg_scale
|
||||
|
||||
def add_stylesheet(self):
|
||||
with open(self.related_paths["Stylesheet"], "r") as stylesheet:
|
||||
if not self.resource_paths["Stylesheet"] or not os.path.exists(self.resource_paths["Stylesheet"]):
|
||||
return
|
||||
with open(self.resource_paths["Stylesheet"], "r") as stylesheet:
|
||||
self.svg.defs.add(self.svg.style(stylesheet.read()))
|
||||
|
||||
def add_markers(self):
|
||||
tree = ET.parse(self.related_paths["Markers"])
|
||||
if not self.resource_paths["Markers"] or not os.path.exists(self.resource_paths["Markers"]):
|
||||
return
|
||||
tree = ET.parse(self.resource_paths["Markers"])
|
||||
root = tree.getroot()
|
||||
for child in root:
|
||||
self.svg.defs.add(External(child))
|
||||
|
||||
def add_symbols(self):
|
||||
tree = ET.parse(self.related_paths["Symbols"])
|
||||
if not self.resource_paths["Symbols"] or not os.path.exists(self.resource_paths["Symbols"]):
|
||||
return
|
||||
tree = ET.parse(self.resource_paths["Symbols"])
|
||||
root = tree.getroot()
|
||||
for child in root:
|
||||
self.svg.defs.add(External(child))
|
||||
|
||||
def find_xml_symbol_by_id(self, id):
|
||||
tree = ET.parse(self.related_paths["Symbols"])
|
||||
tree = ET.parse(self.resource_paths["Symbols"])
|
||||
xml_symbol = tree.find(f'.//*[@id="{id}"]')
|
||||
return External(xml_symbol) if xml_symbol else None
|
||||
|
||||
def add_patterns(self):
|
||||
tree = ET.parse(self.related_paths["Patterns"])
|
||||
if not self.resource_paths["Patterns"] or not os.path.exists(self.resource_paths["Patterns"]):
|
||||
return
|
||||
tree = ET.parse(self.resource_paths["Patterns"])
|
||||
root = tree.getroot()
|
||||
for child in root:
|
||||
self.svg.defs.add(External(child))
|
||||
|
||||
def draw_annotations(self, annotations):
|
||||
def draw_annotations(self, annotations, precision, decimal_places):
|
||||
self.precision = precision
|
||||
self.decimal_places = decimal_places
|
||||
for element in annotations:
|
||||
obj = tool.Ifc.get_object(element)
|
||||
if not obj or element.ObjectType == "DRAWING":
|
||||
@@ -226,21 +225,24 @@ class SvgWriter:
|
||||
)
|
||||
)
|
||||
# TODO: allow metric to be configurable
|
||||
rl = (matrix_world @ points[0].co.xyz).z
|
||||
rl_value = (matrix_world @ points[0].co.xyz).z
|
||||
if bpy.context.scene.unit_settings.system == "IMPERIAL":
|
||||
rl = helper.format_distance(rl)
|
||||
rl = helper.format_distance(rl_value, precision=self.precision, decimal_places=self.decimal_places)
|
||||
else:
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
rl /= unit_scale
|
||||
rl = rl_value / unit_scale
|
||||
rl = ifcopenshell.util.geolocation.auto_z2e(tool.Ifc.get(), rl)
|
||||
rl *= unit_scale
|
||||
rl = "{:.3f}m".format(rl)
|
||||
text_style = {
|
||||
"text-anchor": "start",
|
||||
"alignment-baseline": "baseline",
|
||||
"dominant-baseline": "baseline",
|
||||
}
|
||||
self.svg.add(self.svg.text(f"RL +{rl}", insert=tuple(text_position), class_="SECTIONLEVEL", **text_style))
|
||||
text_style = SvgWriter.get_box_alignment_parameters("bottom-left")
|
||||
self.svg.add(
|
||||
self.svg.text(
|
||||
"RL {}{}".format("" if rl_value < 0 else "+", rl),
|
||||
insert=tuple(text_position),
|
||||
class_="SECTIONLEVEL",
|
||||
**text_style,
|
||||
)
|
||||
)
|
||||
if tag:
|
||||
self.svg.add(self.svg.text(tag, insert=(text_position[0], text_position[1] - 5), **text_style))
|
||||
|
||||
@@ -268,11 +270,7 @@ class SvgWriter:
|
||||
"UP",
|
||||
insert=tuple(text_position),
|
||||
class_="STAIR",
|
||||
**{
|
||||
"text-anchor": "middle",
|
||||
"alignment-baseline": "middle",
|
||||
"dominant-baseline": "middle",
|
||||
},
|
||||
**SvgWriter.get_box_alignment_parameters("center"),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -296,16 +294,13 @@ class SvgWriter:
|
||||
)
|
||||
line["stroke-dasharray"] = "12.5, 3, 3, 3"
|
||||
axis_tag = tool.Ifc.get_entity(obj).Name
|
||||
text_style = SvgWriter.get_box_alignment_parameters("center")
|
||||
self.svg.add(
|
||||
self.svg.text(
|
||||
axis_tag,
|
||||
insert=tuple(start * self.svg_scale),
|
||||
class_="GRID",
|
||||
**{
|
||||
"text-anchor": "middle",
|
||||
"alignment-baseline": "middle",
|
||||
"dominant-baseline": "middle",
|
||||
},
|
||||
**text_style,
|
||||
)
|
||||
)
|
||||
self.svg.add(
|
||||
@@ -313,11 +308,7 @@ class SvgWriter:
|
||||
axis_tag,
|
||||
insert=tuple(end * self.svg_scale),
|
||||
class_="GRID",
|
||||
**{
|
||||
"text-anchor": "middle",
|
||||
"alignment-baseline": "middle",
|
||||
"dominant-baseline": "middle",
|
||||
},
|
||||
**text_style,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -356,7 +347,7 @@ class SvgWriter:
|
||||
def get_attribute_classes(self, obj):
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
global_id = "GlobalId-{}".format(element.GlobalId)
|
||||
predefined_type = "PredefinedType-" + self.canonicalise_class_name(
|
||||
predefined_type = "PredefinedType-" + tool.Drawing.canonicalise_class_name(
|
||||
str(ifcopenshell.util.element.get_predefined_type(element))
|
||||
)
|
||||
classes = [global_id, element.is_a(), predefined_type]
|
||||
@@ -366,12 +357,11 @@ class SvgWriter:
|
||||
for key in self.metadata:
|
||||
value = ifcopenshell.util.selector.get_element_value(element, key)
|
||||
if value:
|
||||
classes.append(self.canonicalise_class_name(key) + "-" + self.canonicalise_class_name(str(value)))
|
||||
classes.append(
|
||||
tool.Drawing.canonicalise_class_name(key) + "-" + tool.Drawing.canonicalise_class_name(str(value))
|
||||
)
|
||||
return classes
|
||||
|
||||
def canonicalise_class_name(self, name):
|
||||
return re.sub("[^0-9a-zA-Z]+", "", name)
|
||||
|
||||
def draw_line_annotation(self, obj):
|
||||
# TODO: properly scope these offsets
|
||||
x_offset = self.raw_width / 2
|
||||
@@ -574,11 +564,7 @@ class SvgWriter:
|
||||
|
||||
reference_id, sheet_id = self.get_reference_and_sheet_id_from_annotation(tool.Ifc.get_entity(obj))
|
||||
text_position = symbol_position_svg
|
||||
text_style = {
|
||||
"text-anchor": "middle",
|
||||
"alignment-baseline": "middle",
|
||||
"dominant-baseline": "middle",
|
||||
}
|
||||
text_style = SvgWriter.get_box_alignment_parameters("center")
|
||||
self.svg.add(
|
||||
self.svg.text(
|
||||
reference_id,
|
||||
@@ -611,11 +597,7 @@ class SvgWriter:
|
||||
|
||||
reference_id, sheet_id = self.get_reference_and_sheet_id_from_annotation(tool.Ifc.get_entity(obj))
|
||||
text_position = symbol_position_svg
|
||||
text_style = {
|
||||
"text-anchor": "middle",
|
||||
"alignment-baseline": "middle",
|
||||
"dominant-baseline": "middle",
|
||||
}
|
||||
text_style = SvgWriter.get_box_alignment_parameters("center")
|
||||
self.svg.add(
|
||||
self.svg.text(
|
||||
reference_id, insert=(text_position[0], text_position[1] - 2.5), class_="ELEVATION", **text_style
|
||||
@@ -642,6 +624,44 @@ class SvgWriter:
|
||||
return (reference_id, sheet_id)
|
||||
return ("-", "-")
|
||||
|
||||
@staticmethod
|
||||
def get_box_alignment_parameters(box_alignment):
|
||||
"""Convenience method to get svg parameters for text alignment
|
||||
in a readable way.
|
||||
|
||||
Metehod expecting values like:
|
||||
|
||||
`top-left`, `top-middle`, `top-right`,
|
||||
|
||||
`middle-left`, `center`, `middle-right`,
|
||||
|
||||
`bottom-left`, `bottom-middle`, `bottom-right`
|
||||
"""
|
||||
# reference for alignment values:
|
||||
# https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/text-anchor
|
||||
# https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/alignment-baseline
|
||||
vertical_alignment = {
|
||||
"top": "hanging",
|
||||
"bottom": "baseline",
|
||||
"center": "middle",
|
||||
"middle": "middle",
|
||||
}
|
||||
alignment_baseline = vertical_alignment[next(align for align in vertical_alignment if align in box_alignment)]
|
||||
horizontal_alignment = {
|
||||
"left": "start",
|
||||
"right": "end",
|
||||
"center": "middle",
|
||||
"middle": "middle",
|
||||
}
|
||||
text_anchor = horizontal_alignment[next(align for align in horizontal_alignment if align in box_alignment)]
|
||||
|
||||
# using dominant-baseline because we plan to use <tspan> subtags
|
||||
# otherwise alignment-baseline would be sufficient
|
||||
return {
|
||||
"dominant-baseline": alignment_baseline,
|
||||
"text-anchor": text_anchor,
|
||||
}
|
||||
|
||||
def draw_text_annotation(self, text_obj, position):
|
||||
x_offset = self.raw_width / 2
|
||||
y_offset = self.raw_height / 2
|
||||
@@ -652,21 +672,28 @@ class SvgWriter:
|
||||
text_position = self.project_point_onto_camera(position)
|
||||
text_position = Vector(((x_offset + text_position.x), (y_offset - text_position.y)))
|
||||
text_position_svg = text_position * self.svg_scale
|
||||
text_position_svg_str = ", ".join(map(str, text_position_svg))
|
||||
|
||||
local_x_axis = text_obj.matrix_world.to_quaternion() @ Vector((1, 0, 0))
|
||||
projected_x_axis = self.project_point_onto_camera(position + local_x_axis)
|
||||
angle = math.degrees(
|
||||
(Vector((x_offset + projected_x_axis.x, y_offset - projected_x_axis.y)) - text_position).angle_signed(
|
||||
Vector((1, 0))
|
||||
)
|
||||
)
|
||||
def get_basis_vector(matrix, i=0):
|
||||
"""returns basis vector for i in world space, unaffected by object scale"""
|
||||
return matrix.inverted()[i].to_3d().normalized()
|
||||
|
||||
transform = "rotate({}, {}, {})".format(angle, *text_position_svg)
|
||||
classes_str = " ".join(self.get_attribute_classes(text_obj))
|
||||
text_dir_world_x_axis = get_basis_vector(text_obj.matrix_world)
|
||||
text_dir = (self.camera.matrix_world.inverted().to_quaternion() @ text_dir_world_x_axis).to_2d().normalized()
|
||||
angle = math.degrees(-text_dir.angle_signed(Vector((1, 0))))
|
||||
|
||||
classes = self.get_attribute_classes(text_obj)
|
||||
classes_str = " ".join(classes)
|
||||
|
||||
symbol = tool.Drawing.get_annotation_symbol(element)
|
||||
template_text_fields = []
|
||||
if symbol:
|
||||
if not symbol:
|
||||
text_transform = f"translate({text_position_svg_str}) rotate({angle})"
|
||||
else:
|
||||
# NOTE: for now we assume that scale is uniform
|
||||
symbol_transform = f"translate({text_position_svg_str}) rotate({angle}) scale({text_obj.scale.x})"
|
||||
text_transform = symbol_transform
|
||||
|
||||
symbol_svg = self.find_xml_symbol_by_id(symbol)
|
||||
if symbol_svg:
|
||||
symbol_xml = symbol_svg.get_xml()
|
||||
@@ -674,7 +701,7 @@ class SvgWriter:
|
||||
# if there is a symbol with template text fields
|
||||
# then we just populate it's fields with the data from text literals
|
||||
if template_text_fields:
|
||||
symbol_xml.attrib["transform"] = f"translate({', '.join(map(str, text_position_svg))})"
|
||||
symbol_xml.attrib["transform"] = symbol_transform
|
||||
symbol_xml.attrib.pop("id")
|
||||
# note: zip makes sure that we iterate over the shortest list
|
||||
for field, text_literal in zip(template_text_fields, text_literals):
|
||||
@@ -684,35 +711,7 @@ class SvgWriter:
|
||||
return None
|
||||
|
||||
if not symbol_svg or not template_text_fields:
|
||||
self.svg.add(self.svg.use(f"#{symbol}", insert=text_position_svg))
|
||||
|
||||
def get_box_alignment_parameters(box_alignment):
|
||||
# reference for alignment values:
|
||||
# https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/text-anchor
|
||||
# https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/alignment-baseline
|
||||
vertical_alignment = {
|
||||
"top": "hanging",
|
||||
"bottom": "baseline",
|
||||
"center": "middle",
|
||||
"middle": "middle",
|
||||
}
|
||||
alignment_baseline = vertical_alignment[
|
||||
next(align for align in vertical_alignment if align in box_alignment)
|
||||
]
|
||||
horizontal_alignment = {
|
||||
"left": "start",
|
||||
"right": "end",
|
||||
"center": "middle",
|
||||
"middle": "middle",
|
||||
}
|
||||
text_anchor = horizontal_alignment[next(align for align in horizontal_alignment if align in box_alignment)]
|
||||
|
||||
# using dominant-baseline because we plan to use <tspan> subtags
|
||||
# otherwise alignment-baseline would be sufficient
|
||||
return {
|
||||
"dominant-baseline": alignment_baseline,
|
||||
"text-anchor": text_anchor,
|
||||
}
|
||||
self.svg.add(self.svg.use(f"#{symbol}", transform=symbol_transform))
|
||||
|
||||
for text_literal in text_literals:
|
||||
# after pretty indentation some redundant spaces can occur in svg tags
|
||||
@@ -721,22 +720,33 @@ class SvgWriter:
|
||||
# ref: https://github.com/IfcOpenShell/IfcOpenShell/issues/2833#issuecomment-1471584960
|
||||
|
||||
text = tool.Drawing.replace_text_literal_variables(text_literal.Literal, product)
|
||||
text_tag = self.svg.text(
|
||||
"",
|
||||
**{
|
||||
"transform": transform,
|
||||
"style": "font-size: 0;",
|
||||
},
|
||||
**get_box_alignment_parameters(text_literal.BoxAlignment),
|
||||
)
|
||||
self.svg.add(text_tag)
|
||||
attribs = {
|
||||
"transform": text_transform,
|
||||
"style": "font-size: 0;",
|
||||
}
|
||||
|
||||
for line_number, text_line in enumerate(text.replace("\\n", "\n").split("\n")):
|
||||
# position has to be inserted at tspan to avoid x offset between tspans
|
||||
t_span = self.svg.tspan(text_line, class_=classes_str, insert=text_position_svg)
|
||||
# doing it here and not in tspan constructor because constructor adds unnecessary spaces
|
||||
t_span.update({"dy": f"{line_number}em"})
|
||||
text_tag.add(t_span)
|
||||
def add_text_tag(add_fill_bg):
|
||||
text_tag = self.svg.text(
|
||||
"",
|
||||
**(attribs | {"filter": "url(#fill-background)"}) if add_fill_bg else attribs,
|
||||
**SvgWriter.get_box_alignment_parameters(text_literal.BoxAlignment),
|
||||
)
|
||||
self.svg.add(text_tag)
|
||||
|
||||
text_lines = text.replace("\\n", "\n").split("\n")
|
||||
|
||||
for line_number, text_line in enumerate(text_lines):
|
||||
# position has to be inserted at tspan to avoid x offset between tspans
|
||||
# note that tspan doesn't support using `transform` attribute
|
||||
# so we use (0,0) position because tspan is already offseted by text transform
|
||||
tspan = self.svg.tspan(text_line, class_=classes_str, insert=(0, 0))
|
||||
# doing it here and not in tspan constructor because constructor adds unnecessary spaces
|
||||
tspan.update({"dy": f"{line_number}em"})
|
||||
text_tag.add(tspan)
|
||||
|
||||
if "fill-bg" in classes:
|
||||
add_text_tag(True)
|
||||
add_text_tag(False)
|
||||
|
||||
def draw_break_annotations(self, obj):
|
||||
x_offset = self.raw_width / 2
|
||||
@@ -784,29 +794,24 @@ class SvgWriter:
|
||||
)
|
||||
)
|
||||
# TODO: allow metric to be configurable
|
||||
rl = ((matrix_world @ points[0].co).xyz + obj.location).z
|
||||
rl_value = (matrix_world @ points[0].co).z
|
||||
if bpy.context.scene.unit_settings.system == "IMPERIAL":
|
||||
rl = helper.format_distance(rl)
|
||||
rl = helper.format_distance(rl_value, precision=self.precision, decimal_places=self.decimal_places)
|
||||
else:
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
rl /= unit_scale
|
||||
rl = rl_value / unit_scale
|
||||
rl = ifcopenshell.util.geolocation.auto_z2e(tool.Ifc.get(), rl)
|
||||
rl *= unit_scale
|
||||
rl = "{:.3f}m".format(rl)
|
||||
if projected_points[0].x > projected_points[-1].x:
|
||||
text_anchor = "end"
|
||||
else:
|
||||
text_anchor = "start"
|
||||
|
||||
box_alignment = "bottom-left" if projected_points[0].x <= projected_points[-1].x else "bottom-right"
|
||||
text_style = SvgWriter.get_box_alignment_parameters(box_alignment)
|
||||
self.svg.add(
|
||||
self.svg.text(
|
||||
"RL +{}".format(rl),
|
||||
"RL {}{}".format("" if rl_value < 0 else "+", rl),
|
||||
insert=tuple(text_position),
|
||||
class_="PLANLEVEL",
|
||||
**{
|
||||
"text-anchor": text_anchor,
|
||||
"alignment-baseline": "baseline",
|
||||
"dominant-baseline": "baseline",
|
||||
},
|
||||
**text_style,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -922,12 +927,7 @@ class SvgWriter:
|
||||
text_offset = (text_position - center_position).xy.normalized() * 5
|
||||
text_position += text_offset
|
||||
|
||||
text_style = {
|
||||
"text-anchor": "middle",
|
||||
"alignment-baseline": "middle",
|
||||
"dominant-baseline": "middle",
|
||||
}
|
||||
|
||||
text_style = SvgWriter.get_box_alignment_parameters("center")
|
||||
angle_text = abs(round(math.degrees(angle), 3))
|
||||
if is_reflex:
|
||||
angle_text = 360 - angle_text
|
||||
@@ -988,14 +988,9 @@ class SvgWriter:
|
||||
)
|
||||
text_position += text_offset
|
||||
|
||||
text_style = {
|
||||
"text-anchor": "middle",
|
||||
"alignment-baseline": "middle",
|
||||
"dominant-baseline": "middle",
|
||||
}
|
||||
|
||||
text_style = SvgWriter.get_box_alignment_parameters("center")
|
||||
radius = (points[-1].co - points[-2].co).length
|
||||
radius = helper.format_distance(radius)
|
||||
radius = helper.format_distance(radius, precision=self.precision, decimal_places=self.decimal_places)
|
||||
tag = element.Description or f"R{radius}"
|
||||
|
||||
self.svg.add(self.svg.text(tag, insert=tuple(text_position), class_="RADIUS", **text_style))
|
||||
@@ -1051,7 +1046,7 @@ class SvgWriter:
|
||||
elif element.ObjectType == "SLOPE_FRACTION":
|
||||
if angle == 90:
|
||||
return "-"
|
||||
return f"{helper.format_distance(rise)} / {helper.format_distance(run)}"
|
||||
return f"{helper.format_distance(rise, precision=self.precision, decimal_places=self.decimal_places)} / {helper.format_distance(run, precision=self.precision, decimal_places=self.decimal_places)}"
|
||||
elif element.ObjectType == "SLOPE_PERCENT":
|
||||
if angle == 90:
|
||||
return "-"
|
||||
@@ -1066,19 +1061,16 @@ class SvgWriter:
|
||||
)
|
||||
text_position += text_offset
|
||||
|
||||
text_style = {
|
||||
"text-anchor": "middle",
|
||||
"alignment-baseline": "middle",
|
||||
"dominant-baseline": "middle",
|
||||
}
|
||||
|
||||
text_style = SvgWriter.get_box_alignment_parameters("center")
|
||||
self.svg.add(self.svg.text(tag, insert=tuple(text_position), class_="RADIUS", **text_style))
|
||||
|
||||
def draw_diameter_annotations(self, obj):
|
||||
classes = self.get_attribute_classes(obj)
|
||||
matrix_world = obj.matrix_world
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
text_override = element.Description
|
||||
dimension_text = element.Description
|
||||
dimension_data = DecoratorData.get_dimension_data(obj)
|
||||
|
||||
for spline in obj.data.splines:
|
||||
points = self.get_spline_points(spline)
|
||||
for i, p in enumerate(points):
|
||||
@@ -1087,22 +1079,39 @@ class SvgWriter:
|
||||
v0_global = matrix_world @ points[i].co.xyz
|
||||
v1_global = matrix_world @ points[i + 1].co.xyz
|
||||
self.draw_dimension_annotation(
|
||||
v0_global, v1_global, classes, text_override, text_format=lambda x: "D" + x
|
||||
v0_global,
|
||||
v1_global,
|
||||
classes,
|
||||
dimension_text,
|
||||
text_format=lambda x: "D" + x,
|
||||
show_description_only=dimension_data["show_description_only"],
|
||||
suppress_zero_inches=dimension_data["suppress_zero_inches"],
|
||||
text_prefix=dimension_data["text_prefix"],
|
||||
text_suffix=dimension_data["text_suffix"],
|
||||
)
|
||||
|
||||
def draw_dimension_annotations(self, obj):
|
||||
classes = self.get_attribute_classes(obj)
|
||||
matrix_world = obj.matrix_world
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
text_override = element.Description
|
||||
dimension_text = element.Description
|
||||
dimension_data = DecoratorData.get_dimension_data(obj)
|
||||
|
||||
for spline in obj.data.splines:
|
||||
points = self.get_spline_points(spline)
|
||||
for i, p in enumerate(points):
|
||||
if i + 1 >= len(points):
|
||||
continue
|
||||
for i in range(len(points) - 1):
|
||||
v0_global = matrix_world @ points[i].co.xyz
|
||||
v1_global = matrix_world @ points[i + 1].co.xyz
|
||||
self.draw_dimension_annotation(v0_global, v1_global, classes, text_override)
|
||||
self.draw_dimension_annotation(
|
||||
v0_global,
|
||||
v1_global,
|
||||
classes,
|
||||
dimension_text=dimension_text,
|
||||
show_description_only=dimension_data["show_description_only"],
|
||||
suppress_zero_inches=dimension_data["suppress_zero_inches"],
|
||||
text_prefix=dimension_data["text_prefix"],
|
||||
text_suffix=dimension_data["text_suffix"],
|
||||
)
|
||||
|
||||
def draw_measureit_arch_dimension_annotations(self):
|
||||
try:
|
||||
@@ -1116,46 +1125,117 @@ class SvgWriter:
|
||||
Vector(coord[0]), Vector(coord[1]), ["IfcAnnotation", "PredefinedType-DIMENSION"]
|
||||
)
|
||||
|
||||
def draw_dimension_annotation(self, v0_global, v1_global, classes, text_override=None, text_format=lambda x: x):
|
||||
x_offset = self.raw_width / 2
|
||||
y_offset = self.raw_height / 2
|
||||
def draw_dimension_annotation(
|
||||
self,
|
||||
v0_global,
|
||||
v1_global,
|
||||
classes,
|
||||
dimension_text=None,
|
||||
text_format=lambda x: x,
|
||||
show_description_only=False,
|
||||
suppress_zero_inches=False,
|
||||
text_prefix="",
|
||||
text_suffix="",
|
||||
):
|
||||
offset = Vector([self.raw_width, self.raw_height]) / 2
|
||||
v0 = self.project_point_onto_camera(v0_global)
|
||||
v1 = self.project_point_onto_camera(v1_global)
|
||||
start = Vector(((x_offset + v0.x), (y_offset - v0.y)))
|
||||
end = Vector(((x_offset + v1.x), (y_offset - v1.y)))
|
||||
start = (offset + v0.xy * Vector((1, -1))) * self.svg_scale
|
||||
end = (offset + v1.xy * Vector((1, -1))) * self.svg_scale
|
||||
mid = ((end - start) / 2) + start
|
||||
vector = end - start
|
||||
perpendicular = Vector((vector.y, -vector.x)).normalized()
|
||||
dimension = (v1_global - v0_global).length
|
||||
dimension = helper.format_distance(dimension)
|
||||
sheet_dimension = ((end * self.svg_scale) - (start * self.svg_scale)).length
|
||||
if sheet_dimension < 5: # annotation can't fit
|
||||
# offset text to right of marker
|
||||
text_position = (end * self.svg_scale) + perpendicular + (3 * vector.normalized())
|
||||
else:
|
||||
text_position = (mid * self.svg_scale) + perpendicular
|
||||
rotation = math.degrees(vector.angle_signed(Vector((1, 0))))
|
||||
line = self.svg.add(
|
||||
self.svg.line(
|
||||
start=tuple(start * self.svg_scale), end=tuple(end * self.svg_scale), class_=" ".join(classes)
|
||||
)
|
||||
dimension = helper.format_distance(
|
||||
dimension,
|
||||
precision=self.precision,
|
||||
decimal_places=self.decimal_places,
|
||||
suppress_zero_inches=suppress_zero_inches,
|
||||
)
|
||||
if text_override is not None:
|
||||
text = text_override
|
||||
else:
|
||||
text = str(dimension)
|
||||
text = text_format(text)
|
||||
self.svg.add(
|
||||
self.svg.text(
|
||||
sheet_dimension = (end - start).length
|
||||
|
||||
# if annotation can't fit offset text to the right of marker
|
||||
text_position = mid if sheet_dimension > 5 else (end + (3 * vector.normalized()))
|
||||
angle = math.degrees(vector.angle_signed(Vector((1, 0))))
|
||||
|
||||
line = self.svg.line(start=start, end=end, class_=" ".join(classes))
|
||||
self.svg.add(line)
|
||||
|
||||
if not show_description_only:
|
||||
text = f"{text_prefix}{str(dimension)}{text_suffix}"
|
||||
text_tag = self.create_text_tag(
|
||||
text,
|
||||
insert=tuple(text_position),
|
||||
class_="DIMENSION",
|
||||
**{
|
||||
"transform": "rotate({} {} {})".format(rotation, text_position.x, text_position.y),
|
||||
"text-anchor": "middle",
|
||||
},
|
||||
text_position + perpendicular,
|
||||
angle,
|
||||
"bottom-middle",
|
||||
"DIMENSION",
|
||||
text_format=text_format,
|
||||
multiline=True,
|
||||
multiline_to_bottom=False,
|
||||
)
|
||||
)
|
||||
self.svg.add(text_tag)
|
||||
if dimension_text:
|
||||
text_tag = self.create_text_tag(
|
||||
dimension_text,
|
||||
text_position - perpendicular,
|
||||
angle,
|
||||
"top-middle",
|
||||
"DIMENSION",
|
||||
text_format=text_format,
|
||||
multiline=True,
|
||||
multiline_to_bottom=True,
|
||||
)
|
||||
self.svg.add(text_tag)
|
||||
|
||||
elif show_description_only and dimension_text:
|
||||
text_tag = self.create_text_tag(
|
||||
dimension_text,
|
||||
text_position + perpendicular,
|
||||
angle,
|
||||
"bottom-middle",
|
||||
"DIMENSION",
|
||||
text_format=text_format,
|
||||
multiline=True,
|
||||
multiline_to_bottom=False,
|
||||
)
|
||||
self.svg.add(text_tag)
|
||||
|
||||
def create_text_tag(
|
||||
self,
|
||||
text,
|
||||
text_position,
|
||||
angle,
|
||||
box_alignment,
|
||||
class_str,
|
||||
text_format=lambda x: x,
|
||||
multiline=False,
|
||||
multiline_to_bottom=False,
|
||||
):
|
||||
if not multiline:
|
||||
text_kwargs = {"transform": "rotate({} {} {})".format(angle, text_position.x, text_position.y)}
|
||||
return self.svg.text(
|
||||
text_format(text),
|
||||
insert=text_position,
|
||||
class_=class_str,
|
||||
**(text_kwargs | SvgWriter.get_box_alignment_parameters(box_alignment)),
|
||||
)
|
||||
|
||||
text_position_svg_str = ", ".join(map(str, text_position))
|
||||
text_transform = f"translate({text_position_svg_str}) rotate({angle})"
|
||||
text_kwargs = {
|
||||
"transform": text_transform,
|
||||
"style": "font-size: 0;",
|
||||
}
|
||||
|
||||
text_tag = self.svg.text("", **text_kwargs, **SvgWriter.get_box_alignment_parameters(box_alignment))
|
||||
text_lines = text.replace("\\n", "\n").split("\n")
|
||||
text_lines = text_lines if multiline_to_bottom else text_lines[::-1]
|
||||
|
||||
for line_number, text_line in enumerate(text_lines):
|
||||
tspan = self.svg.tspan(text_format(text_line), class_=class_str, insert=(0, 0))
|
||||
tspan.update({"dy": f"{line_number if multiline_to_bottom else -line_number}em"})
|
||||
text_tag.add(tspan)
|
||||
return text_tag
|
||||
|
||||
def project_point_onto_camera(self, point):
|
||||
# TODO is this needlessly complex?
|
||||
|
||||
@@ -62,6 +62,11 @@ class BIM_PT_camera(Panel):
|
||||
row.prop(props, "has_annotation", icon="MOD_EDGESPLIT")
|
||||
row.prop(dprops, "should_use_annotation_cache", text="", icon="FILE_REFRESH")
|
||||
|
||||
row = layout.row()
|
||||
row.prop(props, "calculate_shapely_surfaces")
|
||||
row = layout.row()
|
||||
row.prop(props, "calculate_svgfill_surfaces")
|
||||
|
||||
row = layout.row()
|
||||
row.prop(dprops, "should_extract")
|
||||
|
||||
@@ -79,12 +84,18 @@ class BIM_PT_camera(Panel):
|
||||
row = layout.row()
|
||||
row.prop(props, "diagram_scale")
|
||||
if props.diagram_scale == "CUSTOM":
|
||||
row = layout.row()
|
||||
row.prop(props, "custom_diagram_scale")
|
||||
row = layout.row(align=True)
|
||||
row.prop(props, "custom_diagram_scale_input1", text="Custom Scale")
|
||||
if context.scene.unit_settings.system == "IMPERIAL":
|
||||
separator = " ="
|
||||
else:
|
||||
separator = " :"
|
||||
row.label(text=separator)
|
||||
row.prop(props, "custom_diagram_scale_input2", text="")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.create_drawing", text="Create Drawing", icon="OUTPUT")
|
||||
op = row.operator("bim.open_view", icon="URL", text="")
|
||||
op = row.operator("bim.open_drawing", icon="URL", text="")
|
||||
op.view = context.active_object.name.split("/")[1]
|
||||
|
||||
|
||||
@@ -106,41 +117,53 @@ class BIM_PT_drawing_underlay(Panel):
|
||||
layout.use_property_split = True
|
||||
dprops = context.scene.DocProperties
|
||||
props = context.active_object.data.BIMCameraProperties
|
||||
drawing_index_is_valid = props.active_drawing_style_index < len(dprops.drawing_styles)
|
||||
|
||||
if not DrawingsData.is_loaded:
|
||||
DrawingsData.load()
|
||||
drawing_pset_data = DrawingsData.data["active_drawing_pset_data"]
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.add_drawing_style")
|
||||
current_shading_style = drawing_pset_data.get("CurrentShadingStyle", None)
|
||||
if current_shading_style is None:
|
||||
row.label(text="Current style is not set.")
|
||||
else:
|
||||
row.label(text="Current Shading Style:")
|
||||
row.label(text=current_shading_style)
|
||||
row.operator("bim.add_drawing_style", icon="ADD", text="")
|
||||
if drawing_index_is_valid:
|
||||
row.operator("bim.remove_drawing_style", icon="X", text="").index = props.active_drawing_style_index
|
||||
row.operator("bim.reload_drawing_styles", icon="FILE_REFRESH", text="")
|
||||
|
||||
if dprops.drawing_styles:
|
||||
layout.template_list("BIM_UL_generic", "", dprops, "drawing_styles", props, "active_drawing_style_index")
|
||||
if not dprops.drawing_styles:
|
||||
return
|
||||
layout.template_list("BIM_UL_generic", "", dprops, "drawing_styles", props, "active_drawing_style_index")
|
||||
|
||||
if props.active_drawing_style_index < len(dprops.drawing_styles):
|
||||
drawing_style = dprops.drawing_styles[props.active_drawing_style_index]
|
||||
if not drawing_index_is_valid:
|
||||
return
|
||||
drawing_style = dprops.drawing_styles[props.active_drawing_style_index]
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(drawing_style, "name")
|
||||
row.operator("bim.remove_drawing_style", icon="X", text="").index = props.active_drawing_style_index
|
||||
row = layout.row(align=True)
|
||||
row.prop(drawing_style, "name")
|
||||
|
||||
row = layout.row()
|
||||
row.prop(drawing_style, "render_type")
|
||||
row = layout.row(align=True)
|
||||
row.prop(drawing_style, "vector_style")
|
||||
row.operator("bim.edit_vector_style", text="", icon="GREASEPENCIL")
|
||||
row = layout.row(align=True)
|
||||
row.prop(drawing_style, "include_query")
|
||||
row = layout.row(align=True)
|
||||
row.prop(drawing_style, "exclude_query")
|
||||
row = layout.row()
|
||||
row.prop(drawing_style, "render_type")
|
||||
row = layout.row(align=True)
|
||||
row.prop(drawing_style, "include_query")
|
||||
row = layout.row(align=True)
|
||||
row.prop(drawing_style, "exclude_query")
|
||||
|
||||
row = layout.row()
|
||||
row.operator("bim.add_drawing_style_attribute")
|
||||
row = layout.row()
|
||||
row.operator("bim.add_drawing_style_attribute")
|
||||
|
||||
for index, attribute in enumerate(drawing_style.attributes):
|
||||
row = layout.row(align=True)
|
||||
row.prop(attribute, "name", text="")
|
||||
row.operator("bim.remove_drawing_style_attribute", icon="X", text="").index = index
|
||||
for index, attribute in enumerate(drawing_style.attributes):
|
||||
row = layout.row(align=True)
|
||||
row.prop(attribute, "name", text="")
|
||||
row.operator("bim.remove_drawing_style_attribute", icon="X", text="").index = index
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.save_drawing_style")
|
||||
row.operator("bim.activate_drawing_style")
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.save_drawing_style")
|
||||
row.operator("bim.activate_drawing_style")
|
||||
|
||||
|
||||
class BIM_PT_drawings(Panel):
|
||||
@@ -158,6 +181,14 @@ class BIM_PT_drawings(Panel):
|
||||
if not DrawingsData.is_loaded:
|
||||
DrawingsData.load()
|
||||
|
||||
if not DrawingsData.data["has_saved_ifc"]:
|
||||
row = self.layout.row()
|
||||
row.label(text="Project Not Yet Saved", icon="ERROR")
|
||||
row = self.layout.row()
|
||||
op = row.operator("export_ifc.bim", icon="EXPORT", text="Save Project")
|
||||
op.should_save_as = False
|
||||
return
|
||||
|
||||
self.props = context.scene.DocProperties
|
||||
|
||||
if not self.props.is_editing_drawings:
|
||||
@@ -185,9 +216,11 @@ class BIM_PT_drawings(Panel):
|
||||
).drawing = active_drawing.ifc_definition_id
|
||||
col = row.column()
|
||||
col.alignment = "RIGHT"
|
||||
op = row.operator("bim.open_view", icon="URL", text="")
|
||||
op = row.operator("bim.select_all_drawings", icon="SELECT_SUBTRACT", text="")
|
||||
op = row.operator("bim.open_drawing", icon="URL", text="")
|
||||
op.view = active_drawing.name
|
||||
op = row.operator("bim.activate_view", icon="OUTLINER_OB_CAMERA", text="")
|
||||
row.operator("bim.activate_model", icon="VIEW3D", text="")
|
||||
op = row.operator("bim.activate_drawing", icon="OUTLINER_OB_CAMERA", text="")
|
||||
op.drawing = active_drawing.ifc_definition_id
|
||||
row.operator("bim.create_drawing", text="", icon="OUTPUT")
|
||||
self.layout.template_list(
|
||||
@@ -220,6 +253,14 @@ class BIM_PT_schedules(Panel):
|
||||
if not SchedulesData.is_loaded:
|
||||
SchedulesData.load()
|
||||
|
||||
if not SchedulesData.data["has_saved_ifc"]:
|
||||
row = self.layout.row()
|
||||
row.label(text="Project Not Yet Saved", icon="ERROR")
|
||||
row = self.layout.row()
|
||||
op = row.operator("export_ifc.bim", icon="EXPORT", text="Save Project")
|
||||
op.should_save_as = False
|
||||
return
|
||||
|
||||
self.props = context.scene.DocProperties
|
||||
|
||||
if not self.props.is_editing_schedules:
|
||||
@@ -263,6 +304,14 @@ class BIM_PT_sheets(Panel):
|
||||
if not SheetsData.is_loaded:
|
||||
SheetsData.load()
|
||||
|
||||
if not SheetsData.data["has_saved_ifc"]:
|
||||
row = self.layout.row()
|
||||
row.label(text="Project Not Yet Saved", icon="ERROR")
|
||||
row = self.layout.row()
|
||||
op = row.operator("export_ifc.bim", icon="EXPORT", text="Save Project")
|
||||
op.should_save_as = False
|
||||
return
|
||||
|
||||
self.props = context.scene.DocProperties
|
||||
|
||||
if not self.props.is_editing_sheets:
|
||||
@@ -280,6 +329,7 @@ class BIM_PT_sheets(Panel):
|
||||
active_sheet = self.props.sheets[self.props.active_sheet_index]
|
||||
row = self.layout.row(align=True)
|
||||
row.alignment = "RIGHT"
|
||||
row.operator("bim.edit_sheet", icon="GREASEPENCIL", text="")
|
||||
row.operator("bim.open_sheet", icon="URL", text="")
|
||||
row.operator("bim.add_drawing_to_sheet", icon="IMAGE_PLANE", text="")
|
||||
row.operator("bim.add_schedule_to_sheet", icon="PRESET_NEW", text="")
|
||||
@@ -343,7 +393,7 @@ class BIM_PT_text(Panel):
|
||||
element = tool.Ifc.get_entity(context.active_object)
|
||||
if not element:
|
||||
return
|
||||
return element.is_a("IfcAnnotation") and element.ObjectType in ["TEXT", "TEXT_LEADER"]
|
||||
return tool.Drawing.is_annotation_object_type(element, ["TEXT", "TEXT_LEADER"])
|
||||
|
||||
def draw(self, context):
|
||||
obj = context.active_object
|
||||
@@ -470,6 +520,7 @@ class BIM_PT_annotation_utilities(Panel):
|
||||
op = row.operator("bim.add_annotation", text="Batting", icon="FORCE_FORCE")
|
||||
op.object_type = "BATTING"
|
||||
op.data_type = "mesh"
|
||||
op.description = "Add batting annotation.\nThickness could be changed through Thickness property of BBIM_Batting property set"
|
||||
op = row.operator("bim.add_annotation", text="Fill Area", icon="NODE_TEXTURE")
|
||||
op.object_type = "FILL_AREA"
|
||||
|
||||
@@ -487,6 +538,8 @@ class BIM_UL_drawinglist(bpy.types.UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
if item:
|
||||
row = layout.row(align=True)
|
||||
selected_icon = "CHECKBOX_HLT" if item.is_selected else "CHECKBOX_DEHLT"
|
||||
row.prop(item, "is_selected", text="", icon=selected_icon)
|
||||
icon = "UV_FACESEL"
|
||||
if item.target_view == "ELEVATION_VIEW":
|
||||
icon = "UV_VERTEXSEL"
|
||||
@@ -503,26 +556,35 @@ class BIM_UL_drawinglist(bpy.types.UIList):
|
||||
|
||||
class BIM_UL_sheets(bpy.types.UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
if item:
|
||||
row = layout.row(align=True)
|
||||
|
||||
if item.is_sheet:
|
||||
if item.is_expanded:
|
||||
row.operator(
|
||||
"bim.contract_sheet", text="", emboss=False, icon="DISCLOSURE_TRI_DOWN"
|
||||
).sheet = item.ifc_definition_id
|
||||
else:
|
||||
row.operator(
|
||||
"bim.expand_sheet", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT"
|
||||
).sheet = item.ifc_definition_id
|
||||
else:
|
||||
row.label(text="", icon="BLANK1")
|
||||
if item.reference_type == "DRAWING":
|
||||
row.label(text="", icon="IMAGE_DATA")
|
||||
elif item.reference_type == "SCHEDULE":
|
||||
row.label(text="", icon="LONGDISPLAY")
|
||||
|
||||
name = "{} - {}".format(item.identification or "X", item.name or "Unnamed")
|
||||
row.label(text=name)
|
||||
else:
|
||||
if not item:
|
||||
layout.label(text="", translate=False)
|
||||
return
|
||||
|
||||
row = layout.row(align=True)
|
||||
if item.is_sheet:
|
||||
if item.is_expanded:
|
||||
row.operator(
|
||||
"bim.contract_sheet", text="", emboss=False, icon="DISCLOSURE_TRI_DOWN"
|
||||
).sheet = item.ifc_definition_id
|
||||
else:
|
||||
row.operator(
|
||||
"bim.expand_sheet", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT"
|
||||
).sheet = item.ifc_definition_id
|
||||
|
||||
row.label(text=f"{item.identification} - {item.name}")
|
||||
else:
|
||||
row.label(text="", icon="BLANK1")
|
||||
if item.reference_type == "DRAWING":
|
||||
row.label(text="", icon="IMAGE_DATA")
|
||||
elif item.reference_type == "SCHEDULE":
|
||||
row.label(text="", icon="LONGDISPLAY")
|
||||
elif item.reference_type == "TITLEBLOCK":
|
||||
row.label(text="", icon="MENU_PANEL")
|
||||
elif item.reference_type == "REVISION":
|
||||
row.label(text="", icon="RECOVER_LAST")
|
||||
|
||||
if item.identification:
|
||||
name = f"{item.identification} - {item.name or 'Unnamed'}"
|
||||
else:
|
||||
name = item.name or "Unnamed"
|
||||
row.label(text=name)
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2023 @Andrej730
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on 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.
|
||||
#
|
||||
# BlenderBIM Add-on 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 BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import os
|
||||
import bpy
|
||||
import blenderbim.tool as tool
|
||||
from blenderbim.bim.helper import prop_with_search
|
||||
from bpy.types import WorkSpaceTool
|
||||
|
||||
# from blenderbim.bim.module.model.data import AuthoringData, RailingData, RoofData
|
||||
from blenderbim.bim.module.drawing.prop import ANNOTATION_TYPES_DATA
|
||||
from blenderbim.bim.module.drawing.data import DecoratorData, AnnotationData
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
import blenderbim.bim.handler
|
||||
|
||||
|
||||
# declaring it here to avoid circular import problems
|
||||
class Operator:
|
||||
def execute(self, context):
|
||||
IfcStore.execute_ifc_operator(self, context)
|
||||
blenderbim.bim.handler.refresh_ui_data()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AnnotationTool(WorkSpaceTool):
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_context_mode = "OBJECT"
|
||||
|
||||
bl_idname = "bim.annotation_tool"
|
||||
bl_label = "Annotation Tool"
|
||||
bl_description = "Gives you Annotation related superpowers"
|
||||
bl_icon = os.path.join(os.path.dirname(__file__), "ops.authoring.annotation")
|
||||
bl_widget = None
|
||||
# https://docs.blender.org/api/current/bpy.types.KeyMapItems.html
|
||||
bl_keymap = tool.Blender.get_default_selection_keypmap() + (
|
||||
("bim.annotation_hotkey", {"type": "A", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_A")]}),
|
||||
("bim.annotation_hotkey", {"type": "C", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_C")]}),
|
||||
("bim.annotation_hotkey", {"type": "E", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_E")]}),
|
||||
("bim.annotation_hotkey", {"type": "F", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_F")]}),
|
||||
("bim.annotation_hotkey", {"type": "G", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_G")]}),
|
||||
("bim.annotation_hotkey", {"type": "K", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_K")]}),
|
||||
("bim.annotation_hotkey", {"type": "M", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_M")]}),
|
||||
("bim.annotation_hotkey", {"type": "O", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_O")]}),
|
||||
("bim.annotation_hotkey", {"type": "Q", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_Q")]}),
|
||||
("bim.annotation_hotkey", {"type": "R", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_R")]}),
|
||||
("bim.annotation_hotkey", {"type": "T", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_T")]}),
|
||||
("bim.annotation_hotkey", {"type": "V", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_V")]}),
|
||||
("bim.annotation_hotkey", {"type": "X", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_X")]}),
|
||||
("bim.annotation_hotkey", {"type": "Y", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_Y")]}),
|
||||
("bim.annotation_hotkey", {"type": "D", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_D")]}),
|
||||
("bim.annotation_hotkey", {"type": "E", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_E")]}),
|
||||
("bim.annotation_hotkey", {"type": "O", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_O")]}),
|
||||
)
|
||||
|
||||
def draw_settings(context, layout, ws_tool):
|
||||
# Unlike operators, Blender doesn't treat workspace tools as a class, so we'll create our own.
|
||||
AnnotationToolUI.draw(context, layout)
|
||||
|
||||
|
||||
def add_layout_hotkey_operator(layout, text, hotkey, description):
|
||||
modifiers = {
|
||||
"A": "EVENT_ALT",
|
||||
"S": "EVENT_SHIFT",
|
||||
}
|
||||
modifier, key = hotkey.split("_")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon=modifiers[modifier])
|
||||
row.label(text="", icon=f"EVENT_{key}")
|
||||
|
||||
op = row.operator("bim.annotation_hotkey", text=text)
|
||||
op.hotkey = hotkey
|
||||
op.description = description
|
||||
return op, row
|
||||
|
||||
|
||||
# TODO: move to operator
|
||||
def create_annotation_type(context):
|
||||
# just empty to store parameters
|
||||
props = context.scene.BIMAnnotationProperties
|
||||
object_type = props.object_type
|
||||
create_representation = props.create_representation_for_type
|
||||
drawing = tool.Ifc.get_entity(bpy.context.scene.camera)
|
||||
|
||||
if props.create_representation_for_type:
|
||||
obj = tool.Drawing.create_annotation_object(drawing, object_type)
|
||||
else:
|
||||
obj = bpy.data.objects.new(object_type, None)
|
||||
|
||||
obj.name = f"{object_type}_TYPE"
|
||||
obj.location = context.scene.cursor.location
|
||||
tool.Drawing.ensure_annotation_in_drawing_plane(obj)
|
||||
|
||||
drawing = tool.Ifc.get_entity(context.scene.camera)
|
||||
ifc_context = tool.Drawing.get_annotation_context(tool.Drawing.get_drawing_target_view(drawing), object_type)
|
||||
|
||||
element = tool.Drawing.run_root_assign_class(
|
||||
obj=obj,
|
||||
ifc_class="IfcTypeProduct",
|
||||
predefined_type=object_type,
|
||||
should_add_representation=create_representation,
|
||||
context=ifc_context,
|
||||
ifc_representation_class=tool.Drawing.get_ifc_representation_class(object_type),
|
||||
)
|
||||
element.ApplicableOccurrence = f"IfcAnnotation/{object_type}"
|
||||
|
||||
tool.Blender.select_and_activate_single_object(context, obj)
|
||||
|
||||
|
||||
# TODO: move to operator
|
||||
def create_annotation_occurence(context):
|
||||
# object_type = context.scene.BIMAnnotationProperties.object_type
|
||||
props = context.scene.BIMAnnotationProperties
|
||||
relating_type = tool.Ifc.get().by_id(int(props.relating_type_id))
|
||||
object_type = props.object_type
|
||||
|
||||
drawing = tool.Ifc.get_entity(context.scene.camera)
|
||||
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)
|
||||
element = tool.Drawing.run_root_assign_class(
|
||||
obj=obj,
|
||||
ifc_class="IfcAnnotation",
|
||||
predefined_type=object_type,
|
||||
should_add_representation=not bool(relating_type_repr),
|
||||
context=ifc_context,
|
||||
ifc_representation_class=tool.Drawing.get_ifc_representation_class(object_type),
|
||||
)
|
||||
|
||||
blenderbim.core.type.assign_type(tool.Ifc, tool.Type, element=element, type=relating_type)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def create_annotation():
|
||||
props = bpy.context.scene.BIMAnnotationProperties
|
||||
create_type_occurence = props.relating_type_id != "0"
|
||||
if create_type_occurence:
|
||||
create_annotation_occurence(bpy.context)
|
||||
else:
|
||||
object_type = props.object_type
|
||||
bpy.ops.bim.add_annotation(object_type=object_type, data_type=ANNOTATION_TYPES_DATA[object_type][-1])
|
||||
|
||||
|
||||
class AnnotationToolUI:
|
||||
@classmethod
|
||||
def draw(cls, context, layout):
|
||||
cls.layout = layout
|
||||
cls.props = context.scene.BIMAnnotationProperties
|
||||
|
||||
row = cls.layout.row(align=True)
|
||||
if not tool.Ifc.get():
|
||||
row.label(text="No IFC Project", icon="ERROR")
|
||||
return
|
||||
|
||||
if not AnnotationData.is_loaded:
|
||||
AnnotationData.load()
|
||||
|
||||
cls.draw_type_selection_interface()
|
||||
|
||||
if context.active_object and context.selected_objects:
|
||||
cls.draw_edit_object_interface(context)
|
||||
cls.draw_create_object_interface()
|
||||
|
||||
@classmethod
|
||||
def draw_create_object_interface(cls):
|
||||
row = cls.layout.row(align=True)
|
||||
op, row = add_layout_hotkey_operator(cls.layout, "Add Type", "S_C", "Create a new annotation type")
|
||||
selected_icon = "CHECKBOX_HLT" if cls.props.create_representation_for_type else "CHECKBOX_DEHLT"
|
||||
row.prop(cls.props, "create_representation_for_type", text="", icon=selected_icon)
|
||||
|
||||
@classmethod
|
||||
def draw_edit_object_interface(cls, context):
|
||||
if DecoratorData.get_ifc_text_data(bpy.context.object):
|
||||
add_layout_hotkey_operator(cls.layout, "Edit Text", "S_E", "")
|
||||
|
||||
@classmethod
|
||||
def draw_header_interface(cls):
|
||||
cls.draw_type_selection_interface()
|
||||
|
||||
@classmethod
|
||||
def draw_basic_annotation_tool_interface(cls):
|
||||
cls.draw_type_selection_interface()
|
||||
|
||||
@classmethod
|
||||
def draw_type_selection_interface(cls):
|
||||
# shared by both sidebar and header
|
||||
object_type = cls.props.object_type
|
||||
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="FILE_VOLUME")
|
||||
prop_with_search(row, cls.props, "object_type", text="")
|
||||
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="FILE_3D")
|
||||
prop_with_search(row, cls.props, "relating_type_id", text="")
|
||||
|
||||
create_type_occurence = cls.props.relating_type_id != "0"
|
||||
label = "Add Type Occurence" if create_type_occurence else "Add Annotation"
|
||||
add_layout_hotkey_operator(cls.layout, label, "S_A", "Create a new annotation")
|
||||
|
||||
if object_type in ("TEXT", "STAIR_ARROW"):
|
||||
add_layout_hotkey_operator(
|
||||
cls.layout,
|
||||
"Bulk Tag",
|
||||
"S_T",
|
||||
"Create new annotations and automatically adjust them to the selected objects",
|
||||
)
|
||||
add_layout_hotkey_operator(
|
||||
cls.layout, "Readjust", "S_G", "Readjust tags based on the products they are assigned to"
|
||||
)
|
||||
|
||||
|
||||
class Hotkey(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.annotation_hotkey"
|
||||
bl_label = "Hotkey"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
hotkey: bpy.props.StringProperty()
|
||||
description: bpy.props.StringProperty()
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return tool.Ifc.get()
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, operator):
|
||||
return operator.description or ""
|
||||
|
||||
def _execute(self, context):
|
||||
self.props = context.scene.BIMAnnotationProperties
|
||||
getattr(self, f"hotkey_{self.hotkey}")()
|
||||
|
||||
def invoke(self, context, event):
|
||||
# https://blender.stackexchange.com/questions/276035/how-do-i-make-operators-remember-their-property-values-when-called-from-a-hotkey
|
||||
self.props = context.scene.BIMAnnotationProperties
|
||||
return self.execute(context)
|
||||
|
||||
def draw(self, context):
|
||||
pass
|
||||
|
||||
def hotkey_S_T(self):
|
||||
props = bpy.context.scene.BIMAnnotationProperties
|
||||
object_type = props.object_type
|
||||
|
||||
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, object_type, related_object)
|
||||
|
||||
def hotkey_S_A(self):
|
||||
create_annotation()
|
||||
|
||||
def hotkey_S_E(self):
|
||||
if not bpy.context.object:
|
||||
return
|
||||
|
||||
if DecoratorData.get_ifc_text_data(bpy.context.object):
|
||||
bpy.ops.bim.edit_text_popup()
|
||||
|
||||
def hotkey_S_G(self):
|
||||
for obj in bpy.context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not element.is_a("IfcAnnotation"):
|
||||
continue
|
||||
|
||||
related_product = tool.Drawing.get_assigned_product(element)
|
||||
if not related_product:
|
||||
continue
|
||||
related_object = tool.Ifc.get_object(related_product)
|
||||
|
||||
tool.Drawing.setup_annotation_object(obj, element.ObjectType, related_object)
|
||||
|
||||
def hotkey_S_C(self):
|
||||
create_annotation_type(bpy.context)
|
||||
@@ -27,6 +27,8 @@ classes = (
|
||||
operator.OverrideDelete,
|
||||
operator.OverrideDuplicateMove,
|
||||
operator.OverrideDuplicateMoveLinked,
|
||||
operator.OverrideDuplicateMoveLinkedMacro,
|
||||
operator.OverrideDuplicateMoveMacro,
|
||||
operator.OverrideJoin,
|
||||
operator.OverrideModeSetEdit,
|
||||
operator.OverrideModeSetObject,
|
||||
@@ -52,6 +54,11 @@ addon_keymaps = []
|
||||
|
||||
|
||||
def register():
|
||||
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")
|
||||
|
||||
bpy.types.Object.BIMGeometryProperties = bpy.props.PointerProperty(type=prop.BIMObjectGeometryProperties)
|
||||
bpy.types.Scene.BIMGeometryProperties = bpy.props.PointerProperty(type=prop.BIMGeometryProperties)
|
||||
bpy.types.OBJECT_PT_transform.append(ui.BIM_PT_transform)
|
||||
@@ -60,9 +67,8 @@ def register():
|
||||
if wm.keyconfigs.addon:
|
||||
km = wm.keyconfigs.addon.keymaps.new(name="Object Mode", space_type="EMPTY")
|
||||
kmi = km.keymap_items.new("bim.override_object_join", "J", "PRESS", ctrl=True)
|
||||
kmi = km.keymap_items.new("bim.override_object_duplicate_move", "D", "PRESS", shift=True)
|
||||
kmi.properties.is_interactive = True
|
||||
kmi = km.keymap_items.new("bim.override_object_duplicate_move_linked", "D", "PRESS", alt=True)
|
||||
kmi = km.keymap_items.new("bim.override_object_duplicate_move_macro", "D", "PRESS", shift=True)
|
||||
kmi = km.keymap_items.new("bim.override_object_duplicate_move_linked_macro", "D", "PRESS", alt=True)
|
||||
kmi = km.keymap_items.new("bim.override_paste_buffer", "V", "PRESS", ctrl=True)
|
||||
kmi = km.keymap_items.new("bim.override_mode_set_edit", "TAB", "PRESS")
|
||||
kmi = km.keymap_items.new("bim.override_object_delete", "X", "PRESS")
|
||||
@@ -72,6 +78,9 @@ def register():
|
||||
km = wm.keyconfigs.addon.keymaps.new(name="Mesh", space_type="EMPTY")
|
||||
kmi = km.keymap_items.new("bim.override_mode_set_object", "TAB", "PRESS")
|
||||
|
||||
km = wm.keyconfigs.addon.keymaps.new(name="Curve", space_type="EMPTY")
|
||||
kmi = km.keymap_items.new("bim.override_mode_set_object", "TAB", "PRESS")
|
||||
|
||||
km = wm.keyconfigs.addon.keymaps.new(name="Outliner", space_type="OUTLINER")
|
||||
kmi = km.keymap_items.new("bim.override_paste_buffer", "V", "PRESS", ctrl=True)
|
||||
kmi = km.keymap_items.new("bim.override_outliner_delete", "X", "PRESS")
|
||||
|
||||
@@ -38,7 +38,7 @@ class Helper:
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
|
||||
|
||||
bm.faces.ensure_lookup_table()
|
||||
face = None
|
||||
@@ -62,7 +62,7 @@ class Helper:
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
|
||||
|
||||
bm.faces.ensure_lookup_table()
|
||||
potential_faces = []
|
||||
@@ -90,7 +90,7 @@ class Helper:
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
|
||||
|
||||
bm.faces.ensure_lookup_table()
|
||||
potential_faces = []
|
||||
@@ -292,7 +292,7 @@ class Helper:
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
|
||||
|
||||
bm.faces.ensure_lookup_table()
|
||||
potential_faces = []
|
||||
@@ -389,7 +389,7 @@ class Helper:
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
|
||||
|
||||
bm.faces.ensure_lookup_table()
|
||||
faces = bm.faces
|
||||
|
||||
@@ -189,7 +189,8 @@ class UpdateRepresentation(bpy.types.Operator, Operator):
|
||||
ifcopenshell.api.run("boundary.assign_connection_geometry", tool.Ifc.get(), **settings)
|
||||
return
|
||||
|
||||
core.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
||||
if tool.Ifc.is_moved(obj):
|
||||
core.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
||||
|
||||
if material and material.is_a() in ["IfcMaterialProfileSet", "IfcMaterialLayerSet"]:
|
||||
# These objects are parametrically based on an axis and should not be modified as a mesh
|
||||
@@ -198,6 +199,13 @@ class UpdateRepresentation(bpy.types.Operator, Operator):
|
||||
old_representation = self.file.by_id(obj.data.BIMMeshProperties.ifc_definition_id)
|
||||
context_of_items = old_representation.ContextOfItems
|
||||
|
||||
# TODO: remove this code a bit later
|
||||
# added this as a fallback for easier transition some annotation types to 3d
|
||||
# if they were create before as 2d
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if tool.Drawing.is_annotation_object_type(element, ("FALL", "SECTION_LEVEL", "PLAN_LEVEL")):
|
||||
context_of_items = tool.Drawing.get_annotation_context("MODEL_VIEW")
|
||||
|
||||
gprop = context.scene.BIMGeoreferenceProperties
|
||||
coordinate_offset = None
|
||||
if gprop.has_blender_offset and obj.BIMObjectProperties.blender_offset_type == "CARTESIAN_POINT":
|
||||
@@ -250,6 +258,20 @@ class UpdateRepresentation(bpy.types.Operator, Operator):
|
||||
|
||||
obj.data.BIMMeshProperties.ifc_definition_id = int(new_representation.id())
|
||||
obj.data.name = f"{old_representation.ContextOfItems.id()}/{new_representation.id()}"
|
||||
|
||||
# TODO: In simple scenarios, a type has a ShapeRepresentation of ID
|
||||
# 123. This is then mapped through mapped representations by
|
||||
# occurrences, with no cartesian transformation. In this case, the mesh
|
||||
# data is 100% shared and therefore all have the same mesh name
|
||||
# referencing ID 123. (i.e. the local origins are shared). However, in
|
||||
# complex scenarios, occurrences may have their own cartesian
|
||||
# transformation (via MappingTarget). This will mean that occurrences
|
||||
# will not share the same mesh data and will instead reference a
|
||||
# different ShapeRepresentation ID. In this scenario, we have to
|
||||
# propagate the obj.data back to the type itself and all sibling
|
||||
# occurrences and accommodate their individual cartesian
|
||||
# transformations.
|
||||
|
||||
core.remove_representation(tool.Ifc, tool.Geometry, obj=obj, representation=old_representation)
|
||||
if obj.data.BIMMeshProperties.ifc_parameters:
|
||||
core.get_representation_ifc_parameters(tool.Geometry, obj=obj)
|
||||
@@ -345,52 +367,7 @@ class CopyRepresentation(bpy.types.Operator, Operator):
|
||||
return r.MappedRepresentation
|
||||
|
||||
|
||||
class OverrideDeleteTrait:
|
||||
def delete_ifc_object(self, obj):
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
return
|
||||
if element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
|
||||
return blenderbim.core.drawing.remove_drawing(tool.Ifc, tool.Drawing, drawing=element)
|
||||
IfcStore.delete_element(element)
|
||||
if obj.users_collection and obj.users_collection[0].name == obj.name:
|
||||
parent = ifcopenshell.util.element.get_aggregate(element)
|
||||
if not parent:
|
||||
parent = ifcopenshell.util.element.get_container(element)
|
||||
if parent:
|
||||
parent_obj = tool.Ifc.get_object(parent)
|
||||
if parent_obj:
|
||||
parent_collection = bpy.data.collections.get(parent_obj.name)
|
||||
for child in obj.users_collection[0].children:
|
||||
parent_collection.children.link(child)
|
||||
bpy.data.collections.remove(obj.users_collection[0])
|
||||
if getattr(element, "FillsVoids", None):
|
||||
self.remove_filling(element)
|
||||
if element.is_a("IfcOpeningElement"):
|
||||
if element.HasFillings:
|
||||
for rel in element.HasFillings:
|
||||
self.remove_filling(rel.RelatedBuildingElement)
|
||||
else:
|
||||
if element.VoidsElements:
|
||||
self.delete_opening_element(element)
|
||||
else:
|
||||
if getattr(element, "HasOpenings", None):
|
||||
for rel in element.HasOpenings:
|
||||
self.delete_opening_element(rel.RelatedOpeningElement)
|
||||
for port in ifcopenshell.util.system.get_ports(element):
|
||||
self.remove_port(port)
|
||||
|
||||
def delete_opening_element(self, element):
|
||||
bpy.ops.bim.remove_opening(opening_id=element.id())
|
||||
|
||||
def remove_filling(self, element):
|
||||
bpy.ops.bim.remove_filling(filling=element.id())
|
||||
|
||||
def remove_port(self, port):
|
||||
blenderbim.core.system.remove_port(tool.Ifc, tool.System, port=port)
|
||||
|
||||
|
||||
class OverrideDelete(bpy.types.Operator, OverrideDeleteTrait):
|
||||
class OverrideDelete(bpy.types.Operator):
|
||||
bl_idname = "bim.override_object_delete"
|
||||
bl_label = "IFC Delete"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
@@ -419,18 +396,16 @@ class OverrideDelete(bpy.types.Operator, OverrideDeleteTrait):
|
||||
|
||||
def _execute(self, context):
|
||||
for obj in context.selected_objects:
|
||||
self.delete_ifc_object(obj)
|
||||
try:
|
||||
obj.name
|
||||
if tool.Ifc.get_entity(obj):
|
||||
tool.Geometry.delete_ifc_object(obj)
|
||||
else:
|
||||
bpy.data.objects.remove(obj)
|
||||
except:
|
||||
pass
|
||||
# Required otherwise gizmos are still visible
|
||||
context.view_layer.objects.active = None
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class OverrideOutlinerDelete(bpy.types.Operator, OverrideDeleteTrait):
|
||||
class OverrideOutlinerDelete(bpy.types.Operator):
|
||||
bl_idname = "bim.override_outliner_delete"
|
||||
bl_label = "IFC Delete"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
@@ -481,8 +456,7 @@ class OverrideOutlinerDelete(bpy.types.Operator, OverrideDeleteTrait):
|
||||
objects_to_delete.add(bpy.data.objects.get(item.name))
|
||||
for obj in objects_to_delete:
|
||||
# This is the only difference
|
||||
self.delete_ifc_object(obj)
|
||||
bpy.data.objects.remove(obj)
|
||||
tool.Geometry.delete_ifc_object(obj)
|
||||
return {"FINISHED"}
|
||||
|
||||
def get_collection_objects_and_children(self, collection):
|
||||
@@ -498,6 +472,12 @@ class OverrideOutlinerDelete(bpy.types.Operator, OverrideDeleteTrait):
|
||||
return {"objects": objects, "children": children}
|
||||
|
||||
|
||||
class OverrideDuplicateMoveMacro(bpy.types.Macro):
|
||||
bl_idname = "bim.override_object_duplicate_move_macro"
|
||||
bl_label = "IFC Duplicate Objects"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
|
||||
class OverrideDuplicateMove(bpy.types.Operator):
|
||||
bl_idname = "bim.override_object_duplicate_move"
|
||||
bl_label = "IFC Duplicate Objects"
|
||||
@@ -529,8 +509,6 @@ class OverrideDuplicateMove(bpy.types.Operator):
|
||||
new_obj.select_set(True)
|
||||
if new_active_obj:
|
||||
context.view_layer.objects.active = new_active_obj
|
||||
if self.is_interactive:
|
||||
bpy.ops.transform.translate("INVOKE_DEFAULT")
|
||||
return {"FINISHED"}
|
||||
|
||||
def _execute(self, context):
|
||||
@@ -551,17 +529,28 @@ class OverrideDuplicateMove(bpy.types.Operator):
|
||||
# Copy the actual class
|
||||
new = blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj)
|
||||
if new:
|
||||
array_pset = ifcopenshell.util.element.get_pset(new, "BBIM_Array")
|
||||
if array_pset:
|
||||
array_pset = tool.Ifc.get().by_id(array_pset["id"])
|
||||
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=new, pset=array_pset)
|
||||
old_to_new[tool.Ifc.get_entity(obj)] = [new]
|
||||
if new.is_a("IfcRelSpaceBoundary"):
|
||||
tool.Boundary.decorate_boundary(new_obj)
|
||||
# Recreate decompositions
|
||||
tool.Root.recreate_decompositions(relationships, old_to_new)
|
||||
if self.is_interactive:
|
||||
bpy.ops.transform.translate("INVOKE_DEFAULT")
|
||||
blenderbim.bim.handler.purge_module_data()
|
||||
|
||||
|
||||
class OverrideDuplicateMoveLinkedMacro(bpy.types.Macro):
|
||||
bl_idname = "bim.override_object_duplicate_move_linked_macro"
|
||||
bl_label = "IFC Duplicate Linked"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
|
||||
class OverrideDuplicateMoveLinked(bpy.types.Operator):
|
||||
bl_idname = "bim.override_object_duplicate_move_linked"
|
||||
bl_label = "IFC Duplicate Linked"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@@ -586,7 +575,6 @@ class OverrideDuplicateMoveLinked(bpy.types.Operator):
|
||||
new_obj.select_set(True)
|
||||
if new_active_obj:
|
||||
context.view_layer.objects.active = new_active_obj
|
||||
bpy.ops.transform.translate("INVOKE_DEFAULT")
|
||||
return {"FINISHED"}
|
||||
|
||||
def _execute(self, context):
|
||||
@@ -596,6 +584,8 @@ class OverrideDuplicateMoveLinked(bpy.types.Operator):
|
||||
old_to_new = {}
|
||||
for obj in context.selected_objects:
|
||||
new_obj = obj.copy()
|
||||
if obj.data:
|
||||
new_obj.data = obj.data.copy()
|
||||
if obj == context.active_object:
|
||||
self.new_active_obj = new_obj
|
||||
for collection in obj.users_collection:
|
||||
@@ -605,10 +595,13 @@ class OverrideDuplicateMoveLinked(bpy.types.Operator):
|
||||
# Copy the actual class
|
||||
new = blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj)
|
||||
if new:
|
||||
array_pset = ifcopenshell.util.element.get_pset(new, "BBIM_Array")
|
||||
if array_pset:
|
||||
array_pset = tool.Ifc.get().by_id(array_pset["id"])
|
||||
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=new, pset=array_pset)
|
||||
old_to_new[tool.Ifc.get_entity(obj)] = new
|
||||
# Recreate decompositions
|
||||
tool.Root.recreate_decompositions(relationships, old_to_new)
|
||||
bpy.ops.transform.translate("INVOKE_DEFAULT")
|
||||
blenderbim.bim.handler.purge_module_data()
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -616,6 +609,12 @@ class OverrideDuplicateMoveLinked(bpy.types.Operator):
|
||||
class OverrideJoin(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.override_object_join"
|
||||
bl_label = "IFC Join"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
if not tool.Ifc.get():
|
||||
return bpy.ops.object.join()
|
||||
return self.execute(context)
|
||||
|
||||
def _execute(self, context):
|
||||
if not tool.Ifc.get():
|
||||
@@ -638,7 +637,7 @@ class OverrideJoin(bpy.types.Operator, Operator):
|
||||
continue
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element:
|
||||
tool.Ifc.delete(element)
|
||||
ifcopenshell.api.run("root.remove_product", tool.Ifc.get(), product=element)
|
||||
bpy.ops.object.join()
|
||||
bpy.ops.bim.update_representation(obj=self.target.name, ifc_representation_class="")
|
||||
elif representation.RepresentationType == "SweptSolid":
|
||||
@@ -649,7 +648,7 @@ class OverrideJoin(bpy.types.Operator, Operator):
|
||||
continue
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
|
||||
# Non IFC elements cannot be join since we cannot guarantee SweptSolid compliance
|
||||
# Non IFC elements cannot be joined since we cannot guarantee SweptSolid compliance
|
||||
if not element:
|
||||
obj.select_set(False)
|
||||
continue
|
||||
@@ -664,6 +663,9 @@ class OverrideJoin(bpy.types.Operator, Operator):
|
||||
|
||||
for item in obj_rep.Items:
|
||||
copied_item = ifcopenshell.util.element.copy_deep(tool.Ifc.get(), item)
|
||||
for style in item.StyledByItem:
|
||||
copied_style = ifcopenshell.util.element.copy(tool.Ifc.get(), style)
|
||||
copied_style.Item = copied_item
|
||||
if copied_item.Position:
|
||||
position = ifcopenshell.util.placement.get_axis2placement(copied_item.Position)
|
||||
else:
|
||||
@@ -676,7 +678,7 @@ class OverrideJoin(bpy.types.Operator, Operator):
|
||||
tool.Ifc.get().createIfcDirection([float(n) for n in position[:, 0][:3]]),
|
||||
)
|
||||
items.append(copied_item)
|
||||
tool.Ifc.delete(element)
|
||||
ifcopenshell.api.run("root.remove_product", tool.Ifc.get(), product=element)
|
||||
representation.Items = items
|
||||
bpy.ops.object.join()
|
||||
core.switch_representation(
|
||||
@@ -696,13 +698,14 @@ class OverrideJoin(bpy.types.Operator, Operator):
|
||||
continue
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element:
|
||||
tool.Ifc.delete(element)
|
||||
ifcopenshell.api.run("root.remove_product", tool.Ifc.get(), product=element)
|
||||
bpy.ops.object.join()
|
||||
|
||||
|
||||
class OverridePasteBuffer(bpy.types.Operator):
|
||||
bl_idname = "bim.override_paste_buffer"
|
||||
bl_label = "IFC Paste BIM Objects"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
bpy.ops.view3d.pastebuffer()
|
||||
@@ -721,10 +724,16 @@ class OverrideModeSetEdit(bpy.types.Operator):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
|
||||
def _execute(self, context):
|
||||
objs = context.selected_objects or [context.active_object]
|
||||
objs = context.selected_objects or ([context.active_object] if context.active_object else [])
|
||||
active_obj = context.active_object
|
||||
|
||||
if context.active_object:
|
||||
context.active_object.select_set(True)
|
||||
edited_objs = []
|
||||
|
||||
element = tool.Ifc.get_entity(context.active_object)
|
||||
if element and element.is_a("IfcRelSpaceBoundary"):
|
||||
return bpy.ops.bim.enable_editing_boundary_geometry()
|
||||
|
||||
for obj in objs:
|
||||
if not obj:
|
||||
continue
|
||||
@@ -742,26 +751,43 @@ class OverrideModeSetEdit(bpy.types.Operator):
|
||||
obj.select_set(False)
|
||||
continue
|
||||
|
||||
if obj.data.BIMMeshProperties.ifc_definition_id:
|
||||
representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
|
||||
if representation.RepresentationType in ("Tessellation", "Brep"):
|
||||
if element.HasOpenings:
|
||||
# Mesh elements with openings must disable openings
|
||||
# so that you can edit the original topology.
|
||||
core.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
apply_openings=False,
|
||||
)
|
||||
obj.data.BIMMeshProperties.mesh_checksum = tool.Geometry.get_mesh_checksum(obj.data)
|
||||
if not context.selected_objects:
|
||||
return {"FINISHED"}
|
||||
bpy.ops.object.mode_set(mode="EDIT", toggle=True)
|
||||
representation = tool.Geometry.get_active_representation(obj)
|
||||
if not representation:
|
||||
continue
|
||||
|
||||
if tool.Geometry.is_meshlike(representation):
|
||||
if getattr(element, "HasOpenings", None):
|
||||
# Mesh elements with openings must disable openings
|
||||
# so that you can edit the original topology.
|
||||
core.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
apply_openings=False,
|
||||
)
|
||||
obj.data.BIMMeshProperties.mesh_checksum = tool.Geometry.get_mesh_checksum(obj.data)
|
||||
else:
|
||||
obj.select_set(False)
|
||||
continue
|
||||
|
||||
if not context.selected_objects or len(context.selected_objects) != len(objs):
|
||||
# We are trying to edit at least one non-mesh-like object : Display a hint to the user
|
||||
self.report({"INFO"}, "Only mesh-compatible representations may be edited in edit mode.")
|
||||
|
||||
if context.active_object not in context.selected_objects:
|
||||
# The active object is non-mesh-like. Set a valid object (or None) as active
|
||||
context.view_layer.objects.active = context.selected_objects[0] if context.selected_objects else None
|
||||
if context.active_object:
|
||||
bpy.ops.object.mode_set(mode="EDIT", toggle=True)
|
||||
else:
|
||||
# restore the selection if nothing worked
|
||||
for obj in objs:
|
||||
obj.select_set(True)
|
||||
context.view_layer.objects.active = active_obj
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
@@ -783,7 +809,7 @@ class OverrideModeSetObject(bpy.types.Operator):
|
||||
for obj in self.edited_objs:
|
||||
if self.should_save:
|
||||
bpy.ops.bim.update_representation(obj=obj.name, ifc_representation_class="")
|
||||
if tool.Ifc.get_entity(obj).HasOpenings:
|
||||
if getattr(tool.Ifc.get_entity(obj), "HasOpenings", False):
|
||||
self.reload_representation(obj)
|
||||
else:
|
||||
self.reload_representation(obj)
|
||||
@@ -806,15 +832,27 @@ class OverrideModeSetObject(bpy.types.Operator):
|
||||
)
|
||||
|
||||
def draw(self, context):
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self, "should_save")
|
||||
if self.is_valid:
|
||||
row = self.layout.row()
|
||||
row.prop(self, "should_save")
|
||||
else:
|
||||
row = self.layout.row()
|
||||
row.label(text="No Geometry Found: Object will revert to previous state.")
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.is_valid = True
|
||||
self.should_save = True
|
||||
|
||||
bpy.ops.object.mode_set(mode="EDIT", toggle=True)
|
||||
|
||||
if not tool.Ifc.get():
|
||||
return {"FINISHED"}
|
||||
|
||||
if context.active_object:
|
||||
element = tool.Ifc.get_entity(context.active_object)
|
||||
if element and element.is_a("IfcRelSpaceBoundary"):
|
||||
return bpy.ops.bim.edit_boundary_geometry()
|
||||
|
||||
objs = context.selected_objects or [context.active_object]
|
||||
|
||||
self.edited_objs = []
|
||||
@@ -829,13 +867,15 @@ class OverrideModeSetObject(bpy.types.Operator):
|
||||
continue
|
||||
|
||||
if obj.data.BIMMeshProperties.ifc_definition_id:
|
||||
if not tool.Geometry.has_geometric_data(obj):
|
||||
self.is_valid = False
|
||||
self.should_save = False
|
||||
representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
|
||||
if representation.RepresentationType in (
|
||||
"Tessellation",
|
||||
"Brep",
|
||||
if tool.Geometry.is_meshlike(
|
||||
representation
|
||||
) and obj.data.BIMMeshProperties.mesh_checksum != tool.Geometry.get_mesh_checksum(obj.data):
|
||||
self.edited_objs.append(obj)
|
||||
elif element.HasOpenings:
|
||||
elif getattr(element, "HasOpenings", None):
|
||||
self.unchanged_objs_with_openings.append(obj)
|
||||
|
||||
if self.edited_objs:
|
||||
|
||||
@@ -49,7 +49,7 @@ class BIM_OT_cityjson2ifc(Operator):
|
||||
|
||||
class BIM_OT_find_cityjson_lod(Operator):
|
||||
bl_idname = "bim.find_cityjson_lod"
|
||||
bl_label = "Find LODs in CityJSON file"
|
||||
bl_label = "Find LODs in CityJSON File"
|
||||
bl_context = "scene"
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# This program 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 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program 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 this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# 2023 Bruno Postle <bruno@postle.net>, Bruno Perdigão <brunoperdigao@tutanota.com>,
|
||||
# Massimo Fabbro <maxfb87@yahoo.it>
|
||||
|
||||
import bpy
|
||||
from . import ui, prop, operator
|
||||
|
||||
|
||||
classes = (
|
||||
operator.AddFileToRepo,
|
||||
operator.AddTag,
|
||||
operator.CloneRepo,
|
||||
operator.CommitChanges,
|
||||
operator.CreateRepo,
|
||||
operator.DiscardUncommitted,
|
||||
operator.DisplayRevision,
|
||||
operator.DisplayUncommitted,
|
||||
operator.Fetch,
|
||||
operator.Merge,
|
||||
operator.Push,
|
||||
operator.RefreshGit,
|
||||
operator.SwitchRevision,
|
||||
prop.IfcGitTag,
|
||||
prop.IfcGitListItem,
|
||||
prop.IfcGitProperties,
|
||||
ui.IFCGIT_PT_panel,
|
||||
ui.COMMIT_UL_List,
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
bpy.types.Scene.IfcGitProperties = bpy.props.PointerProperty(type=prop.IfcGitProperties)
|
||||
|
||||
|
||||
def unregister():
|
||||
del bpy.types.Scene.IfcGitProperties
|
||||
@@ -0,0 +1,144 @@
|
||||
import bpy
|
||||
import os
|
||||
import shutil
|
||||
|
||||
# import tool
|
||||
import blenderbim.tool as tool
|
||||
|
||||
|
||||
def refresh():
|
||||
IfcGitData.is_loaded = False
|
||||
|
||||
|
||||
class IfcGitData:
|
||||
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.data = {
|
||||
"repo": cls.repo(),
|
||||
"remotes": cls.remotes(),
|
||||
"branch_names": cls.branch_names(),
|
||||
"remote_names": cls.remote_names(),
|
||||
"remote_urls": cls.remote_urls(),
|
||||
"path_ifc": cls.path_ifc(),
|
||||
"branches_by_hexsha": cls.branches_by_hexsha(),
|
||||
"tags_by_hexsha": cls.tags_by_hexsha(),
|
||||
"name_ifc": cls.name_ifc(),
|
||||
"dir_name": cls.dir_name(),
|
||||
"base_name": cls.base_name(),
|
||||
"is_dirty": cls.is_dirty(),
|
||||
"commit": cls.commit(),
|
||||
"current_revision": cls.current_revision(),
|
||||
"git_exe": cls.git_exe(),
|
||||
"ifcmerge_exe": cls.ifcmerge_exe(),
|
||||
}
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def repo(cls):
|
||||
if bool(tool.Ifc.get()):
|
||||
path_ifc = tool.Ifc.get_path()
|
||||
if os.path.isfile(path_ifc):
|
||||
return tool.IfcGit.repo_from_path(path_ifc)
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def remotes(cls):
|
||||
if cls.repo():
|
||||
return cls.repo().remotes
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def branch_names(cls):
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def remote_names(cls):
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def remote_urls(cls):
|
||||
result = {}
|
||||
if cls.repo():
|
||||
for remote in cls.repo().remotes:
|
||||
result[remote.name] = remote.url
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def path_ifc(cls):
|
||||
path_ifc = tool.Ifc.get_path()
|
||||
if os.path.isfile(path_ifc):
|
||||
return tool.Ifc.get_path()
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def branches_by_hexsha(cls):
|
||||
try:
|
||||
if tool.IfcGitRepo.repo.branches:
|
||||
return tool.IfcGit.branches_by_hexsha(tool.IfcGitRepo.repo)
|
||||
except:
|
||||
return {}
|
||||
|
||||
@classmethod
|
||||
def tags_by_hexsha(cls):
|
||||
if tool.IfcGitRepo.repo:
|
||||
return tool.IfcGit.tags_by_hexsha(tool.IfcGitRepo.repo)
|
||||
return {}
|
||||
|
||||
@classmethod
|
||||
def name_ifc(cls):
|
||||
if bool(tool.Ifc.get()):
|
||||
path_ifc = tool.Ifc.get_path()
|
||||
if tool.IfcGitRepo.repo and os.path.isfile(path_ifc):
|
||||
working_dir = tool.IfcGitRepo.repo.working_dir
|
||||
return os.path.relpath(path_ifc, working_dir)
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def dir_name(cls):
|
||||
if bool(tool.Ifc.get()):
|
||||
path_ifc = tool.Ifc.get_path()
|
||||
if os.path.isfile(path_ifc):
|
||||
return os.path.dirname(path_ifc)
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def base_name(cls):
|
||||
if bool(tool.Ifc.get()):
|
||||
path_ifc = tool.Ifc.get_path()
|
||||
if os.path.isfile(path_ifc):
|
||||
return os.path.basename(path_ifc)
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def is_dirty(cls):
|
||||
if cls.repo() and cls.git_exe():
|
||||
path_ifc = tool.Ifc.get_path()
|
||||
if os.path.isfile(path_ifc):
|
||||
return cls.repo().is_dirty(path=path_ifc)
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def commit(cls):
|
||||
props = bpy.context.scene.IfcGitProperties
|
||||
if len(props.ifcgit_commits) > 0:
|
||||
item = props.ifcgit_commits[props.commit_index]
|
||||
if cls.repo():
|
||||
return cls.repo().commit(rev=item.hexsha)
|
||||
|
||||
@classmethod
|
||||
def current_revision(cls):
|
||||
props = bpy.context.scene.IfcGitProperties
|
||||
if len(props.ifcgit_commits) > 0:
|
||||
return tool.IfcGitRepo.repo.commit()
|
||||
|
||||
@classmethod
|
||||
def git_exe(cls):
|
||||
return shutil.which("git")
|
||||
|
||||
@classmethod
|
||||
def ifcmerge_exe(cls):
|
||||
return shutil.which("ifcmerge")
|
||||
@@ -0,0 +1,280 @@
|
||||
import os
|
||||
import re
|
||||
import bpy
|
||||
import blenderbim.core.ifcgit as core
|
||||
import blenderbim.tool as tool
|
||||
from blenderbim.bim.module.ifcgit.data import IfcGitData, refresh
|
||||
|
||||
|
||||
class CreateRepo(bpy.types.Operator):
|
||||
"""Initialise a Git repository"""
|
||||
|
||||
bl_label = "Create Git repository"
|
||||
bl_idname = "ifcgit.createrepo"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
path_ifc = IfcGitData.data["path_ifc"]
|
||||
if not os.path.isfile(path_ifc):
|
||||
return False
|
||||
if IfcGitData.data["repo"]:
|
||||
# repo already exists
|
||||
return False
|
||||
if re.match("^/home/[^/]+/?$", os.path.dirname(path_ifc)):
|
||||
# don't make ${HOME} a repo
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
core.create_repo(tool.IfcGit, tool.Ifc)
|
||||
refresh()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AddFileToRepo(bpy.types.Operator):
|
||||
"""Add a file to a repository"""
|
||||
|
||||
bl_label = "Add file to repository"
|
||||
bl_idname = "ifcgit.addfile"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
path_ifc = IfcGitData.data["path_ifc"]
|
||||
if not os.path.isfile(path_ifc):
|
||||
return False
|
||||
if not IfcGitData.data["repo"]:
|
||||
# repo doesn't exist
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
core.add_file(tool.IfcGit, tool.Ifc)
|
||||
refresh()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class CloneRepo(bpy.types.Operator):
|
||||
"""Clone a remote Git repository"""
|
||||
|
||||
bl_label = "Clone repository"
|
||||
bl_idname = "ifcgit.clone_repo"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
props = context.scene.IfcGitProperties
|
||||
if (
|
||||
props.remote_url
|
||||
and props.local_folder
|
||||
and os.path.isdir(props.local_folder)
|
||||
and not os.listdir(props.local_folder)
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
props = context.scene.IfcGitProperties
|
||||
core.clone_repo(tool.IfcGit, props.remote_url, props.local_folder, self)
|
||||
refresh()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DiscardUncommitted(bpy.types.Operator):
|
||||
"""Discard saved changes and update to HEAD"""
|
||||
|
||||
bl_label = "Discard uncommitted changes"
|
||||
bl_idname = "ifcgit.discard"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
core.discard_uncomitted(tool.IfcGit, tool.Ifc)
|
||||
refresh()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class CommitChanges(bpy.types.Operator):
|
||||
"""Commit current saved changes"""
|
||||
|
||||
bl_label = "Commit changes"
|
||||
bl_idname = "ifcgit.commit_changes"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
props = context.scene.IfcGitProperties
|
||||
repo = IfcGitData.data["repo"]
|
||||
if props.commit_message == "":
|
||||
return False
|
||||
if (
|
||||
repo
|
||||
and repo.head.is_detached
|
||||
and (
|
||||
not tool.IfcGit.is_valid_ref_format(props.new_branch_name)
|
||||
or props.new_branch_name in [branch.name for branch in repo.branches]
|
||||
)
|
||||
):
|
||||
cls.poll_message_set(
|
||||
"The new branch name is invalid, please insert a valid branch name (eg. with no spaces, ...)"
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
repo = IfcGitData.data["repo"]
|
||||
core.commit_changes(tool.IfcGit, tool.Ifc, repo, context)
|
||||
bpy.ops.ifcgit.refresh()
|
||||
refresh()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AddTag(bpy.types.Operator):
|
||||
"""Tag selected revision"""
|
||||
|
||||
bl_label = "Add tag"
|
||||
bl_idname = "ifcgit.add_tag"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
props = context.scene.IfcGitProperties
|
||||
repo = IfcGitData.data["repo"]
|
||||
if repo and (
|
||||
not tool.IfcGit.is_valid_ref_format(props.new_tag_name)
|
||||
or props.new_tag_name in [tag.name for tag in repo.tags]
|
||||
):
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
repo = IfcGitData.data["repo"]
|
||||
core.add_tag(tool.IfcGit, repo)
|
||||
bpy.ops.ifcgit.refresh()
|
||||
refresh()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RefreshGit(bpy.types.Operator):
|
||||
"""Refresh revision list"""
|
||||
|
||||
bl_label = ""
|
||||
bl_idname = "ifcgit.refresh"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
repo = IfcGitData.data["repo"]
|
||||
if repo != None and repo.heads:
|
||||
return True
|
||||
return False
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
repo = IfcGitData.data["repo"]
|
||||
core.refresh_revision_list(tool.IfcGit, repo, tool.Ifc)
|
||||
refresh()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DisplayRevision(bpy.types.Operator):
|
||||
"""Colourise objects by selected revision"""
|
||||
|
||||
bl_label = ""
|
||||
bl_idname = "ifcgit.display_revision"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
core.colourise_revision(tool.IfcGit, context)
|
||||
refresh()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DisplayUncommitted(bpy.types.Operator):
|
||||
"""Colourise uncommitted objects"""
|
||||
|
||||
bl_label = "Show uncommitted changes"
|
||||
bl_idname = "ifcgit.display_uncommitted"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
repo = IfcGitData.data["repo"]
|
||||
core.colourise_uncommitted(tool.IfcGit, tool.Ifc, repo)
|
||||
refresh()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SwitchRevision(bpy.types.Operator):
|
||||
"""Switches the repository to the selected revision and reloads the IFC file"""
|
||||
|
||||
bl_label = ""
|
||||
bl_idname = "ifcgit.switch_revision"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
core.switch_revision(tool.IfcGit, tool.Ifc)
|
||||
refresh()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class Merge(bpy.types.Operator):
|
||||
"""Merges the selected branch into working branch"""
|
||||
|
||||
bl_label = "Merge this branch"
|
||||
bl_idname = "ifcgit.merge"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if IfcGitData.data["ifcmerge_exe"]:
|
||||
return True
|
||||
return False
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
if core.merge_branch(tool.IfcGit, tool.Ifc, self):
|
||||
refresh()
|
||||
return {"FINISHED"}
|
||||
else:
|
||||
return {"CANCELLED"}
|
||||
|
||||
|
||||
class Push(bpy.types.Operator):
|
||||
"""Pushes the working branch to selected remote"""
|
||||
|
||||
bl_label = "Push working branch"
|
||||
bl_idname = "ifcgit.push"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
props = context.scene.IfcGitProperties
|
||||
repo = IfcGitData.data["repo"]
|
||||
remote = repo.remotes[props.select_remote]
|
||||
remote.push()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class Fetch(bpy.types.Operator):
|
||||
"""Fetches from the selected remote"""
|
||||
|
||||
bl_label = "Fetch from remote"
|
||||
bl_idname = "ifcgit.fetch"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
props = context.scene.IfcGitProperties
|
||||
repo = IfcGitData.data["repo"]
|
||||
remote = repo.remotes[props.select_remote]
|
||||
remote.fetch()
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,136 @@
|
||||
import bpy
|
||||
from bpy.types import PropertyGroup
|
||||
from bpy.props import (
|
||||
StringProperty,
|
||||
BoolProperty,
|
||||
CollectionProperty,
|
||||
IntProperty,
|
||||
EnumProperty,
|
||||
)
|
||||
from blenderbim.bim.module.ifcgit.data import IfcGitData, refresh
|
||||
|
||||
|
||||
def git_branches(self, context):
|
||||
"""branches enum"""
|
||||
|
||||
# NOTE "Python must keep a reference to the strings returned by
|
||||
# the callback or Blender will misbehave or even crash"
|
||||
IfcGitData.data["branch_names"] = sorted([branch.name for branch in IfcGitData.data["repo"].heads])
|
||||
|
||||
if "main" in IfcGitData.data["branch_names"]:
|
||||
IfcGitData.data["branch_names"].remove("main")
|
||||
IfcGitData.data["branch_names"] = ["main"] + IfcGitData.data["branch_names"]
|
||||
|
||||
if IfcGitData.data["remotes"]:
|
||||
props = context.scene.IfcGitProperties
|
||||
IfcGitData.data["branch_names"] += [r.name for r in IfcGitData.data["remotes"][props.select_remote].refs]
|
||||
|
||||
return [(myname, myname, myname) for myname in IfcGitData.data["branch_names"]]
|
||||
|
||||
|
||||
def git_remotes(self, context):
|
||||
"""remotes enum"""
|
||||
|
||||
IfcGitData.data["remote_names"] = sorted([remote.name for remote in IfcGitData.data["remotes"]])
|
||||
|
||||
if "origin" in IfcGitData.data["remote_names"]:
|
||||
IfcGitData.data["remote_names"].remove("origin")
|
||||
IfcGitData.data["remote_names"] = ["origin"] + IfcGitData.data["remote_names"]
|
||||
|
||||
return [(myname, myname, myname) for myname in IfcGitData.data["remote_names"]]
|
||||
|
||||
|
||||
def update_revlist(self, context):
|
||||
"""wrapper to trigger update of the revision list"""
|
||||
|
||||
bpy.ops.ifcgit.refresh()
|
||||
props = context.scene.IfcGitProperties
|
||||
props.commit_index = 0
|
||||
|
||||
|
||||
class IfcGitTag(PropertyGroup):
|
||||
"""Properties of a Git tag"""
|
||||
|
||||
name: StringProperty(
|
||||
name="Tag name",
|
||||
default="",
|
||||
)
|
||||
message: StringProperty(
|
||||
name="Tag message",
|
||||
default="",
|
||||
)
|
||||
|
||||
|
||||
class IfcGitListItem(PropertyGroup):
|
||||
"""Group of properties representing an item in the list."""
|
||||
|
||||
hexsha: StringProperty(
|
||||
name="Git hash",
|
||||
description="checksum for this commit",
|
||||
default="Uncommitted data!",
|
||||
)
|
||||
relevant: BoolProperty(
|
||||
name="Is relevant",
|
||||
description="does this commit reference our ifc file",
|
||||
default=False,
|
||||
)
|
||||
author_name: StringProperty(
|
||||
name="Author Name",
|
||||
default="",
|
||||
)
|
||||
author_email: StringProperty(
|
||||
name="Author Email",
|
||||
default="",
|
||||
)
|
||||
message: StringProperty(
|
||||
name="Commit Message",
|
||||
default="",
|
||||
)
|
||||
tags: CollectionProperty(type=IfcGitTag, name="List of revision tags")
|
||||
|
||||
|
||||
class IfcGitProperties(PropertyGroup):
|
||||
|
||||
ifcgit_commits: CollectionProperty(type=IfcGitListItem, name="List of git items")
|
||||
commit_index: IntProperty(name="Index for my_list", default=0)
|
||||
commit_message: StringProperty(
|
||||
name="Commit message",
|
||||
description="A human readable description of these changes",
|
||||
default="",
|
||||
)
|
||||
new_branch_name: StringProperty(
|
||||
name="New branch name",
|
||||
description="A short name used to refer to this branch",
|
||||
default="",
|
||||
)
|
||||
new_tag_name: StringProperty(
|
||||
name="New tag name",
|
||||
description="A short name used to refer to this tag",
|
||||
default="",
|
||||
)
|
||||
new_tag_message: StringProperty(
|
||||
name="Tag message (optional)",
|
||||
description="An optional human readable description of this tag",
|
||||
default="",
|
||||
)
|
||||
remote_url: StringProperty(
|
||||
name="Git URL",
|
||||
description="A URL pointing to a Git repository",
|
||||
default="",
|
||||
)
|
||||
local_folder: StringProperty(
|
||||
name="Local folder",
|
||||
description="A local Git repository path",
|
||||
default="",
|
||||
subtype="DIR_PATH",
|
||||
)
|
||||
display_branch: EnumProperty(items=git_branches, update=update_revlist)
|
||||
select_remote: EnumProperty(items=git_remotes)
|
||||
ifcgit_filter: EnumProperty(
|
||||
items=[
|
||||
("all", "All", "All revisions"),
|
||||
("tagged", "Tagged", "Tagged revisions"),
|
||||
("relevant", "Relevant", "Revisions for this project"),
|
||||
],
|
||||
update=update_revlist,
|
||||
)
|
||||
@@ -0,0 +1,204 @@
|
||||
import bpy
|
||||
import time
|
||||
from blenderbim.bim.module.ifcgit.data import IfcGitData, refresh
|
||||
|
||||
|
||||
class IFCGIT_PT_panel(bpy.types.Panel):
|
||||
"""Scene Properties panel to interact with IFC repository data"""
|
||||
|
||||
bl_label = "IFC Git"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
bl_parent_id = "BIM_PT_project_info"
|
||||
|
||||
def draw(self, context):
|
||||
|
||||
if not IfcGitData.is_loaded:
|
||||
IfcGitData.load()
|
||||
|
||||
layout = self.layout
|
||||
path_ifc = IfcGitData.data["path_ifc"]
|
||||
|
||||
if not IfcGitData.data["git_exe"]:
|
||||
row = layout.row()
|
||||
row.label(text="Git is not installed", icon="ERROR")
|
||||
return
|
||||
|
||||
props = context.scene.IfcGitProperties
|
||||
|
||||
# TODO if file isn't saved, offer to save to disk
|
||||
|
||||
row = layout.row()
|
||||
if path_ifc:
|
||||
if IfcGitData.data["repo"]:
|
||||
name_ifc = IfcGitData.data["name_ifc"]
|
||||
row.label(text=IfcGitData.data["repo"].working_dir, icon="SYSTEM")
|
||||
if name_ifc in IfcGitData.data["repo"].untracked_files:
|
||||
row.operator(
|
||||
"ifcgit.addfile",
|
||||
text="Add '" + name_ifc + "' to repository",
|
||||
icon="FILE",
|
||||
)
|
||||
else:
|
||||
row.label(text=name_ifc, icon="FILE")
|
||||
else:
|
||||
row.operator(
|
||||
"ifcgit.createrepo",
|
||||
text="Create '" + IfcGitData.data["dir_name"] + "' repository",
|
||||
icon="SYSTEM",
|
||||
)
|
||||
row.label(text=IfcGitData.data["base_name"], icon="FILE")
|
||||
return
|
||||
else:
|
||||
row.label(text="No Git repository found", icon="SYSTEM")
|
||||
row.label(text="No IFC project saved", icon="FILE")
|
||||
|
||||
box = layout.box()
|
||||
row = box.row()
|
||||
row.label(text="Clone a remote Git repository")
|
||||
row = box.row()
|
||||
row.prop(props, "remote_url")
|
||||
row = box.row()
|
||||
row.prop(props, "local_folder")
|
||||
row = box.row()
|
||||
row.operator("ifcgit.clone_repo", icon="IMPORT")
|
||||
return
|
||||
|
||||
is_dirty = IfcGitData.data["is_dirty"]
|
||||
|
||||
if is_dirty:
|
||||
row = layout.row()
|
||||
row.label(text="Saved changes have not been committed", icon="ERROR")
|
||||
|
||||
row = layout.row()
|
||||
row.operator("ifcgit.display_uncommitted", icon="SELECT_DIFFERENCE")
|
||||
row.operator("ifcgit.discard", icon="TRASH")
|
||||
|
||||
row = layout.row()
|
||||
row.prop(props, "commit_message")
|
||||
|
||||
if IfcGitData.data["repo"].head.is_detached:
|
||||
row = layout.row()
|
||||
row.label(text="HEAD is detached, commit will create a branch", icon="ERROR")
|
||||
row.prop(props, "new_branch_name")
|
||||
|
||||
row = layout.row()
|
||||
row.operator("ifcgit.commit_changes", icon="GREASEPENCIL")
|
||||
|
||||
row = layout.row()
|
||||
if IfcGitData.data["repo"].head.is_detached:
|
||||
row.label(text="Working branch: Detached HEAD")
|
||||
else:
|
||||
row.label(text="Working branch: " + IfcGitData.data["repo"].active_branch.name)
|
||||
|
||||
grouped = layout.row()
|
||||
column = grouped.column()
|
||||
row = column.row()
|
||||
row.prop(props, "display_branch", text="Browse branch")
|
||||
row.prop(props, "ifcgit_filter", text="Filter revisions")
|
||||
|
||||
row = column.row()
|
||||
row.template_list(
|
||||
"COMMIT_UL_List",
|
||||
"The_List",
|
||||
props,
|
||||
"ifcgit_commits",
|
||||
props,
|
||||
"commit_index",
|
||||
)
|
||||
column = grouped.column()
|
||||
row = column.row()
|
||||
row.operator("ifcgit.refresh", icon="FILE_REFRESH")
|
||||
|
||||
if not is_dirty:
|
||||
|
||||
row = column.row()
|
||||
row.operator("ifcgit.display_revision", icon="SELECT_DIFFERENCE")
|
||||
|
||||
row = column.row()
|
||||
row.operator("ifcgit.switch_revision", icon="CURRENT_FILE")
|
||||
|
||||
# TODO operator to tag selected
|
||||
|
||||
row = column.row()
|
||||
row.operator("ifcgit.merge", icon="EXPERIMENTAL", text="")
|
||||
|
||||
if not props.ifcgit_commits:
|
||||
return
|
||||
|
||||
item = props.ifcgit_commits[props.commit_index]
|
||||
if not item.relevant:
|
||||
row = layout.row()
|
||||
row.label(text="Revision unrelated to current IFC project", icon="ERROR")
|
||||
|
||||
box = layout.box()
|
||||
column = box.column(align=True)
|
||||
row = column.row()
|
||||
row.label(text=item.hexsha)
|
||||
row = column.row()
|
||||
row.label(text=item.author_name + " <" + item.author_email + ">")
|
||||
row = column.row()
|
||||
row.label(text=item.message)
|
||||
|
||||
for tag in item.tags:
|
||||
box = layout.box()
|
||||
item = box.row()
|
||||
column = item.column(align=True)
|
||||
row = column.row()
|
||||
row.label(text=tag.name)
|
||||
if tag.message:
|
||||
row = column.row()
|
||||
row.label(text=tag.message)
|
||||
# TODO
|
||||
# item.operator("ifcgit.delete_tag", icon="PANEL_CLOSE")
|
||||
|
||||
box = layout.box()
|
||||
row = box.row()
|
||||
row.prop(props, "new_tag_name")
|
||||
row = box.row()
|
||||
row.prop(props, "new_tag_message")
|
||||
row = box.row()
|
||||
row.operator("ifcgit.add_tag", icon="GREASEPENCIL")
|
||||
|
||||
if IfcGitData.data["remotes"]:
|
||||
row = layout.row()
|
||||
row.prop(props, "select_remote", text="Select remote")
|
||||
urls = IfcGitData.data["remote_urls"]
|
||||
row.label(text=urls[props.select_remote])
|
||||
row = layout.row()
|
||||
row.operator("ifcgit.push", icon="EXPERIMENTAL")
|
||||
row.operator("ifcgit.fetch", icon="IMPORT")
|
||||
|
||||
|
||||
class COMMIT_UL_List(bpy.types.UIList):
|
||||
"""List of Git commits"""
|
||||
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
|
||||
|
||||
props = context.scene.IfcGitProperties
|
||||
|
||||
current_revision = IfcGitData.data["current_revision"]
|
||||
|
||||
# TODO Figure how this "item" can be acesse in "data.py"
|
||||
# so it's possible to move the ".commit"
|
||||
commit = IfcGitData.data["repo"].commit(rev=item.hexsha)
|
||||
|
||||
lookup = IfcGitData.data["branches_by_hexsha"]
|
||||
refs = ""
|
||||
if item.hexsha in lookup:
|
||||
for branch in lookup[item.hexsha]:
|
||||
if branch.name == props.display_branch:
|
||||
refs = "[" + branch.name + "] "
|
||||
|
||||
lookup = IfcGitData.data["tags_by_hexsha"]
|
||||
if item.hexsha in lookup:
|
||||
for tag in lookup[item.hexsha]:
|
||||
refs += "{" + tag.name + "} "
|
||||
|
||||
if commit == current_revision:
|
||||
layout.label(text="[HEAD] " + refs + commit.message, icon="DECORATE_KEYFRAME")
|
||||
else:
|
||||
layout.label(text=refs + commit.message, icon="DECORATE_ANIMATE")
|
||||
layout.label(text=time.strftime("%c", time.localtime(commit.committed_date)))
|
||||
@@ -31,13 +31,16 @@ classes = (
|
||||
operator.ContractMaterialCategory,
|
||||
operator.CopyMaterial,
|
||||
operator.DisableEditingAssignedMaterial,
|
||||
operator.DisableEditingMaterial,
|
||||
operator.DisableEditingMaterials,
|
||||
operator.DisableEditingMaterialSetItem,
|
||||
operator.DisableEditingMaterialSetItemProfile,
|
||||
operator.DisableEditingMaterials,
|
||||
operator.EditAssignedMaterial,
|
||||
operator.EditMaterial,
|
||||
operator.EditMaterialSetItem,
|
||||
operator.EditMaterialSetItemProfile,
|
||||
operator.EnableEditingAssignedMaterial,
|
||||
operator.EnableEditingMaterial,
|
||||
operator.EnableEditingMaterialSetItem,
|
||||
operator.EnableEditingMaterialSetItemProfile,
|
||||
operator.ExpandMaterialCategory,
|
||||
|
||||
@@ -24,6 +24,7 @@ import ifcopenshell.util.attribute
|
||||
import ifcopenshell.util.representation
|
||||
import blenderbim.bim.helper
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.core.style
|
||||
import blenderbim.core.material as core
|
||||
import blenderbim.bim.module.model.profile as model_profile
|
||||
from blenderbim.bim.module.material.prop import purge as material_prop_purge
|
||||
@@ -58,6 +59,36 @@ class SelectByMaterial(bpy.types.Operator, tool.Ifc.Operator):
|
||||
core.select_by_material(tool.Material, material=tool.Ifc.get().by_id(self.material))
|
||||
|
||||
|
||||
class EnableEditingMaterial(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.enable_editing_material"
|
||||
bl_label = "Enable Editing Material"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
material: bpy.props.IntProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
core.enable_editing_material(tool.Material, material=tool.Ifc.get().by_id(self.material))
|
||||
|
||||
|
||||
class EditMaterial(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.edit_material"
|
||||
bl_label = "Edit Material"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
material: bpy.props.IntProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
core.edit_material(tool.Ifc, tool.Material, material=tool.Ifc.get().by_id(self.material))
|
||||
|
||||
|
||||
class DisableEditingMaterial(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.disable_editing_material"
|
||||
bl_label = "Disable Editing Material"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
material: bpy.props.IntProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
core.disable_editing_material(tool.Material)
|
||||
|
||||
|
||||
class AssignParameterizedProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.assign_parameterized_profile"
|
||||
bl_label = "Assign Parameterized Profile"
|
||||
@@ -337,7 +368,8 @@ class RemoveLayer(bpy.types.Operator, tool.Ifc.Operator):
|
||||
def _execute(self, context):
|
||||
for inverse in tool.Ifc.get().get_inverse(tool.Ifc.get().by_id(self.layer)):
|
||||
if inverse.is_a("IfcMaterialLayerSet") and len(inverse.MaterialLayers) == 1:
|
||||
return
|
||||
self.report({"ERROR"}, "Cannot remove material layer - IfcMaterialLayerSet should alawys have atleast 1 layer")
|
||||
return {"ERROR"}
|
||||
ifcopenshell.api.run("material.remove_layer", tool.Ifc.get(), layer=tool.Ifc.get().by_id(self.layer))
|
||||
|
||||
|
||||
@@ -515,7 +547,7 @@ class EditAssignedMaterial(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
class EnableEditingMaterialSetItemProfile(bpy.types.Operator):
|
||||
bl_idname = "bim.enable_editing_material_set_item_profile"
|
||||
bl_label = "Enable Editing Material Set Item"
|
||||
bl_label = "Enable Editing Material Set Item Profile"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
material_set_item: bpy.props.IntProperty()
|
||||
@@ -531,7 +563,7 @@ class EnableEditingMaterialSetItemProfile(bpy.types.Operator):
|
||||
|
||||
class DisableEditingMaterialSetItemProfile(bpy.types.Operator):
|
||||
bl_idname = "bim.disable_editing_material_set_item_profile"
|
||||
bl_label = "Disable Editing Material Set Item"
|
||||
bl_label = "Disable Editing Material Set Item Profile"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
|
||||
@@ -666,8 +698,12 @@ class CopyMaterial(bpy.types.Operator, tool.Ifc.Operator):
|
||||
def _execute(self, context):
|
||||
blender_material = context.active_object.active_material
|
||||
material = tool.Ifc.get_entity(blender_material)
|
||||
|
||||
if tool.Ifc.has_changed_shading(blender_material):
|
||||
blenderbim.core.style.update_style_colours(tool.Ifc, tool.Style, obj=blender_material)
|
||||
|
||||
copied_material = ifcopenshell.api.run("material.copy_material", tool.Ifc.get(), material=material)
|
||||
copied_blender_material = bpy.data.materials.new(blender_material.name)
|
||||
copied_blender_material = blender_material.copy()
|
||||
copied_style = self.get_style(copied_material)
|
||||
tool.Ifc.link(copied_material, copied_blender_material)
|
||||
if copied_style:
|
||||
|
||||
@@ -131,6 +131,9 @@ class BIMMaterialProperties(PropertyGroup):
|
||||
materials: CollectionProperty(name="Materials", type=Material)
|
||||
active_material_index: IntProperty(name="Active Material Index")
|
||||
profiles: EnumProperty(items=get_profiles, name="Profiles")
|
||||
active_material_id: IntProperty(name="Active Material ID")
|
||||
material_attributes: CollectionProperty(name="Material Attributes", type=Attribute)
|
||||
editing_material_type = StringProperty(name="Editing Material Type")
|
||||
|
||||
|
||||
class BIMObjectMaterialProperties(PropertyGroup):
|
||||
|
||||
@@ -57,13 +57,21 @@ class BIM_PT_materials(Panel):
|
||||
row.alignment = "RIGHT"
|
||||
|
||||
if self.props.material_type == "IfcMaterial":
|
||||
row.operator("bim.add_material", text="", icon="ADD")
|
||||
if self.props.materials and self.props.active_material_index < len(self.props.materials):
|
||||
material = self.props.materials[self.props.active_material_index]
|
||||
if material.ifc_definition_id:
|
||||
op = row.operator("bim.select_by_material", text="", icon="RESTRICT_SELECT_OFF")
|
||||
op.material = material.ifc_definition_id
|
||||
row.operator("bim.remove_material", text="", icon="X").material = material.ifc_definition_id
|
||||
if self.props.active_material_id:
|
||||
row.operator("bim.edit_material", text="", icon="CHECKMARK").material = material.ifc_definition_id
|
||||
row.operator("bim.disable_editing_material", text="", icon="CANCEL").material = material.ifc_definition_id
|
||||
self.draw_editable_material_attributes_ui()
|
||||
else:
|
||||
row.operator("bim.add_material", text="", icon="ADD")
|
||||
op = row.operator("bim.select_by_material", text="", icon="RESTRICT_SELECT_OFF")
|
||||
op.material = material.ifc_definition_id
|
||||
row.operator("bim.enable_editing_material", text="", icon="GREASEPENCIL").material = material.ifc_definition_id
|
||||
row.operator("bim.remove_material", text="", icon="X").material = material.ifc_definition_id
|
||||
else:
|
||||
row.operator("bim.add_material", text="", icon="ADD")
|
||||
else:
|
||||
row.operator("bim.add_material_set", text="", icon="ADD").set_type = self.props.material_type
|
||||
if self.props.materials and self.props.active_material_index < len(self.props.materials):
|
||||
@@ -76,6 +84,9 @@ class BIM_PT_materials(Panel):
|
||||
self.layout.template_list("BIM_UL_materials", "", self.props, "materials", self.props, "active_material_index")
|
||||
|
||||
|
||||
def draw_editable_material_attributes_ui(self):
|
||||
blenderbim.bim.helper.draw_attributes(self.props.material_attributes, self.layout)
|
||||
|
||||
class BIM_PT_material(Panel):
|
||||
bl_label = "IFC Material"
|
||||
bl_idname = "BIM_PT_material"
|
||||
@@ -196,7 +207,7 @@ class BIM_PT_object_material(Panel):
|
||||
else:
|
||||
row = self.layout.row(align=True)
|
||||
if ObjectMaterialData.data["set_item_name"] == "profile":
|
||||
row.prop(self.mprops, "profiles", icon="ITALIC", text="")
|
||||
prop_with_search(row, self.mprops, "profiles", icon="ITALIC", text="")
|
||||
prop_with_search(row, self.props, "material", icon="MATERIAL", text="")
|
||||
op = row.operator(f"bim.add_{ObjectMaterialData.data['set_item_name']}", icon="ADD", text="")
|
||||
setattr(op, f"{ObjectMaterialData.data['set_item_name']}_set", ObjectMaterialData.data["set"]["id"])
|
||||
@@ -230,11 +241,11 @@ class BIM_PT_object_material(Panel):
|
||||
draw_attributes(self.props.material_set_item_attributes, box)
|
||||
|
||||
row = box.row()
|
||||
row.prop(self.props, "material_set_item_material", icon="MATERIAL", text="Material")
|
||||
prop_with_search(row, self.props, "material_set_item_material", icon="MATERIAL", text="Material")
|
||||
|
||||
if ObjectMaterialData.data["set_item_name"] == "profile":
|
||||
row = box.row()
|
||||
row.prop(self.mprops, "profiles", icon="ITALIC", text="Profile")
|
||||
prop_with_search(row, self.mprops, "profiles", icon="ITALIC", text="Profile")
|
||||
|
||||
def draw_read_only_set_item_ui(self, set_item, index, is_first=False, is_last=False):
|
||||
if ObjectMaterialData.data["material_class"] == "IfcMaterialList":
|
||||
|
||||
@@ -47,6 +47,9 @@ classes = (
|
||||
array.EnableEditingArray,
|
||||
array.RemoveArray,
|
||||
array.SelectArrayParent,
|
||||
array.Input3DCursorXArray,
|
||||
array.Input3DCursorYArray,
|
||||
array.Input3DCursorZArray,
|
||||
product.AddConstrTypeInstance,
|
||||
product.AddEmptyType,
|
||||
product.AlignProduct,
|
||||
@@ -93,9 +96,7 @@ classes = (
|
||||
slab.ResetVertex,
|
||||
slab.SetArcIndex,
|
||||
space.GenerateSpace,
|
||||
prop.ConstrTypeInfo,
|
||||
prop.ConstrClassInfo,
|
||||
prop.ConstrBrowserState,
|
||||
space.GenerateSpacesFromWalls,
|
||||
prop.BIMModelProperties,
|
||||
prop.BIMArrayProperties,
|
||||
prop.BIMStairProperties,
|
||||
@@ -113,7 +114,6 @@ classes = (
|
||||
ui.BIM_PT_railing,
|
||||
ui.BIM_PT_roof,
|
||||
ui.LaunchTypeManager,
|
||||
ui.HelpConstrTypes,
|
||||
ui.BIM_MT_model,
|
||||
grid.BIM_OT_add_object,
|
||||
stair.BIM_OT_add_object,
|
||||
@@ -126,6 +126,7 @@ classes = (
|
||||
pie.OpenPieClass,
|
||||
pie.PieUpdateContainer,
|
||||
pie.PieAddOpening,
|
||||
pie.PieAssignObjectAggregation,
|
||||
pie.VIEW3D_MT_PIE_bim,
|
||||
pie.VIEW3D_MT_PIE_bim_class,
|
||||
sverchok_modifier.CreateNewSverchokGraph,
|
||||
@@ -149,6 +150,7 @@ classes = (
|
||||
railing.AddRailing,
|
||||
railing.CancelEditingRailing,
|
||||
railing.FinishEditingRailing,
|
||||
railing.FlipRailingPathOrder,
|
||||
railing.EnableEditingRailing,
|
||||
railing.CancelEditingRailingPath,
|
||||
railing.FinishEditingRailingPath,
|
||||
|
||||
@@ -22,19 +22,28 @@ import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.util.element
|
||||
import blenderbim.tool as tool
|
||||
from mathutils import Vector
|
||||
from mathutils import Vector, Matrix
|
||||
|
||||
|
||||
class AddArray(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_array"
|
||||
bl_label = "Add Array"
|
||||
bl_options = {"REGISTER"}
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
obj = context.active_object
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
|
||||
array = {"children": [], "count": 1, "x": 0.0, "y": 0.0, "z": 0.0, "use_local_space": True}
|
||||
array = {
|
||||
"children": [],
|
||||
"count": 1,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0,
|
||||
"use_local_space": True,
|
||||
"sync_children": False,
|
||||
"method": "OFFSET",
|
||||
}
|
||||
|
||||
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array")
|
||||
|
||||
@@ -58,7 +67,7 @@ class AddArray(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class DisableEditingArray(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.disable_editing_array"
|
||||
bl_label = "Disable Editing Array"
|
||||
bl_options = {"REGISTER"}
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
context.active_object.BIMArrayProperties.is_editing = -1
|
||||
@@ -68,7 +77,7 @@ class DisableEditingArray(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class EnableEditingArray(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.enable_editing_array"
|
||||
bl_label = "Enable Editing Array"
|
||||
bl_options = {"REGISTER"}
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
item: bpy.props.IntProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
@@ -81,6 +90,8 @@ class EnableEditingArray(bpy.types.Operator, tool.Ifc.Operator):
|
||||
props.y = data["y"]
|
||||
props.z = data["z"]
|
||||
props.use_local_space = data.get("use_local_space", False)
|
||||
props.sync_children = data.get("sync_children", False)
|
||||
props.method = data.get("method", "OFFSET")
|
||||
props.is_editing = self.item
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -88,7 +99,7 @@ class EnableEditingArray(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class EditArray(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.edit_array"
|
||||
bl_label = "Edit Array"
|
||||
bl_options = {"REGISTER"}
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
item: bpy.props.IntProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
@@ -105,6 +116,8 @@ class EditArray(bpy.types.Operator, tool.Ifc.Operator):
|
||||
"y": props.y,
|
||||
"z": props.z,
|
||||
"use_local_space": props.use_local_space,
|
||||
"sync_children": props.sync_children,
|
||||
"method": props.method,
|
||||
}
|
||||
|
||||
props.is_editing = -1
|
||||
@@ -125,7 +138,7 @@ class EditArray(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class RemoveArray(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.remove_array"
|
||||
bl_label = "Remove Array"
|
||||
bl_options = {"REGISTER"}
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
item: bpy.props.IntProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
@@ -173,3 +186,48 @@ class SelectArrayParent(bpy.types.Operator):
|
||||
context.view_layer.objects.active = obj
|
||||
obj.select_set(True)
|
||||
return {"FINISHED"}
|
||||
|
||||
class Input3DCursorXArray(bpy.types.Operator):
|
||||
bl_idname = "bim.input_cursor_x_array"
|
||||
bl_label = "Get 3d Cursor X Input for Array"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
props = obj.BIMArrayProperties
|
||||
cursor = context.scene.cursor
|
||||
if props.use_local_space:
|
||||
props.x = (Matrix.inverted(obj.matrix_world) @ cursor.matrix.col[3]).x
|
||||
else:
|
||||
props.x = cursor.location.x - obj.location.x
|
||||
return {"FINISHED"}
|
||||
|
||||
class Input3DCursorYArray(bpy.types.Operator):
|
||||
bl_idname = "bim.input_cursor_y_array"
|
||||
bl_label = "Get 3d Cursor Y Input for Array"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
props = obj.BIMArrayProperties
|
||||
cursor = context.scene.cursor
|
||||
if props.use_local_space:
|
||||
props.y = (Matrix.inverted(obj.matrix_world) @ cursor.matrix.col[3]).y
|
||||
else:
|
||||
props.y = cursor.location.y - obj.location.y
|
||||
return {"FINISHED"}
|
||||
|
||||
class Input3DCursorZArray(bpy.types.Operator):
|
||||
bl_idname = "bim.input_cursor_z_array"
|
||||
bl_label = "Get 3d Cursor Z Input for Array"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
props = obj.BIMArrayProperties
|
||||
cursor = context.scene.cursor
|
||||
if props.use_local_space:
|
||||
props.z = (Matrix.inverted(obj.matrix_world) @ cursor.matrix.col[3]).z
|
||||
else:
|
||||
props.z = cursor.location.z - obj.location.z
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -24,8 +24,6 @@ import ifcopenshell
|
||||
import ifcopenshell.util.element
|
||||
from ifcopenshell.util.doc import get_entity_doc, get_predefined_type_doc
|
||||
import blenderbim.tool as tool
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.module.model.root import ConstrTypeEntityNotFound
|
||||
|
||||
|
||||
def refresh():
|
||||
@@ -49,9 +47,8 @@ class AuthoringData:
|
||||
def load(cls):
|
||||
cls.is_loaded = True
|
||||
cls.props = bpy.context.scene.BIMModelProperties
|
||||
cls.load_ifc_classes()
|
||||
cls.load_relating_types()
|
||||
cls.load_relating_types_browser()
|
||||
cls.data["ifc_classes"] = cls.ifc_classes()
|
||||
cls.data["relating_type_id"] = cls.relating_type_id()
|
||||
cls.data["type_class"] = cls.type_class()
|
||||
cls.data["type_predefined_type"] = cls.type_predefined_type()
|
||||
cls.data["total_types"] = cls.total_types()
|
||||
@@ -62,8 +59,19 @@ class AuthoringData:
|
||||
cls.data["type_thumbnail"] = cls.type_thumbnail()
|
||||
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()
|
||||
cls.data["active_class"] = cls.active_class()
|
||||
cls.data["active_material_usage"] = cls.active_material_usage()
|
||||
cls.data["active_representation_type"] = cls.active_representation_type()
|
||||
cls.data["boundary_class"] = cls.boundary_class()
|
||||
|
||||
@classmethod
|
||||
def boundary_class(cls):
|
||||
declaration = tool.Ifc.schema().declaration_by_name("IfcRelSpaceBoundary")
|
||||
declarations = ifcopenshell.util.schema.get_subtypes(declaration)
|
||||
names = [d.name() for d in declarations]
|
||||
version = tool.Ifc.get_schema()
|
||||
return [(c, c, get_entity_doc(version, c).get("description", "")) for c in sorted(names)]
|
||||
|
||||
@classmethod
|
||||
def type_class(cls):
|
||||
@@ -71,7 +79,10 @@ class AuthoringData:
|
||||
declarations = ifcopenshell.util.schema.get_subtypes(declaration)
|
||||
names = [d.name() for d in declarations]
|
||||
|
||||
declaration = tool.Ifc.schema().declaration_by_name("IfcSpatialElementType")
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
declaration = tool.Ifc.schema().declaration_by_name("IfcSpatialStructureElementType")
|
||||
else:
|
||||
declaration = tool.Ifc.schema().declaration_by_name("IfcSpatialElementType")
|
||||
declarations = ifcopenshell.util.schema.get_subtypes(declaration)
|
||||
names.extend([d.name() for d in declarations])
|
||||
|
||||
@@ -99,23 +110,11 @@ class AuthoringData:
|
||||
|
||||
@classmethod
|
||||
def type_thumbnail(cls):
|
||||
if not cls.data["relating_types_ids"]:
|
||||
if not cls.data["relating_type_id"]:
|
||||
return 0
|
||||
element = tool.Ifc.get().by_id(int(cls.props.relating_type_id))
|
||||
return cls.type_thumbnails.get(element.id(), None) or 0
|
||||
|
||||
@classmethod
|
||||
def load_ifc_classes(cls):
|
||||
cls.data["ifc_classes"] = cls.ifc_classes()
|
||||
|
||||
@classmethod
|
||||
def load_relating_types(cls):
|
||||
cls.data["relating_types_ids"] = cls.relating_types()
|
||||
|
||||
@classmethod
|
||||
def load_relating_types_browser(cls):
|
||||
cls.data["relating_types_ids_browser"] = cls.relating_types_browser()
|
||||
|
||||
@classmethod
|
||||
def total_types(cls):
|
||||
type_class = cls.props.type_class
|
||||
@@ -175,6 +174,17 @@ class AuthoringData:
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def has_visible_boundaries(cls):
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
if element:
|
||||
if element.is_a("IfcRelSpaceBoundary"):
|
||||
return True
|
||||
for boundary in getattr(element, "BoundedBy", []):
|
||||
if tool.Ifc.get_object(boundary):
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def active_class(cls):
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
@@ -187,169 +197,45 @@ class AuthoringData:
|
||||
if element:
|
||||
return tool.Model.get_usage_type(element)
|
||||
|
||||
@classmethod
|
||||
def active_representation_type(cls):
|
||||
if bpy.context.active_object:
|
||||
representation = tool.Geometry.get_active_representation(bpy.context.active_object)
|
||||
if representation and representation.is_a("IfcShapeRepresentation"):
|
||||
return representation.RepresentationType
|
||||
|
||||
@classmethod
|
||||
def ifc_classes(cls):
|
||||
results = []
|
||||
classes = {
|
||||
e.is_a()
|
||||
for e in tool.Ifc.get().by_type("IfcElementType")
|
||||
+ tool.Ifc.get().by_type("IfcDoorStyle")
|
||||
+ tool.Ifc.get().by_type("IfcWindowStyle")
|
||||
+ tool.Ifc.get().by_type("IfcSpaceType")
|
||||
e.is_a() for e in (tool.Ifc.get().by_type("IfcElementType") + tool.Ifc.get().by_type("IfcSpaceType"))
|
||||
}
|
||||
|
||||
if tool.Ifc.get_schema() in ("IFC2X3", "IFC4"):
|
||||
classes.update(
|
||||
{e.is_a() for e in (tool.Ifc.get().by_type("IfcDoorStyle") + tool.Ifc.get().by_type("IfcWindowStyle"))}
|
||||
)
|
||||
results.extend([(c, c, "") for c in sorted(classes)])
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
def constr_class_entities(cls, ifc_class=None):
|
||||
def relating_type_id(cls):
|
||||
ifc_classes = cls.data["ifc_classes"]
|
||||
if not ifc_classes:
|
||||
return []
|
||||
results = []
|
||||
if ifc_class is None:
|
||||
ifc_class = cls.props.ifc_class
|
||||
ifc_class = cls.props.ifc_class
|
||||
if not ifc_class and ifc_classes:
|
||||
ifc_class = ifc_classes[0][0]
|
||||
if ifc_class:
|
||||
elements = sorted(tool.Ifc.get().by_type(ifc_class), key=lambda s: s.Name or "Unnamed")
|
||||
results.extend(elements)
|
||||
return results
|
||||
return [
|
||||
(str(e.id()), e.Name or "Unnamed", e.Description or "")
|
||||
for e in results
|
||||
]
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def relating_types(cls, ifc_class=None):
|
||||
return [
|
||||
(str(e.id()), e.Name or "Unnamed", e.Description or "")
|
||||
for e in cls.constr_class_entities(ifc_class=ifc_class)
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def relating_types_browser(cls):
|
||||
if cls.data["ifc_classes"]:
|
||||
return cls.relating_types(ifc_class=cls.props.ifc_class_browser)
|
||||
|
||||
@classmethod
|
||||
def new_constr_class_info(cls, ifc_class):
|
||||
if ifc_class not in cls.props.constr_classes:
|
||||
cls.props.constr_classes.add().name = ifc_class
|
||||
return cls.props.constr_classes[ifc_class]
|
||||
|
||||
@classmethod
|
||||
def assetize_constr_class(cls, ifc_class=None):
|
||||
selected_ifc_class = cls.props.ifc_class
|
||||
selected_relating_type_id = cls.props.relating_type_id
|
||||
if ifc_class is None:
|
||||
ifc_class = cls.props.ifc_class_browser
|
||||
if cls.constr_class_info(ifc_class) is None:
|
||||
cls.new_constr_class_info(ifc_class)
|
||||
constr_class_occurrences = cls.constr_class_entities(ifc_class)
|
||||
constr_classes = cls.props.constr_classes
|
||||
|
||||
for constr_class_entity in constr_class_occurrences:
|
||||
if (
|
||||
ifc_class not in constr_classes
|
||||
or constr_class_entity.Name not in constr_classes[ifc_class].constr_types
|
||||
):
|
||||
obj = tool.Ifc.get_object(constr_class_entity)
|
||||
cls.assetize_object(obj, ifc_class, constr_class_entity)
|
||||
cls.constr_class_info(ifc_class).fully_loaded = True
|
||||
cls.props.updating = True
|
||||
cls.props.ifc_class = selected_ifc_class
|
||||
cls.props.relating_type_id = selected_relating_type_id
|
||||
cls.props.updating = False
|
||||
|
||||
@classmethod
|
||||
def assetize_object(cls, obj, ifc_class, ifc_class_entity, from_selection=False):
|
||||
relating_type_id = ifc_class_entity.id()
|
||||
to_be_deleted = False
|
||||
if obj.type == "EMPTY":
|
||||
kwargs = {}
|
||||
if not from_selection:
|
||||
kwargs.update({"ifc_class": ifc_class, "relating_type_id": relating_type_id})
|
||||
new_obj = cls.new_relating_type(**kwargs)
|
||||
if new_obj is not None:
|
||||
to_be_deleted = True
|
||||
obj = new_obj
|
||||
obj.hide_set(True)
|
||||
obj.asset_mark()
|
||||
obj.asset_generate_preview()
|
||||
blender33_or_above = bpy.app.version >= (3, 3, 0)
|
||||
interval = 1e-4
|
||||
|
||||
def wait_for_asset_previews_generation(check_interval_seconds=interval):
|
||||
if blender33_or_above and bpy.app.is_job_running("RENDER_PREVIEW"):
|
||||
return check_interval_seconds
|
||||
else:
|
||||
if ifc_class not in cls.props.constr_classes:
|
||||
cls.props.constr_classes.add().name = ifc_class
|
||||
constr_class_info = cls.props.constr_classes[ifc_class]
|
||||
# relating_type = cls.relating_type_name_by_id(ifc_class, relating_type_id)
|
||||
if str(relating_type_id) not in constr_class_info.constr_types:
|
||||
constr_class_info.constr_types.add().name = str(relating_type_id)
|
||||
relating_type_info = constr_class_info.constr_types[str(relating_type_id)]
|
||||
relating_type_info.object = obj
|
||||
relating_type_info.icon_id = obj.preview.icon_id
|
||||
|
||||
if to_be_deleted:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element:
|
||||
tool.Ifc.delete(element)
|
||||
tool.Ifc.unlink(obj=obj)
|
||||
for collection in obj.users_collection:
|
||||
collection.objects.unlink(obj)
|
||||
return None
|
||||
|
||||
first_interval = 0 if blender33_or_above else interval
|
||||
bpy.app.timers.register(wait_for_asset_previews_generation, first_interval=first_interval)
|
||||
|
||||
@classmethod
|
||||
def assetize_relating_type_from_selection(cls, browser=False):
|
||||
ifc_class = cls.props.ifc_class_browser if browser else cls.props.ifc_class
|
||||
relating_type_id = cls.props.relating_type_id_browser if browser else cls.props.relating_type_id
|
||||
constr_class_occurrences = cls.constr_class_entities(ifc_class=ifc_class)
|
||||
constr_class_occurrences = [
|
||||
entity for entity in constr_class_occurrences if entity.id() == int(relating_type_id)
|
||||
]
|
||||
if len(constr_class_occurrences) == 0:
|
||||
raise ConstrTypeEntityNotFound()
|
||||
constr_class_entity = constr_class_occurrences[0]
|
||||
if (obj := tool.Ifc.get_object(constr_class_entity)) is None:
|
||||
raise ConstrTypeEntityNotFound()
|
||||
cls.assetize_object(obj, ifc_class, constr_class_entity, from_selection=True)
|
||||
|
||||
@staticmethod
|
||||
def constr_class_info(ifc_class):
|
||||
props = bpy.context.scene.BIMModelProperties
|
||||
return props.constr_classes[ifc_class] if ifc_class in props.constr_classes else None
|
||||
|
||||
@classmethod
|
||||
def new_relating_type(cls, ifc_class=None, relating_type_id=None):
|
||||
if ifc_class is None:
|
||||
bpy.ops.bim.add_constr_type_instance(
|
||||
ifc_class=cls.props.ifc_class, relating_type_id=int(cls.props.relating_type_id)
|
||||
)
|
||||
else:
|
||||
cls.props.updating = True
|
||||
cls.props.ifc_class = ifc_class
|
||||
cls.props.relating_type_id = str(relating_type_id)
|
||||
cls.props.updating = False
|
||||
bpy.ops.bim.add_constr_type_instance()
|
||||
return bpy.context.selected_objects[-1]
|
||||
|
||||
@staticmethod
|
||||
def relating_type_name_by_id(ifc_class, relating_type_id):
|
||||
file = IfcStore.get_file()
|
||||
try:
|
||||
constr_class_entity = file.by_id(int(relating_type_id))
|
||||
except (RuntimeError, ValueError):
|
||||
return None
|
||||
return constr_class_entity.Name if constr_class_entity.is_a() == ifc_class else None
|
||||
|
||||
@classmethod
|
||||
def relating_type_id_by_name(cls, ifc_class, relating_type):
|
||||
relating_types = [ct[0] for ct in cls.relating_types(ifc_class=ifc_class) if ct[1] == relating_type]
|
||||
return None if len(relating_types) == 0 else relating_types[0]
|
||||
|
||||
|
||||
class ArrayData:
|
||||
data = {}
|
||||
|
||||
@@ -17,25 +17,22 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import gpu
|
||||
import bgl
|
||||
import bmesh
|
||||
import blenderbim.tool as tool
|
||||
from math import pi, degrees, sin, cos, radians
|
||||
from math import sin, cos, radians
|
||||
from bpy.types import SpaceView3D
|
||||
from mathutils import Vector, Matrix
|
||||
from gpu.types import GPUShader, GPUBatch, GPUIndexBuf, GPUVertBuf, GPUVertFormat
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
|
||||
|
||||
white = (1, 1, 1, 1)
|
||||
lightgrey = (0.7, 0.7, 0.7, 1)
|
||||
green = (0.545, 0.863, 0, 1)
|
||||
red = (1, 0.2, 0.322, 1)
|
||||
blue = (0.157, 0.565, 1, 1)
|
||||
grey = (0.2, 0.2, 0.2, 1)
|
||||
ERROR_ELEMENTS_COLOR = (1, 0.2, 0.322, 1) # RED
|
||||
UNSPECIAL_ELEMENT_COLOR = (0.2, 0.2, 0.2, 1) # GREY
|
||||
|
||||
faces_color = (0.494, 0.540, 0.593, 1)
|
||||
preview_edges_color = (0.130, 0.141, 0.371, 1)
|
||||
|
||||
def transparent_color(color, alpha=0.1):
|
||||
color = [i for i in color]
|
||||
color[3] = alpha
|
||||
return color
|
||||
|
||||
|
||||
def bm_check_vertex_in_groups(vertex, deform_layer, groups):
|
||||
@@ -71,13 +68,11 @@ class ProfileDecorator:
|
||||
pass
|
||||
cls.installed = None
|
||||
|
||||
def create_batch(self, shader_type, content_pos, color, indices=None, bind=True):
|
||||
batch = batch_for_shader(self.shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
# TODO: what's bind is for?
|
||||
if bind:
|
||||
self.shader.bind()
|
||||
self.shader.uniform_float("color", color)
|
||||
batch.draw(self.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_faces(self, bm, vertices_coords):
|
||||
"""mutates original bm (triangulates it)
|
||||
@@ -87,9 +82,15 @@ class ProfileDecorator:
|
||||
bmesh.ops.triangulate(traingulated_bm, faces=traingulated_bm.faces)
|
||||
|
||||
face_indices = [[v.index for v in f.verts] for f in traingulated_bm.faces]
|
||||
self.create_batch("TRIS", vertices_coords, faces_color, face_indices)
|
||||
faces_color = transparent_color(self.addon_prefs.decorator_color_special)
|
||||
self.draw_batch("TRIS", vertices_coords, faces_color, face_indices)
|
||||
|
||||
def __call__(self, context, get_custom_bmesh=None, draw_faces=False, exit_edit_mode_callback=None):
|
||||
self.addon_prefs = context.preferences.addons["blenderbim"].preferences
|
||||
selected_elements_color = self.addon_prefs.decorator_color_selected
|
||||
unselected_elements_color = self.addon_prefs.decorator_color_unselected
|
||||
special_elements_color = self.addon_prefs.decorator_color_special
|
||||
|
||||
obj = context.active_object
|
||||
|
||||
if obj.mode != "EDIT":
|
||||
@@ -103,20 +104,8 @@ class ProfileDecorator:
|
||||
else:
|
||||
bm = bmesh.from_edit_mesh(obj.data)
|
||||
|
||||
def gl_init(use_bgl=False):
|
||||
# TODO: remove as deprecated?
|
||||
if use_bgl:
|
||||
bgl.glLineWidth(2)
|
||||
bgl.glPointSize(6)
|
||||
bgl.glEnable(bgl.GL_BLEND)
|
||||
bgl.glEnable(bgl.GL_LINE_SMOOTH)
|
||||
else:
|
||||
gpu.state.line_width_set(2)
|
||||
gpu.state.point_size_set(6)
|
||||
gpu.state.blend_set("ALPHA")
|
||||
bgl.glEnable(bgl.GL_LINE_SMOOTH)
|
||||
|
||||
gl_init(True)
|
||||
gpu.state.point_size_set(6)
|
||||
gpu.state.blend_set("ALPHA")
|
||||
|
||||
### Actually drawing
|
||||
all_vertices = []
|
||||
@@ -202,22 +191,31 @@ class ProfileDecorator:
|
||||
unselected_edges.append(edge_indices)
|
||||
|
||||
### Actually drawing
|
||||
# 3D_POLYLINE_UNIFORM_COLOR is good for smoothed lines since `bgl.enable(GL_LINE_SMOOTH)` is deprecated
|
||||
self.line_shader = gpu.shader.from_builtin("3D_POLYLINE_UNIFORM_COLOR")
|
||||
self.line_shader.bind()
|
||||
# POLYLINE_UNIFORM_COLOR specific uniforms
|
||||
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
|
||||
self.line_shader.uniform_float("lineWidth", 2.0)
|
||||
|
||||
# general shader
|
||||
self.shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
|
||||
self.shader.bind()
|
||||
|
||||
# Draw faces
|
||||
if draw_faces:
|
||||
self.draw_faces(bm, all_vertices)
|
||||
|
||||
self.create_batch("LINES", all_vertices, lightgrey, unselected_edges)
|
||||
self.create_batch("LINES", all_vertices, green, selected_edges)
|
||||
self.create_batch("LINES", all_vertices, grey, arc_edges)
|
||||
self.create_batch("LINES", all_vertices, preview_edges_color, preview_edges)
|
||||
self.create_batch("LINES", all_vertices, blue, roof_angle_edges)
|
||||
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, UNSPECIAL_ELEMENT_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.create_batch("POINTS", unselected_vertices, lightgrey)
|
||||
self.create_batch("POINTS", error_vertices, red)
|
||||
self.create_batch("POINTS", special_vertices, blue)
|
||||
self.create_batch("POINTS", selected_vertices, green)
|
||||
self.draw_batch("POINTS", unselected_vertices, transparent_color(unselected_elements_color, 0.5))
|
||||
self.draw_batch("POINTS", error_vertices, ERROR_ELEMENTS_COLOR)
|
||||
self.draw_batch("POINTS", special_vertices, special_elements_color)
|
||||
self.draw_batch("POINTS", selected_vertices, selected_elements_color)
|
||||
|
||||
# Draw arcs
|
||||
arc_centroids = []
|
||||
@@ -242,9 +240,9 @@ class ProfileDecorator:
|
||||
arc_centroids.append(tuple(centroid))
|
||||
arc_segments.append(tool.Cad.create_arc_segments(pts=points, num_verts=17, make_edges=True))
|
||||
|
||||
self.create_batch("POINTS", arc_centroids, grey, bind=False)
|
||||
self.draw_batch("POINTS", arc_centroids, UNSPECIAL_ELEMENT_COLOR)
|
||||
for verts, edges in arc_segments:
|
||||
self.create_batch("LINES", verts, blue, edges, bind=False)
|
||||
self.draw_batch("LINES", verts, special_elements_color, edges)
|
||||
|
||||
# Draw circles
|
||||
circle_centroids = []
|
||||
@@ -263,9 +261,9 @@ class ProfileDecorator:
|
||||
segments = [[list(matrix @ Vector(v)) for v in segments[0]], segments[1]]
|
||||
circle_segments.append(segments)
|
||||
|
||||
self.create_batch("POINTS", circle_centroids, grey, bind=False)
|
||||
self.draw_batch("POINTS", circle_centroids, UNSPECIAL_ELEMENT_COLOR)
|
||||
for verts, edges in circle_segments:
|
||||
self.create_batch("LINES", verts, blue, edges, bind=False)
|
||||
self.draw_batch("LINES", verts, special_elements_color, edges)
|
||||
|
||||
def create_matrix(self, p, x, y, z):
|
||||
return Matrix([x, y, z, p]).to_4x4().transposed()
|
||||
|
||||
@@ -43,6 +43,7 @@ def update_door_modifier_representation(context):
|
||||
props = obj.BIMDoorProperties
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
ifc_file = tool.Ifc.get()
|
||||
sliding_door = "SLIDING" in props.door_type
|
||||
|
||||
representation_data = {
|
||||
"operation_type": props.door_type,
|
||||
@@ -87,13 +88,37 @@ def update_door_modifier_representation(context):
|
||||
model_representation = ifcopenshell.api.run("geometry.add_door_representation", ifc_file, **representation_data)
|
||||
tool.Model.replace_object_ifc_representation(body, obj, model_representation)
|
||||
|
||||
# PLAN_VIEW representation
|
||||
plan = ifcopenshell.util.representation.get_context(ifc_file, "Plan", "Body", "PLAN_VIEW")
|
||||
if plan:
|
||||
representation_data["context"] = plan
|
||||
# Body/PLAN_VIEW representation
|
||||
plan_body = ifcopenshell.util.representation.get_context(ifc_file, "Plan", "Body", "PLAN_VIEW")
|
||||
if plan_body:
|
||||
representation_data["context"] = plan_body
|
||||
plan_representation = ifcopenshell.api.run("geometry.add_door_representation", ifc_file, **representation_data)
|
||||
tool.Model.replace_object_ifc_representation(plan, obj, plan_representation)
|
||||
tool.Model.replace_object_ifc_representation(plan_body, obj, plan_representation)
|
||||
|
||||
# Annotation/PLAN_VIEW representation
|
||||
plan_annotation = ifcopenshell.util.representation.get_context(ifc_file, "Plan", "Annotation", "PLAN_VIEW")
|
||||
if plan_annotation:
|
||||
if not sliding_door:
|
||||
# only sliding doors have Annotation/PLAN_VIEW
|
||||
# for other types we just check for old representation and remove it if it's there
|
||||
old_representation = ifcopenshell.util.representation.get_representation(
|
||||
element, "Plan", "Annotation", "PLAN_VIEW"
|
||||
)
|
||||
if old_representation:
|
||||
core.remove_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=old_representation,
|
||||
)
|
||||
else:
|
||||
representation_data["context"] = plan_annotation
|
||||
plan_representation = ifcopenshell.api.run(
|
||||
"geometry.add_door_representation", ifc_file, **representation_data
|
||||
)
|
||||
tool.Model.replace_object_ifc_representation(plan_annotation, obj, plan_representation)
|
||||
|
||||
if plan_body or plan_annotation:
|
||||
# adding switch representation at the end instead of changing order of representations
|
||||
# to prevent #2744
|
||||
core.switch_representation(
|
||||
@@ -263,13 +288,17 @@ def update_door_modifier_bmesh(context):
|
||||
door_type = props.door_type
|
||||
double_swing_door = "DOUBLE_SWING" in door_type
|
||||
double_door = "DOUBLE_DOOR" in door_type
|
||||
sliding_door = "SLIDING" in door_type
|
||||
|
||||
# lining params
|
||||
lining_depth = props.lining_depth * si_conversion
|
||||
lining_thickness_default = props.lining_thickness * si_conversion
|
||||
lining_offset = props.lining_offset * si_conversion
|
||||
lining_to_panel_offset_x = props.lining_to_panel_offset_x * si_conversion
|
||||
lining_to_panel_offset_y = props.lining_to_panel_offset_y * si_conversion
|
||||
lining_to_panel_offset_x = (
|
||||
props.lining_to_panel_offset_x * si_conversion if not sliding_door else lining_thickness_default
|
||||
)
|
||||
panel_depth = props.panel_depth * si_conversion
|
||||
lining_to_panel_offset_y = props.lining_to_panel_offset_y * si_conversion if not sliding_door else -panel_depth
|
||||
|
||||
transom_thickness = props.transom_thickness * si_conversion / 2
|
||||
transfom_offset = props.transom_offset * si_conversion
|
||||
@@ -278,10 +307,10 @@ def update_door_modifier_bmesh(context):
|
||||
window_lining_height = overall_height - transfom_offset - transom_thickness
|
||||
|
||||
side_lining_thickness = lining_thickness_default
|
||||
panel_lining_overlap_x = max(lining_thickness_default - lining_to_panel_offset_x, 0)
|
||||
panel_lining_overlap_x = max(lining_thickness_default - lining_to_panel_offset_x, 0) if not sliding_door else 0
|
||||
|
||||
top_lining_thickness = transom_thickness or lining_thickness_default
|
||||
panel_top_lining_overlap_x = max(top_lining_thickness - lining_to_panel_offset_x, 0)
|
||||
panel_top_lining_overlap_x = max(top_lining_thickness - lining_to_panel_offset_x, 0) if not sliding_door else 0
|
||||
door_opening_width = overall_width - lining_to_panel_offset_x * 2
|
||||
if double_swing_door:
|
||||
side_lining_thickness = side_lining_thickness - panel_lining_overlap_x
|
||||
@@ -296,7 +325,6 @@ def update_door_modifier_bmesh(context):
|
||||
casing_depth = props.casing_depth * si_conversion
|
||||
|
||||
# panel params
|
||||
panel_depth = props.panel_depth * si_conversion
|
||||
panel_width = door_opening_width * props.panel_width_ratio
|
||||
frame_depth = props.frame_depth * si_conversion
|
||||
frame_thickness = props.frame_thickness * si_conversion
|
||||
@@ -389,7 +417,7 @@ def update_door_modifier_bmesh(context):
|
||||
panel_position = V(lining_to_panel_offset_x, lining_to_panel_offset_y, threshold_thickness)
|
||||
|
||||
if double_door:
|
||||
# TODO: keep a little space between doors for readibility?
|
||||
# keeping a little space between doors for readibility
|
||||
double_door_offset = 0.001 * si_conversion
|
||||
panel_size.x = panel_size.x / 2 - double_door_offset
|
||||
door_verts.extend(create_bm_door_panel(panel_size, panel_position, "LEFT"))
|
||||
@@ -516,7 +544,7 @@ class AddDoor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
class CancelEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.cancel_editing_door"
|
||||
bl_label = "Cancel editing Door"
|
||||
bl_label = "Cancel Editing Door"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def _execute(self, context):
|
||||
@@ -545,7 +573,7 @@ class CancelEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
class FinishEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.finish_editing_door"
|
||||
bl_label = "Finish editing door"
|
||||
bl_label = "Finish Editing Door"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def _execute(self, context):
|
||||
|
||||
@@ -60,9 +60,6 @@ def load_post(*args):
|
||||
product.regenerate_profile_usage,
|
||||
)
|
||||
|
||||
IfcStore.add_element_listener(wall.element_listener)
|
||||
IfcStore.add_element_listener(profile.element_listener)
|
||||
|
||||
ifcopenshell.api.add_post_listener(
|
||||
"geometry.add_representation", "BlenderBIM.DumbWall.CalculateQuantities", wall.calculate_quantities
|
||||
)
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
|
||||
import bpy
|
||||
import gpu
|
||||
import bgl
|
||||
import bmesh
|
||||
import logging
|
||||
import numpy as np
|
||||
@@ -36,7 +35,6 @@ from bpy.types import Operator
|
||||
from bpy.types import SpaceView3D
|
||||
from bpy.props import FloatProperty
|
||||
from bpy_extras.object_utils import AddObjectHelper, object_data_add
|
||||
from gpu.types import GPUShader, GPUBatch, GPUIndexBuf, GPUVertBuf, GPUVertFormat
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
|
||||
|
||||
@@ -217,8 +215,9 @@ class FilledOpeningGenerator:
|
||||
|
||||
def generate_opening_from_filling(self, filling, filling_obj, voided_obj):
|
||||
thickness = voided_obj.dimensions[1] + 0.1 + 0.1
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
shape_builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
|
||||
ifc_file = tool.Ifc.get()
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
|
||||
shape_builder = ifcopenshell.util.shape_builder.ShapeBuilder(ifc_file)
|
||||
|
||||
profile = None
|
||||
filling_type = ifcopenshell.util.element.get_type(filling)
|
||||
@@ -227,47 +226,59 @@ class FilledOpeningGenerator:
|
||||
filling_type, "Model", "Profile", "ELEVATION_VIEW"
|
||||
)
|
||||
filling_obj = tool.Ifc.get_object(filling_type)
|
||||
context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
|
||||
context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
|
||||
|
||||
if profile:
|
||||
curve_3d = ifcopenshell.util.representation.resolve_representation(profile).Items[0]
|
||||
|
||||
def get_curve_2d_from_3d(curve_3d):
|
||||
ifc_segments = [shape_builder.deep_copy(s) for s in curve_3d.Segments]
|
||||
ifc_points = ifc_file.createIfcCartesianPointList2D([Vector(p).xz for p in curve_3d.Points.CoordList])
|
||||
ifc_curve = ifc_file.createIfcIndexedPolyCurve(Points=ifc_points, Segments=ifc_segments)
|
||||
return ifc_curve
|
||||
|
||||
extrusion = shape_builder.extrude(
|
||||
ifcopenshell.util.representation.resolve_representation(profile).Items[0],
|
||||
get_curve_2d_from_3d(curve_3d),
|
||||
magnitude=thickness / unit_scale,
|
||||
position=Vector([0.0, -0.1 / unit_scale, 0.0]),
|
||||
extrusion_vector=Vector([0.0, 1.0, 0.0]),
|
||||
position_x_axis=Vector((1, 0, 0)),
|
||||
position_z_axis=Vector((0, -1, 0)),
|
||||
extrusion_vector=Vector((0, 0, -1)),
|
||||
)
|
||||
return shape_builder.get_representation(context, [extrusion])
|
||||
|
||||
x, y, z = filling_obj.dimensions
|
||||
opening_position = Vector([0.0, -0.1 / unit_scale, 0.0])
|
||||
opening_size = Vector([x, 0, z]) / unit_scale
|
||||
opening_size = Vector([x, z]) / unit_scale
|
||||
|
||||
# Windows and doors can have a casing that overlaps the wall
|
||||
# but shouldn't affect the size of the opening.
|
||||
# So we shouldn't use object dimensions in that case. More: #2784
|
||||
# Just keeping it for windows and doors for now to be safe
|
||||
x_redefined, z_redefined = False, False
|
||||
has_width_attribute, has_height_attribute = False, False
|
||||
if filling.is_a() in ["IfcWindow", "IfcDoor"]:
|
||||
if filling.OverallWidth:
|
||||
opening_size.x = filling.OverallWidth
|
||||
x_redefined = True
|
||||
has_width_attribute = True
|
||||
if filling.OverallHeight:
|
||||
opening_size.z = filling.OverallHeight
|
||||
z_redefined = True
|
||||
opening_size.y = filling.OverallHeight
|
||||
has_height_attribute = True
|
||||
|
||||
# making sure if min_x or min_z != 0 to shift the opening accordingly
|
||||
# to prevent something like #2784
|
||||
if not x_redefined:
|
||||
if not has_width_attribute:
|
||||
opening_position.x = min(v[0] for v in filling_obj.bound_box)
|
||||
|
||||
if not z_redefined:
|
||||
if not has_height_attribute:
|
||||
opening_position.z = min(v[2] for v in filling_obj.bound_box)
|
||||
|
||||
extrusion = shape_builder.extrude(
|
||||
shape_builder.rectangle(size=opening_size),
|
||||
magnitude=thickness / unit_scale,
|
||||
position=opening_position,
|
||||
extrusion_vector=Vector([0.0, 1.0, 0.0]),
|
||||
position_z_axis=Vector((0.0, -1.0, 0.0)),
|
||||
position_x_axis=Vector((1.0, 0.0, 0.0)),
|
||||
extrusion_vector=Vector((0.0, 0.0, -1.0)),
|
||||
)
|
||||
|
||||
return shape_builder.get_representation(context, [extrusion])
|
||||
@@ -325,15 +336,13 @@ class RecalculateFill(bpy.types.Operator, tool.Ifc.Operator):
|
||||
for building_element in decomposed_building_elements:
|
||||
building_obj = tool.Ifc.get_object(building_element)
|
||||
if building_obj and building_obj.data:
|
||||
body = ifcopenshell.util.representation.get_representation(
|
||||
building_element, "Model", "Body", "MODEL_VIEW"
|
||||
)
|
||||
if body:
|
||||
representation = tool.Geometry.get_active_representation(building_obj)
|
||||
if representation:
|
||||
blenderbim.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=building_obj,
|
||||
representation=body,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
@@ -648,7 +657,7 @@ class ShowOpenings(Operator, tool.Ifc.Operator):
|
||||
props = bpy.context.scene.BIMModelProperties
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
if not element or not getattr(element, "HasOpenings", None):
|
||||
continue
|
||||
if tool.Ifc.is_moved(obj):
|
||||
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
||||
@@ -673,7 +682,6 @@ class HideOpenings(Operator, tool.Ifc.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
props = bpy.context.scene.BIMModelProperties
|
||||
to_delete = set()
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
@@ -685,7 +693,7 @@ class HideOpenings(Operator, tool.Ifc.Operator):
|
||||
if opening_obj:
|
||||
to_delete.add(opening_obj)
|
||||
for opening_obj in to_delete:
|
||||
tool.Ifc.unlink(element=opening, obj=opening_obj)
|
||||
tool.Ifc.unlink(obj=opening_obj)
|
||||
bpy.data.objects.remove(opening_obj)
|
||||
tool.Model.clear_scene_openings()
|
||||
return {"FINISHED"}
|
||||
@@ -709,13 +717,12 @@ class EditOpenings(Operator, tool.Ifc.Operator):
|
||||
if opening_obj:
|
||||
if tool.Ifc.is_edited(opening_obj):
|
||||
tool.Geometry.run_geometry_update_representation(obj=opening_obj)
|
||||
building_objs.add(obj)
|
||||
building_objs.update(self.get_all_building_objects_of_similar_openings(opening))
|
||||
elif tool.Ifc.is_moved(opening_obj):
|
||||
blenderbim.core.geometry.edit_object_placement(
|
||||
tool.Ifc, tool.Geometry, tool.Surveyor, obj=opening_obj
|
||||
)
|
||||
building_objs.add(obj)
|
||||
building_objs.add(obj)
|
||||
building_objs.update(self.get_all_building_objects_of_similar_openings(opening))
|
||||
tool.Ifc.unlink(element=opening, obj=opening_obj)
|
||||
bpy.data.objects.remove(opening_obj)
|
||||
|
||||
@@ -759,6 +766,7 @@ class EditOpenings(Operator, tool.Ifc.Operator):
|
||||
return results
|
||||
|
||||
|
||||
# TODO: merge with ProfileDecorator?
|
||||
class DecorationsHandler:
|
||||
installed = None
|
||||
|
||||
@@ -777,27 +785,38 @@ class DecorationsHandler:
|
||||
pass
|
||||
cls.installed = None
|
||||
|
||||
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 __call__(self, context):
|
||||
bgl.glLineWidth(2)
|
||||
bgl.glPointSize(6)
|
||||
bgl.glEnable(bgl.GL_BLEND)
|
||||
bgl.glEnable(bgl.GL_LINE_SMOOTH)
|
||||
self.addon_prefs = context.preferences.addons["blenderbim"].preferences
|
||||
selected_elements_color = self.addon_prefs.decorator_color_selected
|
||||
unselected_elements_color = self.addon_prefs.decorator_color_unselected
|
||||
special_elements_color = self.addon_prefs.decorator_color_special
|
||||
|
||||
def transparent_color(color, alpha=0.1):
|
||||
color = [i for i in color]
|
||||
color[3] = alpha
|
||||
return color
|
||||
|
||||
gpu.state.point_size_set(6)
|
||||
gpu.state.blend_set("ALPHA")
|
||||
|
||||
for opening in context.scene.BIMModelProperties.openings:
|
||||
obj = opening.obj
|
||||
|
||||
if not obj:
|
||||
continue
|
||||
|
||||
white = (1, 1, 1, 1)
|
||||
white_t = (1, 1, 1, 0.1)
|
||||
green = (0.545, 0.863, 0, 1)
|
||||
red = (1, 0.2, 0.322, 1)
|
||||
red_t = (1, 0.2, 0.322, 0.1)
|
||||
blue = (0.157, 0.565, 1, 1)
|
||||
blue_t = (0.157, 0.565, 1, 0.1)
|
||||
grey = (0.2, 0.2, 0.2, 1)
|
||||
self.line_shader = gpu.shader.from_builtin("3D_POLYLINE_UNIFORM_COLOR")
|
||||
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", 2.0)
|
||||
|
||||
# general shader
|
||||
self.shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
|
||||
|
||||
verts = []
|
||||
@@ -829,41 +848,22 @@ class DecorationsHandler:
|
||||
else:
|
||||
unselected_edges.append(edge_indices)
|
||||
|
||||
batch = batch_for_shader(self.shader, "LINES", {"pos": verts}, indices=unselected_edges)
|
||||
self.shader.bind()
|
||||
self.shader.uniform_float("color", white)
|
||||
batch.draw(self.shader)
|
||||
|
||||
batch = batch_for_shader(self.shader, "LINES", {"pos": verts}, indices=selected_edges)
|
||||
self.shader.uniform_float("color", green)
|
||||
batch.draw(self.shader)
|
||||
|
||||
batch = batch_for_shader(self.shader, "POINTS", {"pos": unselected_vertices})
|
||||
self.shader.uniform_float("color", white)
|
||||
batch.draw(self.shader)
|
||||
|
||||
batch = batch_for_shader(self.shader, "POINTS", {"pos": selected_vertices})
|
||||
self.shader.uniform_float("color", green)
|
||||
batch.draw(self.shader)
|
||||
self.draw_batch("LINES", verts, transparent_color(unselected_elements_color, 0.5), unselected_edges)
|
||||
self.draw_batch("LINES", verts, selected_elements_color, selected_edges)
|
||||
self.draw_batch("POINTS", unselected_vertices, unselected_elements_color)
|
||||
self.draw_batch("POINTS", selected_vertices, selected_elements_color)
|
||||
else:
|
||||
bm = bmesh.new()
|
||||
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]
|
||||
|
||||
batch = batch_for_shader(self.shader, "LINES", {"pos": verts}, indices=edges)
|
||||
self.shader.bind()
|
||||
self.shader.uniform_float("color", green if obj in context.selected_objects else blue)
|
||||
batch.draw(self.shader)
|
||||
color = selected_elements_color if obj in context.selected_objects else special_elements_color
|
||||
self.draw_batch("LINES", verts, color, edges)
|
||||
|
||||
obj.data.calc_loop_triangles()
|
||||
tris = [tuple(t.vertices) for t in obj.data.loop_triangles]
|
||||
|
||||
batch = batch_for_shader(self.shader, "TRIS", {"pos": verts}, indices=tris)
|
||||
self.shader.bind()
|
||||
self.shader.uniform_float("color", blue_t)
|
||||
batch.draw(self.shader)
|
||||
self.draw_batch("TRIS", verts, transparent_color(special_elements_color), tris)
|
||||
|
||||
if "HalfSpaceSolid" in obj.name:
|
||||
# Arrow shape
|
||||
@@ -876,10 +876,8 @@ class DecorationsHandler:
|
||||
tuple(obj.matrix_world @ Vector((0, -0.05, 0.45))),
|
||||
]
|
||||
edges = [(0, 1), (1, 2), (1, 3), (1, 4), (1, 5)]
|
||||
batch = batch_for_shader(self.shader, "LINES", {"pos": verts}, indices=edges)
|
||||
self.shader.bind()
|
||||
self.shader.uniform_float("color", green if obj in context.selected_objects else blue)
|
||||
batch.draw(self.shader)
|
||||
color = selected_elements_color if obj in context.selected_objects else special_elements_color
|
||||
self.draw_batch("LINES", verts, color, edges)
|
||||
|
||||
if obj.mode != "EDIT":
|
||||
bm.free()
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import bpy
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.core.spatial
|
||||
import blenderbim.core.aggregate
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
|
||||
|
||||
@@ -75,6 +76,22 @@ class PieUpdateContainer(bpy.types.Operator):
|
||||
break
|
||||
return {"FINISHED"}
|
||||
|
||||
class PieAssignObjectAggregation(bpy.types.Operator,tool.Ifc.Operator):
|
||||
bl_idname = "bim.pie_assign_object_aggregation"
|
||||
bl_label = "Assign Parts to Active Object"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
for obj in context.selected_objects:
|
||||
if obj == context.active_object:
|
||||
continue
|
||||
blenderbim.core.aggregate.assign_object(
|
||||
tool.Ifc,
|
||||
tool.Aggregate,
|
||||
tool.Collector,
|
||||
relating_obj=context.active_object,
|
||||
related_obj=obj,
|
||||
)
|
||||
|
||||
class VIEW3D_MT_PIE_bim(bpy.types.Menu):
|
||||
bl_label = "Geometry"
|
||||
@@ -86,6 +103,7 @@ class VIEW3D_MT_PIE_bim(bpy.types.Menu):
|
||||
pie.operator("bim.pie_add_opening")
|
||||
pie.operator("bim.pie_update_container")
|
||||
pie.operator("bim.open_pie_class", text="Assign IFC Class")
|
||||
pie.operator("bim.pie_assign_object_aggregation", text ="Assign Aggregation")
|
||||
|
||||
|
||||
class VIEW3D_MT_PIE_bim_class(bpy.types.Menu):
|
||||
|
||||
@@ -37,12 +37,6 @@ from . import prop
|
||||
import json
|
||||
|
||||
|
||||
def select_and_activate_single_object(context, obj):
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
context.view_layer.objects.active = obj
|
||||
obj.select_set(True)
|
||||
|
||||
|
||||
class AddEmptyType(bpy.types.Operator, AddObjectHelper):
|
||||
bl_idname = "bim.add_empty_type"
|
||||
bl_label = "Add Empty Type"
|
||||
@@ -52,7 +46,7 @@ class AddEmptyType(bpy.types.Operator, AddObjectHelper):
|
||||
obj = bpy.data.objects.new("TYPEX", None)
|
||||
context.scene.collection.objects.link(obj)
|
||||
context.scene.BIMRootProperties.ifc_product = "IfcElementType"
|
||||
select_and_activate_single_object(context, obj)
|
||||
tool.Blender.select_and_activate_single_object(context, obj)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -97,7 +91,7 @@ class AddConstrTypeInstance(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
elif material and material.is_a("IfcMaterialLayerSet"):
|
||||
if self.generate_layered_element(ifc_class, relating_type):
|
||||
select_and_activate_single_object(context, context.selected_objects[-1])
|
||||
tool.Blender.select_and_activate_single_object(context, context.selected_objects[-1])
|
||||
return {"FINISHED"}
|
||||
if relating_type.is_a("IfcFlowSegmentType") and not relating_type.RepresentationMaps:
|
||||
if mep.MepGenerator(relating_type).generate():
|
||||
@@ -179,7 +173,7 @@ class AddConstrTypeInstance(bpy.types.Operator):
|
||||
if ifc_class == "IfcDoorType" and len(context.selected_objects) >= 1:
|
||||
pass
|
||||
else:
|
||||
select_and_activate_single_object(context, obj)
|
||||
tool.Blender.select_and_activate_single_object(context, obj)
|
||||
return {"FINISHED"}
|
||||
|
||||
@staticmethod
|
||||
@@ -402,7 +396,7 @@ class MirrorElements(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.mirror_elements"
|
||||
bl_label = "Mirror Elements"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Faux-mirrors the selected object by an active empty along a mirror plane."
|
||||
bl_description = "Faux-mirrors the selected objects by an active empty along a mirror plane"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@@ -429,6 +423,13 @@ class MirrorElements(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
mirror.select_set(False)
|
||||
|
||||
if not context.selected_objects:
|
||||
self.report(
|
||||
{"INFO"},
|
||||
"At least two objects must be selected: an object to be mirrored, and a mirror axis as the active object.",
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
bpy.ops.bim.override_object_duplicate_move(is_interactive=False)
|
||||
|
||||
for obj in context.selected_objects:
|
||||
|
||||
@@ -31,64 +31,10 @@ import blenderbim.core.geometry
|
||||
from math import pi, degrees, inf
|
||||
from mathutils import Vector, Matrix, Quaternion
|
||||
from blenderbim.bim.module.geometry.helper import Helper
|
||||
from blenderbim.bim.module.model.wall import DumbWallRecalculator
|
||||
from blenderbim.bim.module.model.decorator import ProfileDecorator
|
||||
|
||||
|
||||
def element_listener(element, obj):
|
||||
blenderbim.bim.handler.subscribe_to(obj, "mode", mode_callback)
|
||||
|
||||
|
||||
def mode_callback(obj, data):
|
||||
for obj in set(bpy.context.selected_objects + [bpy.context.active_object]):
|
||||
if (
|
||||
not obj.data
|
||||
or not isinstance(obj.data, (bpy.types.Mesh, bpy.types.Curve, bpy.types.TextCurve))
|
||||
or not obj.BIMObjectProperties.ifc_definition_id
|
||||
or not bpy.context.scene.BIMProjectProperties.is_authoring
|
||||
):
|
||||
return
|
||||
product = tool.Ifc.get().by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
parametric = ifcopenshell.util.element.get_psets(product).get("EPset_Parametric")
|
||||
if not parametric or parametric["Engine"] != "BlenderBIM.DumbProfile":
|
||||
return
|
||||
if obj.mode == "EDIT":
|
||||
tool.Ifc.edit(obj)
|
||||
bm = bmesh.from_edit_mesh(obj.data)
|
||||
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
|
||||
bmesh.update_edit_mesh(obj.data)
|
||||
else:
|
||||
material_usage = ifcopenshell.util.element.get_material(product)
|
||||
x, y = obj.dimensions[0:2]
|
||||
if not material_usage.CardinalPoint:
|
||||
new_origin = obj.matrix_world @ (Vector(obj.bound_box[0]) + (Vector((x, y, 0)) / 2))
|
||||
elif material_usage.CardinalPoint == 1:
|
||||
new_origin = obj.matrix_world @ Vector(obj.bound_box[4])
|
||||
elif material_usage.CardinalPoint == 2:
|
||||
new_origin = obj.matrix_world @ (Vector(obj.bound_box[0]) + (Vector((x, 0, 0)) / 2))
|
||||
elif material_usage.CardinalPoint == 3:
|
||||
new_origin = obj.matrix_world @ Vector(obj.bound_box[0])
|
||||
elif material_usage.CardinalPoint == 4:
|
||||
new_origin = obj.matrix_world @ (Vector(obj.bound_box[4]) + (Vector((0, y, 0)) / 2))
|
||||
elif material_usage.CardinalPoint == 5:
|
||||
new_origin = obj.matrix_world @ (Vector(obj.bound_box[0]) + (Vector((x, y, 0)) / 2))
|
||||
elif material_usage.CardinalPoint == 6:
|
||||
new_origin = obj.matrix_world @ (Vector(obj.bound_box[0]) + (Vector((0, y, 0)) / 2))
|
||||
elif material_usage.CardinalPoint == 7:
|
||||
new_origin = obj.matrix_world @ Vector(obj.bound_box[7])
|
||||
elif material_usage.CardinalPoint == 8:
|
||||
new_origin = obj.matrix_world @ (Vector(obj.bound_box[3]) + (Vector((x, 0, 0)) / 2))
|
||||
elif material_usage.CardinalPoint == 9:
|
||||
new_origin = obj.matrix_world @ Vector(obj.bound_box[3])
|
||||
if (obj.matrix_world.translation - new_origin).length < 0.001:
|
||||
return
|
||||
obj.data.transform(
|
||||
Matrix.Translation(
|
||||
(obj.matrix_world.inverted().to_quaternion() @ (obj.matrix_world.translation - new_origin))
|
||||
)
|
||||
)
|
||||
obj.matrix_world.translation = new_origin
|
||||
|
||||
|
||||
class DumbProfileGenerator:
|
||||
def __init__(self, relating_type):
|
||||
self.relating_type = relating_type
|
||||
@@ -108,7 +54,7 @@ class DumbProfileGenerator:
|
||||
props = bpy.context.scene.BIMModelProperties
|
||||
self.collection = bpy.context.view_layer.active_layer_collection.collection
|
||||
self.collection_obj = bpy.data.objects.get(self.collection.name)
|
||||
self.depth = props.extrusion_depth * self.unit_scale
|
||||
self.depth = props.extrusion_depth
|
||||
self.rotation = 0
|
||||
self.location = Vector((0, 0, 0))
|
||||
self.cardinal_point = int(bpy.context.scene.BIMModelProperties.cardinal_point)
|
||||
@@ -314,7 +260,7 @@ class DumbProfileJoiner:
|
||||
body[1 if connection == "ATEND" else 0] = intersect
|
||||
self.recreate_profile(element1, profile1, axis, body)
|
||||
|
||||
def set_depth(self, profile1, length):
|
||||
def set_depth(self, profile1, si_length):
|
||||
element1 = tool.Ifc.get_entity(profile1)
|
||||
if not element1:
|
||||
return
|
||||
@@ -324,8 +270,6 @@ class DumbProfileJoiner:
|
||||
axis1 = self.get_profile_axis(profile1)
|
||||
axis = copy.deepcopy(axis1)
|
||||
body = copy.deepcopy(axis1)
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
si_length = unit_scale * length
|
||||
end = profile1.matrix_world @ Vector((0, 0, si_length))
|
||||
axis[1] = end
|
||||
body[1] = end
|
||||
@@ -892,15 +836,24 @@ class Rotate90(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return context.selected_objects
|
||||
|
||||
def _execute(self, context):
|
||||
objs = []
|
||||
profile_objs = []
|
||||
layer2_objs = []
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
usage = tool.Model.get_usage_type(element)
|
||||
if usage == "PROFILE":
|
||||
profile_objs.append(obj)
|
||||
elif usage == "LAYER2":
|
||||
layer2_objs.append(obj)
|
||||
if element.ConnectedTo or element.ConnectedFrom:
|
||||
objs.append(obj)
|
||||
ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element, connection_type="ATSTART")
|
||||
ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element, connection_type="ATEND")
|
||||
ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element, connection_type="ATPATH")
|
||||
rotate_matrix = Matrix.Rotation(pi / 2, 4, self.axis)
|
||||
obj.matrix_world @= rotate_matrix
|
||||
bpy.context.view_layer.update()
|
||||
DumbProfileRecalculator().recalculate(objs)
|
||||
DumbProfileRecalculator().recalculate(profile_objs)
|
||||
DumbWallRecalculator().recalculate(layer2_objs)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -1022,5 +975,5 @@ class EditExtrusionAxis(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
joiner = DumbProfileJoiner()
|
||||
joiner.set_depth(obj, depth / self.unit_scale)
|
||||
joiner.set_depth(obj, depth)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -21,7 +21,6 @@ import ifcopenshell
|
||||
import blenderbim.tool as tool
|
||||
from blenderbim.bim.prop import ObjProperty
|
||||
from blenderbim.bim.module.model.data import AuthoringData
|
||||
from blenderbim.bim.module.model.root import ConstrTypeEntityNotFound
|
||||
from bpy.types import PropertyGroup, NodeTree
|
||||
from math import pi
|
||||
|
||||
@@ -38,16 +37,16 @@ def get_type_class(self, context):
|
||||
return AuthoringData.data["type_class"]
|
||||
|
||||
|
||||
def get_relating_type(self, context):
|
||||
def get_boundary_class(self, context):
|
||||
if not AuthoringData.is_loaded:
|
||||
AuthoringData.load()
|
||||
return AuthoringData.data["relating_types_ids"]
|
||||
return AuthoringData.data["boundary_class"]
|
||||
|
||||
|
||||
def get_relating_type_browser(self, context):
|
||||
def get_relating_type_id(self, context):
|
||||
if not AuthoringData.is_loaded:
|
||||
AuthoringData.load()
|
||||
return AuthoringData.data["relating_types_ids_browser"]
|
||||
return AuthoringData.data["relating_type_id"]
|
||||
|
||||
|
||||
def get_type_predefined_type(self, context):
|
||||
@@ -56,33 +55,9 @@ def get_type_predefined_type(self, context):
|
||||
return AuthoringData.data["type_predefined_type"]
|
||||
|
||||
|
||||
def update_icon_id(self, context, browser=False):
|
||||
if context == "lost_context" or (context.region is not None and context.region.type != "TOOL_HEADER"):
|
||||
ifc_class = self.ifc_class_browser if browser else self.ifc_class
|
||||
relating_type_id = self.relating_type_id_browser if browser else self.relating_type_id
|
||||
# relating_type = AuthoringData.relating_type_name_by_id(ifc_class, relating_type_id)
|
||||
|
||||
if ifc_class not in self.constr_classes or relating_type_id not in self.constr_classes[ifc_class].constr_types:
|
||||
try:
|
||||
AuthoringData.assetize_relating_type_from_selection(browser=browser)
|
||||
except ConstrTypeEntityNotFound:
|
||||
return
|
||||
|
||||
def set_icon(update_interval_seconds=1e-4):
|
||||
if (
|
||||
ifc_class not in self.constr_classes
|
||||
or relating_type_id not in self.constr_classes[ifc_class].constr_types
|
||||
):
|
||||
return update_interval_seconds
|
||||
else:
|
||||
self.icon_id = self.constr_classes[ifc_class].constr_types[relating_type_id].icon_id
|
||||
|
||||
bpy.app.timers.register(set_icon)
|
||||
|
||||
|
||||
def update_ifc_class(self, context):
|
||||
bpy.ops.bim.load_type_thumbnails(ifc_class=self.ifc_class)
|
||||
AuthoringData.data["relating_types_ids"] = AuthoringData.relating_types()
|
||||
AuthoringData.data["relating_type_id"] = AuthoringData.relating_types()
|
||||
AuthoringData.data["type_thumbnail"] = AuthoringData.type_thumbnail()
|
||||
|
||||
|
||||
@@ -95,21 +70,8 @@ def update_type_class(self, context):
|
||||
AuthoringData.data["type_predefined_type"] = AuthoringData.type_predefined_type()
|
||||
|
||||
|
||||
def update_ifc_class_browser(self, context):
|
||||
if context.region is not None and context.region.type != "TOOL_HEADER":
|
||||
AuthoringData.load_ifc_classes()
|
||||
AuthoringData.load_relating_types_browser()
|
||||
if self.updating:
|
||||
return
|
||||
ifc_class = self.ifc_class_browser
|
||||
constr_class_info = AuthoringData.constr_class_info(ifc_class)
|
||||
|
||||
if constr_class_info is None or not constr_class_info.fully_loaded:
|
||||
AuthoringData.assetize_constr_class(ifc_class)
|
||||
|
||||
|
||||
def update_relating_type(self, context):
|
||||
AuthoringData.load_relating_types()
|
||||
def update_relating_type_id(self, context):
|
||||
AuthoringData.data["relating_type_id"] = AuthoringData.relating_type_id()
|
||||
AuthoringData.data["type_thumbnail"] = AuthoringData.type_thumbnail()
|
||||
|
||||
|
||||
@@ -117,81 +79,20 @@ def update_type_page(self, context):
|
||||
AuthoringData.data["paginated_relating_types"] = AuthoringData.paginated_relating_types()
|
||||
|
||||
|
||||
def update_relating_type_browser(self, context):
|
||||
AuthoringData.load_relating_types_browser()
|
||||
if not self.updating:
|
||||
update_icon_id(self, context, browser=True)
|
||||
|
||||
|
||||
def update_relating_type_by_name(self, context):
|
||||
AuthoringData.load_relating_types()
|
||||
relating_type_id = AuthoringData.relating_type_id_by_name(self.ifc_class, self.relating_type)
|
||||
if relating_type_id is not None:
|
||||
self.relating_type_id = relating_type_id
|
||||
|
||||
|
||||
def get_constr_class_info(props, ifc_class):
|
||||
return props.constr_classes[ifc_class] if ifc_class in props.constr_classes else None
|
||||
|
||||
|
||||
def update_preview_multiple(self, context):
|
||||
if context.region is not None and context.region.type != "TOOL_HEADER":
|
||||
if self.preview_multiple_constr_types:
|
||||
ifc_class = self.ifc_class
|
||||
constr_class_info = get_constr_class_info(self, ifc_class)
|
||||
if constr_class_info is None or not constr_class_info.fully_loaded:
|
||||
AuthoringData.assetize_constr_class(ifc_class)
|
||||
else:
|
||||
update_relating_type(self, context)
|
||||
|
||||
|
||||
class ConstrTypeInfo(PropertyGroup):
|
||||
name: bpy.props.StringProperty(name="Construction type ID")
|
||||
icon_id: bpy.props.IntProperty(name="Icon ID")
|
||||
object: bpy.props.PointerProperty(name="Object", type=bpy.types.Object)
|
||||
|
||||
|
||||
class ConstrClassInfo(PropertyGroup):
|
||||
name: bpy.props.StringProperty(name="Construction class")
|
||||
constr_types: bpy.props.CollectionProperty(type=ConstrTypeInfo)
|
||||
fully_loaded: bpy.props.BoolProperty(default=False)
|
||||
|
||||
|
||||
class ConstrBrowserState(PropertyGroup):
|
||||
cursor_x: bpy.props.IntProperty()
|
||||
cursor_y: bpy.props.IntProperty()
|
||||
window_x: bpy.props.IntProperty()
|
||||
window_y: bpy.props.IntProperty()
|
||||
far_away_x: bpy.props.IntProperty(default=10) # lower left corner to temporarily warp the mouse
|
||||
far_away_y: bpy.props.IntProperty(default=10) # useful to close popup operators
|
||||
updating: bpy.props.BoolProperty()
|
||||
update_delay: bpy.props.FloatProperty(default=3e-2)
|
||||
|
||||
|
||||
class BIMModelProperties(PropertyGroup):
|
||||
ifc_class: bpy.props.EnumProperty(items=get_ifc_class, name="Construction Class", update=update_ifc_class)
|
||||
ifc_class_browser: bpy.props.EnumProperty(
|
||||
items=get_ifc_class, name="Construction Class", update=update_ifc_class_browser
|
||||
)
|
||||
relating_type: bpy.props.StringProperty(update=update_relating_type_by_name)
|
||||
relating_type_id: bpy.props.EnumProperty(
|
||||
items=get_relating_type, name="Construction Type", update=update_relating_type
|
||||
)
|
||||
relating_type_id_browser: bpy.props.EnumProperty(
|
||||
items=get_relating_type_browser, name="Construction Type", update=update_relating_type_browser
|
||||
items=get_relating_type_id, name="Construction Type", update=update_relating_type_id
|
||||
)
|
||||
icon_id: bpy.props.IntProperty()
|
||||
preview_multiple_constr_types: bpy.props.BoolProperty(default=False, update=update_preview_multiple)
|
||||
updating: bpy.props.BoolProperty(default=False)
|
||||
occurrence_name_style: bpy.props.EnumProperty(
|
||||
items=[("CLASS", "By Class", ""), ("TYPE", "By Type", ""), ("CUSTOM", "Custom", "")],
|
||||
name="Occurrence Name Style",
|
||||
)
|
||||
occurrence_name_function: bpy.props.StringProperty(name="Occurrence Name Function")
|
||||
getter_enum = {"ifc_class": get_ifc_class, "relating_type": get_relating_type}
|
||||
constr_classes: bpy.props.CollectionProperty(type=ConstrClassInfo)
|
||||
constr_browser_state: bpy.props.PointerProperty(type=ConstrBrowserState)
|
||||
extrusion_depth: bpy.props.FloatProperty(default=42.0)
|
||||
getter_enum = {"ifc_class": get_ifc_class, "relating_type": get_relating_type_id}
|
||||
extrusion_depth: bpy.props.FloatProperty(default=42.0, subtype="DISTANCE")
|
||||
cardinal_point: bpy.props.EnumProperty(
|
||||
items=(
|
||||
# TODO: complain to buildingSMART
|
||||
@@ -218,14 +119,14 @@ class BIMModelProperties(PropertyGroup):
|
||||
name="Cardinal Point",
|
||||
default="5",
|
||||
)
|
||||
length: bpy.props.FloatProperty(default=42.0)
|
||||
length: bpy.props.FloatProperty(default=42.0, subtype="DISTANCE")
|
||||
openings: bpy.props.CollectionProperty(type=ObjProperty)
|
||||
x: bpy.props.FloatProperty(name="X", default=0.5)
|
||||
y: bpy.props.FloatProperty(name="Y", default=0.5)
|
||||
z: bpy.props.FloatProperty(name="Z", default=0.5)
|
||||
rl1: bpy.props.FloatProperty(name="RL", default=1) # Used for things like walls, doors, flooring, skirting, etc
|
||||
rl2: bpy.props.FloatProperty(name="RL", default=1) # Used for things like windows, other hosted furniture
|
||||
x_angle: bpy.props.FloatProperty(name="X Angle", default=0)
|
||||
x_angle: bpy.props.FloatProperty(name="X Angle", default=0, subtype="ANGLE")
|
||||
type_page: bpy.props.IntProperty(name="Type Page", default=1, update=update_type_page)
|
||||
type_template: bpy.props.EnumProperty(
|
||||
items=(
|
||||
@@ -246,13 +147,14 @@ class BIMModelProperties(PropertyGroup):
|
||||
type_class: bpy.props.EnumProperty(items=get_type_class, name="IFC Class", update=update_type_class)
|
||||
type_predefined_type: bpy.props.EnumProperty(items=get_type_predefined_type, name="Predefined Type", default=None)
|
||||
type_name: bpy.props.StringProperty(name="Name", default="TYPEX")
|
||||
boundary_class: bpy.props.EnumProperty(items=get_boundary_class, name="Boundary Class")
|
||||
|
||||
|
||||
class BIMArrayProperties(PropertyGroup):
|
||||
is_editing: bpy.props.IntProperty(
|
||||
default=-1, description="Currently edited array index. -1 if not in array editing mode."
|
||||
)
|
||||
count: bpy.props.IntProperty(name="Count", default=0)
|
||||
count: bpy.props.IntProperty(name="Count", default=0, min=0)
|
||||
x: bpy.props.FloatProperty(name="X", default=0)
|
||||
y: bpy.props.FloatProperty(name="Y", default=0)
|
||||
z: bpy.props.FloatProperty(name="Z", default=0)
|
||||
@@ -261,6 +163,16 @@ class BIMArrayProperties(PropertyGroup):
|
||||
description="Use local space for array items offset instead of world space",
|
||||
default=True,
|
||||
)
|
||||
method: bpy.props.EnumProperty(
|
||||
items=(("OFFSET", "Offset", ""), ("DISTRIBUTE", "Distribute", "")),
|
||||
name="Method",
|
||||
default="OFFSET",
|
||||
)
|
||||
sync_children: bpy.props.BoolProperty(
|
||||
name="Sync Children",
|
||||
description="Regenerate all children based on the parent object",
|
||||
default=False,
|
||||
)
|
||||
|
||||
|
||||
class BIMStairProperties(PropertyGroup):
|
||||
@@ -342,6 +254,7 @@ class BIMWindowProperties(PropertyGroup):
|
||||
)
|
||||
|
||||
# number of panels and default mullion/transom values
|
||||
# fmt: off
|
||||
window_types_panels = {
|
||||
"SINGLE_PANEL": (1, ((0, 0 ), (0, 0 ))),
|
||||
"DOUBLE_PANEL_HORIZONTAL": (2, ((0, 0 ), (0.45, 0 ))),
|
||||
@@ -353,6 +266,7 @@ class BIMWindowProperties(PropertyGroup):
|
||||
"TRIPLE_PANEL_HORIZONTAL": (3, ((0, 0 ), (0.3, 0.6))),
|
||||
"TRIPLE_PANEL_VERTICAL": (3, ((0.2, 0.4), (0, 0 ))),
|
||||
}
|
||||
# fmt: on
|
||||
|
||||
window_added_previously: bpy.props.BoolProperty(default=False)
|
||||
is_editing: bpy.props.IntProperty(default=-1)
|
||||
@@ -455,7 +369,9 @@ class BIMDoorProperties(PropertyGroup):
|
||||
("DOUBLE_SWING_LEFT", "DOUBLE_SWING_LEFT", ""),
|
||||
("DOUBLE_SWING_RIGHT", "DOUBLE_SWING_RIGHT", ""),
|
||||
("DOUBLE_DOOR_SINGLE_SWING", "DOUBLE_DOOR_SINGLE_SWING", ""),
|
||||
("DOUBLE_DOOR_DOUBLE_SWING", "DOUBLE_DOOR_DOUBLE_SWING", ""),
|
||||
("SLIDING_TO_LEFT", "SLIDING_TO_LEFT", ""),
|
||||
("SLIDING_TO_RIGHT", "SLIDING_TO_RIGHT", ""),
|
||||
("DOUBLE_DOOR_SLIDING", "DOUBLE_DOOR_SLIDING", ""),
|
||||
)
|
||||
|
||||
door_added_previously: bpy.props.BoolProperty(default=False)
|
||||
@@ -525,10 +441,12 @@ class BIMDoorProperties(PropertyGroup):
|
||||
"lining_depth": self.lining_depth,
|
||||
"lining_thickness": self.lining_thickness,
|
||||
"lining_offset": self.lining_offset,
|
||||
"lining_to_panel_offset_x": self.lining_to_panel_offset_x,
|
||||
"lining_to_panel_offset_y": self.lining_to_panel_offset_y,
|
||||
}
|
||||
|
||||
if "SLIDING" not in self.door_type:
|
||||
kwargs["lining_to_panel_offset_x"] = self.lining_to_panel_offset_x
|
||||
kwargs["lining_to_panel_offset_y"] = self.lining_to_panel_offset_y
|
||||
|
||||
kwargs["transom_thickness"] = self.transom_thickness
|
||||
if self.transom_thickness:
|
||||
kwargs["transom_offset"] = self.transom_offset
|
||||
@@ -556,7 +474,18 @@ class BIMDoorProperties(PropertyGroup):
|
||||
|
||||
|
||||
class BIMRailingProperties(PropertyGroup):
|
||||
railing_types = (("FRAMELESS_PANEL", "FRAMELESS_PANEL", ""),)
|
||||
railing_types = (
|
||||
("FRAMELESS_PANEL", "FRAMELESS_PANEL", ""),
|
||||
("WALL_MOUNTED_HANDRAIL", "WALL_MOUNTED_HANDRAIL", ""),
|
||||
)
|
||||
cap_types = (
|
||||
("TO_END_POST_AND_FLOOR", "TO_END_POST_AND_FLOOR", ""),
|
||||
("TO_END_POST", "TO_END_POST", ""),
|
||||
("TO_FLOOR", "TO_FLOOR", ""),
|
||||
("TO_WALL", "TO_WALL", ""),
|
||||
("180", "180", ""),
|
||||
("NONE", "NONE", ""),
|
||||
)
|
||||
|
||||
railing_added_previously: bpy.props.BoolProperty(default=False)
|
||||
is_editing: bpy.props.IntProperty(default=-1)
|
||||
@@ -567,13 +496,42 @@ class BIMRailingProperties(PropertyGroup):
|
||||
thickness: bpy.props.FloatProperty(name="Thickness", default=0.050)
|
||||
spacing: bpy.props.FloatProperty(name="Spacing", default=0.050)
|
||||
|
||||
# wall mounted handrail specific properties
|
||||
use_manual_supports: bpy.props.BoolProperty(
|
||||
name="Use Manual Supports",
|
||||
default=False,
|
||||
description="If enabled, supports are added on every vertex on the edges of the railing path.\n"
|
||||
"If disabled, supports are added automatically based on the support spacing",
|
||||
)
|
||||
support_spacing: bpy.props.FloatProperty(
|
||||
name="Support Spacing", default=1.0, description="Distance between supports if automatic supports are used"
|
||||
)
|
||||
railing_diameter: bpy.props.FloatProperty(name="Railing Diameter", default=0.050)
|
||||
clear_width: bpy.props.FloatProperty(
|
||||
name="Clear Width", default=0.040, description="Clear width between the railing and the wall"
|
||||
)
|
||||
terminal_type: bpy.props.EnumProperty(name="Terminal Type", items=cap_types, default="180")
|
||||
|
||||
def get_general_kwargs(self):
|
||||
return {
|
||||
base_kwargs = {
|
||||
"railing_type": self.railing_type,
|
||||
"height": self.height,
|
||||
"thickness": self.thickness,
|
||||
"spacing": self.spacing,
|
||||
}
|
||||
additional_kwargs = {}
|
||||
if self.railing_type == "FRAMELESS_PANEL":
|
||||
additional_kwargs = {
|
||||
"thickness": self.thickness,
|
||||
"spacing": self.spacing,
|
||||
}
|
||||
elif self.railing_type == "WALL_MOUNTED_HANDRAIL":
|
||||
additional_kwargs = {
|
||||
"railing_diameter": self.railing_diameter,
|
||||
"clear_width": self.clear_width,
|
||||
"use_manual_supports": self.use_manual_supports,
|
||||
"support_spacing": self.support_spacing,
|
||||
"terminal_type": self.terminal_type,
|
||||
}
|
||||
return base_kwargs | additional_kwargs
|
||||
|
||||
|
||||
class BIMRoofProperties(PropertyGroup):
|
||||
|
||||
@@ -18,14 +18,11 @@
|
||||
|
||||
|
||||
import bpy
|
||||
from bpy.types import Operator
|
||||
import bmesh
|
||||
|
||||
import ifcopenshell
|
||||
from ifcopenshell.util.shape_builder import V
|
||||
import blenderbim
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.core.geometry as core
|
||||
from blenderbim.bim.helper import convert_property_group_from_si
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.module.model.door import bm_sort_out_geom
|
||||
@@ -41,6 +38,15 @@ import json
|
||||
# https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcRailingType.htm
|
||||
|
||||
|
||||
NON_SI_RAILING_PROPS = (
|
||||
"is_editing",
|
||||
"railing_type",
|
||||
"railing_added_previously",
|
||||
"use_manual_supports",
|
||||
"terminal_type",
|
||||
)
|
||||
|
||||
|
||||
def bm_split_edge_at_offset(edge, offset):
|
||||
v0, v1 = edge.verts
|
||||
|
||||
@@ -82,7 +88,31 @@ def update_railing_modifier_ifc_data(context):
|
||||
"Height": props.height,
|
||||
},
|
||||
)
|
||||
tool.Ifc.edit(obj)
|
||||
|
||||
if props.railing_type == "WALL_MOUNTED_HANDRAIL":
|
||||
body = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
|
||||
railing_path = [Vector(v) for v in RailingData.data["parameters"]["data_dict"]["path_data"]["verts"]]
|
||||
representation_data = {
|
||||
"railing_type": props.railing_type,
|
||||
"context": body,
|
||||
"railing_path": railing_path,
|
||||
"use_manual_supports": props.use_manual_supports,
|
||||
"support_spacing": props.support_spacing,
|
||||
"railing_diameter": props.railing_diameter,
|
||||
"clear_width": props.clear_width,
|
||||
"terminal_type": props.terminal_type,
|
||||
"height": props.height,
|
||||
}
|
||||
model_representation = ifcopenshell.api.run(
|
||||
"geometry.add_railing_representation", ifc_file, **representation_data
|
||||
)
|
||||
tool.Model.replace_object_ifc_representation(body, obj, model_representation)
|
||||
|
||||
# hacky way to ensure tha ifc representation won't get tessellated at project save
|
||||
IfcStore.edited_objs.discard(obj)
|
||||
|
||||
elif props.railing_type == "FRAMELESS_PANEL":
|
||||
tool.Ifc.edit(obj)
|
||||
|
||||
|
||||
def update_bbim_railing_pset(element, railing_data):
|
||||
@@ -118,77 +148,114 @@ def update_railing_modifier_bmesh(context):
|
||||
tool.Blender.apply_bmesh(obj.data, bm)
|
||||
return
|
||||
|
||||
# generating the entire railing
|
||||
height = props.height * si_conversion
|
||||
thickness = props.thickness * si_conversion
|
||||
spacing = props.spacing * si_conversion
|
||||
if props.railing_type != "FRAMELESS_PANEL":
|
||||
return
|
||||
|
||||
# spacing
|
||||
# split each edge in 3 segments by 0.5 * spacing by x-y plane
|
||||
main_edges = bm.edges[:]
|
||||
for main_edge in main_edges:
|
||||
bm_split_edge_at_offset(main_edge, spacing)
|
||||
def generate_frameless_panel_railing():
|
||||
# generating FRAMELESS_PANEL railing
|
||||
height = props.height * si_conversion
|
||||
thickness = props.thickness * si_conversion
|
||||
spacing = props.spacing * si_conversion
|
||||
|
||||
# thickness
|
||||
# keep track of translated verts so we won't translate the same
|
||||
# vert twice
|
||||
edge_dissolving_verts = []
|
||||
for main_edge in main_edges:
|
||||
v0, v1 = main_edge.verts
|
||||
edge_dissolving_verts.extend([v0, v1])
|
||||
# spacing
|
||||
# split each edge in 3 segments by 0.5 * spacing by x-y plane
|
||||
main_edges = bm.edges[:]
|
||||
for main_edge in main_edges:
|
||||
bm_split_edge_at_offset(main_edge, spacing)
|
||||
|
||||
edge_dir = ((v1.co - v0.co) * V(1, 1, 0)).normalized()
|
||||
ortho_vector = edge_dir.cross(V(0, 0, 1))
|
||||
# thickness
|
||||
# keep track of translated verts so we won't translate the same
|
||||
# vert twice
|
||||
edge_dissolving_verts = []
|
||||
for main_edge in main_edges:
|
||||
v0, v1 = main_edge.verts
|
||||
edge_dissolving_verts.extend([v0, v1])
|
||||
|
||||
extruded_geom = bmesh.ops.extrude_edge_only(bm, edges=[main_edge])["geom"]
|
||||
edge_dir = ((v1.co - v0.co) * V(1, 1, 0)).normalized()
|
||||
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"]
|
||||
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"]
|
||||
bmesh.ops.translate(bm, vec=ortho_vector * (thickness / 2), verts=extruded_verts)
|
||||
|
||||
# dissolve middle edge
|
||||
bmesh.ops.dissolve_edges(bm, edges=[main_edge])
|
||||
|
||||
# height
|
||||
extruded_geom = bmesh.ops.extrude_face_region(bm, geom=bm.faces)["geom"]
|
||||
extruded_verts = bm_sort_out_geom(extruded_geom)["verts"]
|
||||
bmesh.ops.translate(bm, vec=ortho_vector * (-thickness / 2), verts=extruded_verts)
|
||||
extrusion_vector = Vector((0, 0, 1)) * height
|
||||
bmesh.ops.translate(bm, vec=extrusion_vector, verts=extruded_verts)
|
||||
|
||||
extruded_geom = bmesh.ops.extrude_edge_only(bm, edges=[main_edge])["geom"]
|
||||
extruded_verts = bm_sort_out_geom(extruded_geom)["verts"]
|
||||
bmesh.ops.translate(bm, vec=ortho_vector * (thickness / 2), verts=extruded_verts)
|
||||
# dissolve middle edges
|
||||
edges_to_dissolve = []
|
||||
verts_to_dissolve = []
|
||||
for v in edge_dissolving_verts:
|
||||
for e in v.link_edges:
|
||||
other_vert = e.other_vert(v)
|
||||
if other_vert in extruded_verts:
|
||||
edges_to_dissolve.append(e)
|
||||
verts_to_dissolve.append(other_vert)
|
||||
bmesh.ops.dissolve_edges(bm, edges=edges_to_dissolve)
|
||||
bmesh.ops.dissolve_verts(bm, verts=verts_to_dissolve)
|
||||
|
||||
# dissolve middle edge
|
||||
bmesh.ops.dissolve_edges(bm, edges=[main_edge])
|
||||
# to remove unnecessary verts in 0 spacing case
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
|
||||
|
||||
# height
|
||||
extruded_geom = bmesh.ops.extrude_face_region(bm, geom=bm.faces)["geom"]
|
||||
extruded_verts = bm_sort_out_geom(extruded_geom)["verts"]
|
||||
extrusion_vector = Vector((0, 0, 1)) * height
|
||||
bmesh.ops.translate(bm, vec=extrusion_vector, verts=extruded_verts)
|
||||
tool.Blender.apply_bmesh(obj.data, bm)
|
||||
|
||||
# dissolve middle edges
|
||||
edges_to_dissolve = []
|
||||
verts_to_dissolve = []
|
||||
for v in edge_dissolving_verts:
|
||||
for e in v.link_edges:
|
||||
other_vert = e.other_vert(v)
|
||||
if other_vert in extruded_verts:
|
||||
edges_to_dissolve.append(e)
|
||||
verts_to_dissolve.append(other_vert)
|
||||
bmesh.ops.dissolve_edges(bm, edges=edges_to_dissolve)
|
||||
bmesh.ops.dissolve_verts(bm, verts=verts_to_dissolve)
|
||||
|
||||
# to remove unnecessary verts in 0 spacing case
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
|
||||
|
||||
tool.Blender.apply_bmesh(obj.data, bm)
|
||||
generate_frameless_panel_railing()
|
||||
|
||||
|
||||
def get_path_data(obj):
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
if obj.mode == "EDIT":
|
||||
# otherwise mesh may not contain all changes
|
||||
# added in edit mode
|
||||
obj.update_from_editmode()
|
||||
|
||||
mesh = obj.data
|
||||
path_data = dict()
|
||||
path_data["edges"] = [e.vertices[:] for e in mesh.edges]
|
||||
path_data["verts"] = [v.co / si_conversion for v in mesh.vertices]
|
||||
|
||||
if not path_data["edges"] or not path_data["verts"]:
|
||||
bm = tool.Blender.get_bmesh_for_mesh(obj.data)
|
||||
end_points = [v for v in bm.verts if len(v.link_edges) == 1]
|
||||
if not end_points:
|
||||
return None
|
||||
|
||||
# if we have some previous data then we try to match
|
||||
# start or end of the path with the previous path
|
||||
previous_data = False
|
||||
if previous_data:
|
||||
previous_start = previous_data[0]
|
||||
previous_end = previous_data[-1]
|
||||
|
||||
potential_start = min([(v, (v.co - previous_start).length) for v in end_points], key=lambda v_data: v_data[1])
|
||||
potential_end = min([(v, (v.co - previous_end).length) for v in end_points], key=lambda v_data: v_data[1])
|
||||
|
||||
if potential_start[1] < potential_end[1]:
|
||||
start_point = potential_start[0]
|
||||
else:
|
||||
start_point = next(v for v in end_points if v != potential_start[0])
|
||||
else:
|
||||
start_point = min(end_points, key=lambda v: v.index)
|
||||
|
||||
# walking through the path
|
||||
# to make sure all verts and in consequent order
|
||||
edge = start_point.link_edges[0]
|
||||
v = edge.other_vert(start_point)
|
||||
points = [start_point.co, v.co]
|
||||
segments = [(0, 1)]
|
||||
i = 2
|
||||
|
||||
other_edge = lambda edges, edge: next(e for e in edges if e != edge)
|
||||
|
||||
while len(link_edges := v.link_edges) != 1:
|
||||
link_edges = v.link_edges
|
||||
edge = other_edge(link_edges, edge)
|
||||
v = edge.other_vert(v)
|
||||
points.append(v.co)
|
||||
segments.append((i - 1, i))
|
||||
i += 1
|
||||
|
||||
path_data = {"edges": segments, "verts": [p / si_conversion for p in points]}
|
||||
|
||||
return path_data
|
||||
|
||||
|
||||
@@ -248,8 +315,7 @@ class AddRailing(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
# need to make sure all default props will have correct units
|
||||
if not props.railing_added_previously:
|
||||
skip_props = ("is_editing", "railing_type", "railing_added_previously")
|
||||
convert_property_group_from_si(props, skip_props=skip_props)
|
||||
convert_property_group_from_si(props, skip_props=NON_SI_RAILING_PROPS)
|
||||
|
||||
railing_data = props.get_general_kwargs()
|
||||
path_data = get_path_data(obj)
|
||||
@@ -289,8 +355,7 @@ class EnableEditingRailing(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
# need to make sure all props that weren't used before
|
||||
# will have correct units
|
||||
skip_props = ("is_editing", "railing_type", "railing_added_previously")
|
||||
skip_props += tuple(data.keys())
|
||||
skip_props = NON_SI_RAILING_PROPS + tuple(data.keys())
|
||||
convert_property_group_from_si(props, skip_props=skip_props)
|
||||
|
||||
props.is_editing = 1
|
||||
@@ -299,7 +364,7 @@ class EnableEditingRailing(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
class CancelEditingRailing(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.cancel_editing_railing"
|
||||
bl_label = "Cancel editing Railing"
|
||||
bl_label = "Cancel Editing Railing"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def _execute(self, context):
|
||||
@@ -328,7 +393,7 @@ class CancelEditingRailing(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
class FinishEditingRailing(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.finish_editing_railing"
|
||||
bl_label = "Finish editing railing"
|
||||
bl_label = "Finish Editing Railing"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def _execute(self, context):
|
||||
@@ -349,6 +414,37 @@ class FinishEditingRailing(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class FlipRailingPathOrder(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.flip_railing_path_order"
|
||||
bl_label = "Flip Railing Path Order"
|
||||
bl_description = "Can be useful to maintain railing supports direction"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
obj = context.active_object
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
props = obj.BIMRailingProperties
|
||||
|
||||
if not RailingData.is_loaded:
|
||||
RailingData.load()
|
||||
path_data = RailingData.data["parameters"]["data_dict"]["path_data"]
|
||||
|
||||
# flip the vertex order and edges
|
||||
path_data["verts"] = path_data["verts"][::-1]
|
||||
last_vert_i = len(path_data["verts"]) - 1
|
||||
edges = []
|
||||
for edge in path_data["edges"][::-1]:
|
||||
edge = [abs(vi - last_vert_i) for vi in edge[::-1]]
|
||||
edges.append(edge)
|
||||
|
||||
railing_data = props.get_general_kwargs()
|
||||
railing_data["path_data"] = path_data
|
||||
|
||||
update_bbim_railing_pset(element, railing_data)
|
||||
update_railing_modifier_ifc_data(context)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class EnableEditingRailingPath(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.enable_editing_railing_path"
|
||||
bl_label = "Enable Editing Railing Path"
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
from bpy.types import Operator
|
||||
import bmesh
|
||||
|
||||
import ifcopenshell
|
||||
@@ -29,20 +28,18 @@ from blenderbim.bim.module.model.data import RoofData, refresh
|
||||
from blenderbim.bim.module.model.decorator import ProfileDecorator
|
||||
|
||||
import json
|
||||
from math import tan, radians, degrees, atan
|
||||
from math import tan, pi
|
||||
from mathutils import Vector, Matrix
|
||||
from bpypolyskel import bpypolyskel
|
||||
import shapely
|
||||
from pprint import pprint
|
||||
from itertools import chain
|
||||
from math import pi
|
||||
|
||||
# reference:
|
||||
# https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcRoof.htm
|
||||
# https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcRoofType.htm
|
||||
|
||||
|
||||
# create read only property in blender operator
|
||||
NON_SI_ROOF_PROPS = ("is_editing", "roof_type", "roof_added_previously", "generation_method", "angle")
|
||||
|
||||
|
||||
def float_is_zero(f):
|
||||
@@ -116,10 +113,13 @@ def generate_hiped_roof_bmesh(bm, mode="ANGLE", height=1.0, angle=pi / 18, mutat
|
||||
|
||||
original_geometry_data = dict()
|
||||
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]) for e in bm.edges]
|
||||
original_geometry_data["edges"] = [
|
||||
(set(bm_get_indices(e.verts)), e[angle_layer], e[separate_verts_layer]) for e in bm.edges
|
||||
]
|
||||
else:
|
||||
original_geometry_data["edges"] = [(set(bm_get_indices(e.verts)), None) for e in bm.edges]
|
||||
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
|
||||
@@ -212,6 +212,8 @@ def generate_hiped_roof_bmesh(bm, mode="ANGLE", height=1.0, angle=pi / 18, mutat
|
||||
footprint_edges = []
|
||||
footprint_verts = set()
|
||||
verts_to_change = {}
|
||||
verts_to_rip = []
|
||||
bottom_chords_to_remove = []
|
||||
|
||||
# find footprint edges
|
||||
for edge in bm.edges:
|
||||
@@ -227,7 +229,7 @@ def generate_hiped_roof_bmesh(bm, mode="ANGLE", height=1.0, angle=pi / 18, mutat
|
||||
# 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
|
||||
for old_edge_verts, defined_angle in original_geometry_data["edges"]:
|
||||
for old_edge_verts, defined_angle, separate_verts in original_geometry_data["edges"]:
|
||||
if not defined_angle:
|
||||
continue
|
||||
|
||||
@@ -240,14 +242,38 @@ def generate_hiped_roof_bmesh(bm, mode="ANGLE", height=1.0, angle=pi / 18, mutat
|
||||
|
||||
verts_to_move = find_other_polygon_verts(identical_edge)
|
||||
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
|
||||
if not separate_verts:
|
||||
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
|
||||
else:
|
||||
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])
|
||||
|
||||
if defined_angle >= pi / 2:
|
||||
bottom_chords_to_remove.append(identical_edge)
|
||||
|
||||
def separate_vert(bm, vert, edge, new_co):
|
||||
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])
|
||||
|
||||
# apply all changes once at the end
|
||||
for v in verts_to_change:
|
||||
v.co = verts_to_change[v]
|
||||
|
||||
for v, new_co, edge in verts_to_rip:
|
||||
separate_vert(bm, v, edge, new_co)
|
||||
|
||||
bmesh.ops.delete(bm, geom=bottom_chords_to_remove, context="EDGES")
|
||||
|
||||
extrusion_geom = bmesh.ops.extrude_face_region(bm, geom=bm.faces)["geom"]
|
||||
extruded_verts = bm_sort_out_geom(extrusion_geom)["verts"]
|
||||
bmesh.ops.translate(bm, vec=[0.0, 0.0, 0.1], verts=extruded_verts)
|
||||
@@ -289,9 +315,6 @@ def update_roof_modifier_ifc_data(context):
|
||||
# occurences attributes
|
||||
# occurences = tool.Ifc.get_all_element_occurences(element)
|
||||
|
||||
# TODO: add Qto_RoofBaseQuantities, need to calculate GrossArea, NetArea, ProjectedArea
|
||||
# https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/Qto_RoofBaseQuantities.htm
|
||||
|
||||
tool.Ifc.edit(obj)
|
||||
|
||||
|
||||
@@ -311,12 +334,14 @@ def update_roof_modifier_bmesh(context):
|
||||
RoofData.load()
|
||||
path_data = RoofData.data["parameters"]["data_dict"]["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"]]
|
||||
@@ -325,6 +350,7 @@ def update_roof_modifier_bmesh(context):
|
||||
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:
|
||||
@@ -349,12 +375,15 @@ def get_path_data(obj):
|
||||
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
|
||||
@@ -417,21 +446,32 @@ class AddRoof(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
# need to make sure all default props will have correct units
|
||||
if not props.roof_added_previously:
|
||||
skip_props = ("is_editing", "roof_type", "roof_added_previously", "generation_method")
|
||||
convert_property_group_from_si(props, skip_props=skip_props)
|
||||
convert_property_group_from_si(props, skip_props=NON_SI_ROOF_PROPS)
|
||||
|
||||
# rejecting original roof shape to be safe
|
||||
# taking into account only it's bounding box dimensions
|
||||
if obj.dimensions.x == 0 or obj.dimensions.y == 0:
|
||||
min_x, min_y = -5, -5
|
||||
max_x, max_y = 5, 5
|
||||
min_z = 0
|
||||
else:
|
||||
bbox = tool.Blender.get_object_bounding_box(obj)
|
||||
min_x = bbox["min_x"]
|
||||
min_y = bbox["min_y"]
|
||||
max_x = bbox["max_x"]
|
||||
max_y = bbox["max_y"]
|
||||
min_z = bbox["min_z"]
|
||||
|
||||
roof_data = props.get_general_kwargs()
|
||||
path_data = get_path_data(obj)
|
||||
if not path_data:
|
||||
path_data = {
|
||||
"edges": [[0, 1], [1, 2], [2, 3], [3, 0]],
|
||||
"verts": [
|
||||
Vector([-5.0, -5.0, 0.0]) / si_conversion,
|
||||
Vector([-5.0, 5.0, 0.0]) / si_conversion,
|
||||
Vector([5.0, 5.0, 0.0]) / si_conversion,
|
||||
Vector([5.0, -5.0, 0.0]) / si_conversion,
|
||||
],
|
||||
}
|
||||
path_data = {
|
||||
"edges": [[0, 1], [1, 2], [2, 3], [3, 0]],
|
||||
"verts": [
|
||||
Vector([min_x, min_y, min_z]) / si_conversion,
|
||||
Vector([min_x, max_y, min_z]) / si_conversion,
|
||||
Vector([max_x, max_y, min_z]) / si_conversion,
|
||||
Vector([max_x, min_y, min_z]) / si_conversion,
|
||||
],
|
||||
}
|
||||
roof_data["path_data"] = path_data
|
||||
|
||||
update_bbim_roof_pset(element, roof_data)
|
||||
@@ -459,8 +499,7 @@ class EnableEditingRoof(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
# need to make sure all props that weren't used before
|
||||
# will have correct units
|
||||
skip_props = ("is_editing", "roof_type", "roof_added_previously", "generation_method")
|
||||
skip_props += tuple(data.keys())
|
||||
skip_props = NON_SI_ROOF_PROPS + tuple(data.keys())
|
||||
convert_property_group_from_si(props, skip_props=skip_props)
|
||||
|
||||
props.is_editing = 1
|
||||
@@ -469,7 +508,7 @@ class EnableEditingRoof(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
class CancelEditingRoof(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.cancel_editing_roof"
|
||||
bl_label = "Cancel editing Roof"
|
||||
bl_label = "Cancel Editing Roof"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def _execute(self, context):
|
||||
@@ -498,7 +537,7 @@ class CancelEditingRoof(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
class FinishEditingRoof(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.finish_editing_roof"
|
||||
bl_label = "Finish editing roof"
|
||||
bl_label = "Finish Editing Roof"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def _execute(self, context):
|
||||
@@ -633,9 +672,10 @@ class RemoveRoof(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
class SetGableRoofEdgeAngle(bpy.types.Operator):
|
||||
bl_idname = "bim.set_gable_roof_edge_angle"
|
||||
bl_label = "Set gable roof edge angle"
|
||||
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):
|
||||
@@ -658,12 +698,17 @@ 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"}
|
||||
|
||||
@@ -48,8 +48,3 @@ def sync_name(usecase_path, ifc_file, settings):
|
||||
collection.name = new_name
|
||||
obj.name = new_name
|
||||
blenderbim.bim.handler.refresh_ui_data()
|
||||
|
||||
|
||||
class ConstrTypeEntityNotFound(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ class DumbSlabGenerator:
|
||||
self.length = 3
|
||||
self.rotation = 0
|
||||
self.location = Vector((0, 0, 0))
|
||||
self.x_angle = 0 if tool.Cad.is_x(props.x_angle, 0, tolerance=0.001) else radians(props.x_angle)
|
||||
self.x_angle = 0 if tool.Cad.is_x(props.x_angle, 0, tolerance=0.001) else props.x_angle
|
||||
return self.derive_from_cursor()
|
||||
|
||||
def derive_from_cursor(self):
|
||||
@@ -238,11 +238,11 @@ class DumbSlabPlaner:
|
||||
if not rel.is_a("IfcRelAssociatesMaterial"):
|
||||
continue
|
||||
for element in rel.RelatedObjects:
|
||||
self.change_thickness(element, thickness)
|
||||
self.change_thickness(element, total_thickness)
|
||||
else:
|
||||
for rel in inverse.AssociatedTo:
|
||||
for element in rel.RelatedObjects:
|
||||
self.change_thickness(element, thickness)
|
||||
self.change_thickness(element, total_thickness)
|
||||
|
||||
def regenerate_from_type(self, usecase_path, ifc_file, settings):
|
||||
obj = tool.Ifc.get_object(settings["related_object"])
|
||||
@@ -274,7 +274,7 @@ class DumbSlabPlaner:
|
||||
extrusion.Depth = thickness
|
||||
else:
|
||||
props = bpy.context.scene.BIMModelProperties
|
||||
x_angle = 0 if tool.Cad.is_x(props.x_angle, 0, tolerance=0.001) else radians(props.x_angle)
|
||||
x_angle = 0 if tool.Cad.is_x(props.x_angle, 0, tolerance=0.001) else props.x_angle
|
||||
new_rep = ifcopenshell.api.run(
|
||||
"geometry.add_slab_representation",
|
||||
tool.Ifc.get(),
|
||||
@@ -299,7 +299,7 @@ class DumbSlabPlaner:
|
||||
return
|
||||
else:
|
||||
props = bpy.context.scene.BIMModelProperties
|
||||
x_angle = 0 if tool.Cad.is_x(props.x_angle, 0, tolerance=0.001) else radians(props.x_angle)
|
||||
x_angle = 0 if tool.Cad.is_x(props.x_angle, 0, tolerance=0.001) else props.x_angle
|
||||
representation = ifcopenshell.api.run(
|
||||
"geometry.add_slab_representation",
|
||||
tool.Ifc.get(),
|
||||
@@ -604,6 +604,7 @@ class EnableEditingExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
|
||||
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
||||
body = ifcopenshell.util.representation.resolve_representation(body)
|
||||
extrusion = tool.Model.get_extrusion(body)
|
||||
|
||||
if extrusion.Position:
|
||||
@@ -636,8 +637,9 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
obj = context.active_object
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
|
||||
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
||||
extrusion = tool.Model.get_extrusion(representation)
|
||||
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
||||
body = ifcopenshell.util.representation.resolve_representation(body)
|
||||
extrusion = tool.Model.get_extrusion(body)
|
||||
if extrusion.Position:
|
||||
position = Matrix(ifcopenshell.util.placement.get_axis2placement(extrusion.Position).tolist())
|
||||
position[0][3] *= self.unit_scale
|
||||
@@ -670,37 +672,42 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
representation=body,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
bpy.data.meshes.remove(profile_mesh)
|
||||
|
||||
# Only certain classes should have a footprint
|
||||
if element.is_a() not in ("IfcSlab", "IfcRamp"):
|
||||
return
|
||||
|
||||
footprint_context = ifcopenshell.util.representation.get_context(
|
||||
tool.Ifc.get(), "Plan", "FootPrint", "SKETCH_VIEW"
|
||||
)
|
||||
if footprint_context:
|
||||
curves = [profile.OuterCurve]
|
||||
if profile.is_a("IfcArbitraryProfileDefWithVoids"):
|
||||
curves.extend(profile.InnerCurves)
|
||||
new_footprint = ifcopenshell.api.run(
|
||||
"geometry.add_footprint_representation", tool.Ifc.get(), context=footprint_context, curves=curves
|
||||
if not footprint_context:
|
||||
return
|
||||
|
||||
curves = [profile.OuterCurve]
|
||||
if profile.is_a("IfcArbitraryProfileDefWithVoids"):
|
||||
curves.extend(profile.InnerCurves)
|
||||
new_footprint = ifcopenshell.api.run(
|
||||
"geometry.add_footprint_representation", tool.Ifc.get(), context=footprint_context, curves=curves
|
||||
)
|
||||
old_footprint = ifcopenshell.util.representation.get_representation(
|
||||
element, "Plan", "FootPrint", "SKETCH_VIEW"
|
||||
)
|
||||
if old_footprint:
|
||||
for inverse in tool.Ifc.get().get_inverse(old_footprint):
|
||||
ifcopenshell.util.element.replace_attribute(inverse, old_footprint, new_footprint)
|
||||
blenderbim.core.geometry.remove_representation(
|
||||
tool.Ifc, tool.Geometry, obj=obj, representation=old_footprint
|
||||
)
|
||||
old_footprint = ifcopenshell.util.representation.get_representation(
|
||||
element, "Plan", "FootPrint", "SKETCH_VIEW"
|
||||
else:
|
||||
ifcopenshell.api.run(
|
||||
"geometry.assign_representation", tool.Ifc.get(), product=element, representation=new_footprint
|
||||
)
|
||||
if old_footprint:
|
||||
for inverse in tool.Ifc.get().get_inverse(old_footprint):
|
||||
ifcopenshell.util.element.replace_attribute(inverse, old_footprint, new_footprint)
|
||||
blenderbim.core.geometry.remove_representation(
|
||||
tool.Ifc, tool.Geometry, obj=obj, representation=old_footprint
|
||||
)
|
||||
else:
|
||||
ifcopenshell.api.run(
|
||||
"geometry.assign_representation", tool.Ifc.get(), product=element, representation=new_footprint
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ResetVertex(bpy.types.Operator):
|
||||
|
||||
@@ -26,12 +26,20 @@ import blenderbim.tool as tool
|
||||
import blenderbim.core.type
|
||||
from math import pi
|
||||
from mathutils import Vector, Matrix
|
||||
from shapely import Polygon
|
||||
|
||||
|
||||
class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.generate_space"
|
||||
bl_label = "Generate Space"
|
||||
bl_options = {"REGISTER"}
|
||||
bl_description = "Create a space from the cursor position. Move the cursor position into the desired position, select the right space collection and run the operator"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
collection = context.view_layer.active_layer_collection.collection
|
||||
collection_obj = bpy.data.objects.get(collection.name)
|
||||
return tool.Ifc.get_entity(collection_obj)
|
||||
|
||||
def _execute(self, context):
|
||||
# This only works based on a 2D plan only considering the standard
|
||||
@@ -65,8 +73,13 @@ class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator):
|
||||
element = None
|
||||
if bpy.context.selected_objects and active_obj:
|
||||
element = tool.Ifc.get_entity(active_obj)
|
||||
x, y, z = active_obj.matrix_world.translation.xyz
|
||||
mat = active_obj.matrix_world
|
||||
local_bbox_center = 0.125 * sum((Vector(b) for b in active_obj.bound_box), Vector())
|
||||
global_bbox_center = mat @ local_bbox_center
|
||||
x = global_bbox_center.x
|
||||
y = global_bbox_center.y
|
||||
z = (mat @ Vector(active_obj.bound_box[0])).z
|
||||
|
||||
h = active_obj.dimensions.z
|
||||
else:
|
||||
x, y = context.scene.cursor.location.xy
|
||||
@@ -166,3 +179,177 @@ class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator):
|
||||
(obj.matrix_world @ Vector((max_x, max_y, 0.0))).to_2d(),
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.generate_spaces_from_walls"
|
||||
bl_label = "Generate Spaces From Walls"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Generate spaces from selected walls. The active object must be a wall."
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
active_obj = bpy.context.active_object
|
||||
element = tool.Ifc.get_entity(active_obj)
|
||||
if element:
|
||||
return context.selected_objects and element.is_a("IfcWall")
|
||||
|
||||
def _execute(self, context):
|
||||
# This only works based on a 2D plan only considering the standard
|
||||
# walls (i.e. prismatic) in the active object storey.
|
||||
# In order to run, the active objct must be a wall and
|
||||
# have to be selected walls
|
||||
props = context.scene.BIMModelProperties
|
||||
active_obj = bpy.context.active_object
|
||||
|
||||
if not active_obj:
|
||||
self.report({'ERROR'}, "No active object. Please select a wall")
|
||||
return
|
||||
|
||||
element = None
|
||||
element = tool.Ifc.get_entity(active_obj)
|
||||
if element:
|
||||
if not element.is_a("IfcWall"):
|
||||
self.report({'ERROR'}, "The active object is not a wall. Please select a wall.")
|
||||
return
|
||||
|
||||
collection = active_obj.users_collection[0]
|
||||
collection_obj = bpy.data.objects.get(collection.name)
|
||||
if not collection_obj:
|
||||
self.report({'ERROR'}, "No collection found. Please insert one.")
|
||||
return
|
||||
|
||||
spatial_element = tool.Ifc.get_entity(collection_obj)
|
||||
if not spatial_element:
|
||||
self.report({'ERROR'}, "The collection hasn't an ifc space entity. Please provide one.")
|
||||
return
|
||||
|
||||
if not bpy.context.selected_objects:
|
||||
self.report({'ERROR'}, "No selected objects found. Please select walls.")
|
||||
return
|
||||
|
||||
x, y, z = active_obj.matrix_world.translation.xyz
|
||||
mat = active_obj.matrix_world
|
||||
h = active_obj.dimensions.z
|
||||
selected_objects = bpy.context.selected_objects
|
||||
|
||||
boundary_elements = self.get_boundary_elements(selected_objects)
|
||||
|
||||
polys = self.get_polygons(boundary_elements)
|
||||
|
||||
converted_tolerance = self.get_converted_tolerance(tolerance = 0.03)
|
||||
|
||||
union = shapely.ops.unary_union(polys).buffer(converted_tolerance, cap_style = 2, join_style = 2)
|
||||
|
||||
i=0
|
||||
for linear_ring in union.interiors:
|
||||
poly = Polygon(linear_ring)
|
||||
poly = poly.buffer(converted_tolerance, single_sided=True, cap_style = 2, join_style = 2)
|
||||
|
||||
bm = self.get_bmesh_from_polygon(poly, mat, h)
|
||||
|
||||
name = "Space" + str(i)
|
||||
mesh = bpy.data.meshes.new(name = name)
|
||||
bm.to_mesh(mesh)
|
||||
bm.free()
|
||||
|
||||
obj = bpy.data.objects.new(name, mesh)
|
||||
obj.matrix_world = mat
|
||||
|
||||
self.set_obj_origin_to_bboxcenter(obj)
|
||||
|
||||
collection.objects.link(obj)
|
||||
|
||||
bpy.ops.bim.assign_class(obj=obj.name, ifc_class="IfcSpace")
|
||||
i+=1
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def get_boundary_elements(self, selected_objects):
|
||||
boundary_elements = []
|
||||
for obj in selected_objects:
|
||||
subelement = tool.Ifc.get_entity(obj)
|
||||
if subelement.is_a("IfcWall"):
|
||||
boundary_elements.append(subelement)
|
||||
return boundary_elements
|
||||
|
||||
def get_polygons(self, boundary_elements):
|
||||
polys = []
|
||||
for boundary_element in boundary_elements:
|
||||
obj = tool.Ifc.get_object(boundary_element)
|
||||
if not obj:
|
||||
continue
|
||||
points = []
|
||||
base = self.get_obj_base_points(obj)
|
||||
for index in ["low_left", "low_right", "high_right", "high_left"]:
|
||||
point = base[index]
|
||||
points.append(point)
|
||||
|
||||
polys.append(Polygon(points))
|
||||
return polys
|
||||
|
||||
def get_obj_base_points(self, obj):
|
||||
x_values = [(obj.matrix_world @ Vector(v)).x for v in obj.bound_box]
|
||||
y_values = [(obj.matrix_world @ Vector(v)).y for v in obj.bound_box]
|
||||
return {
|
||||
"low_left": (x_values[0], y_values[0]),
|
||||
"high_left": (x_values[3], y_values[3]),
|
||||
"low_right": (x_values[4], y_values[4]),
|
||||
"high_right": (x_values[7], y_values[7]),
|
||||
}
|
||||
|
||||
def get_converted_tolerance(self, tolerance):
|
||||
model = tool.Ifc.get()
|
||||
project_unit = ifcopenshell.util.unit.get_project_unit(model, "LENGTHUNIT")
|
||||
prefix=getattr(project_unit, "Prefix", None)
|
||||
|
||||
converted_tolerance = ifcopenshell.util.unit.convert(
|
||||
value = tolerance,
|
||||
from_prefix = None,
|
||||
from_unit = "METRE",
|
||||
to_prefix = prefix,
|
||||
to_unit = project_unit.Name,
|
||||
)
|
||||
return tolerance
|
||||
|
||||
def get_bmesh_from_polygon(self, poly, mat, h):
|
||||
bm = bmesh.new()
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
|
||||
mat_invert = mat.inverted()
|
||||
|
||||
new_verts = [bm.verts.new(mat_invert @ Vector([v[0], v[1], 0])) for v in poly.exterior.coords[0:-1]]
|
||||
[bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
|
||||
bm.edges.new((new_verts[len(new_verts) - 1], new_verts[0]))
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
|
||||
bmesh.ops.triangle_fill(bm, edges=bm.edges)
|
||||
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 5, verts=bm.verts, edges=bm.edges)
|
||||
|
||||
extrusion = bmesh.ops.extrude_face_region(bm, geom=bm.faces)
|
||||
extruded_verts = [g for g in extrusion["geom"] if isinstance(g, bmesh.types.BMVert)]
|
||||
bmesh.ops.translate(bm, vec=[0.0, 0.0, h], verts=extruded_verts)
|
||||
|
||||
bmesh.ops.recalc_face_normals(bm, faces = bm.faces)
|
||||
|
||||
return bm
|
||||
|
||||
def set_obj_origin_to_bboxcenter(self, obj):
|
||||
mat = obj.matrix_world
|
||||
inverted = mat.inverted()
|
||||
local_bbox_center = 0.125 * sum((Vector(b) for b in obj.bound_box), Vector())
|
||||
global_bbox_center = mat @ local_bbox_center
|
||||
|
||||
oldLoc = obj.location
|
||||
newLoc = global_bbox_center
|
||||
diff = newLoc - oldLoc
|
||||
for vert in obj.data.vertices:
|
||||
aux_vector = mat @ vert.co
|
||||
aux_vector = aux_vector - diff
|
||||
vert.co = inverted @ aux_vector
|
||||
obj.location = newLoc
|
||||
|
||||
@@ -287,6 +287,22 @@ def update_ifc_stair_props(obj):
|
||||
)
|
||||
tool.Ifc.edit(obj)
|
||||
|
||||
# update related annotation objects
|
||||
def get_elements_from_product(product):
|
||||
elements = []
|
||||
for rel in product.ReferencedBy:
|
||||
if not rel.is_a("IfcRelAssignsToProduct"):
|
||||
continue
|
||||
elements.extend(rel.RelatedObjects)
|
||||
return elements
|
||||
|
||||
stair_obj = obj
|
||||
for rel_element in get_elements_from_product(element):
|
||||
if not rel_element.is_a("IfcAnnotation") or rel_element.ObjectType != "STAIR_ARROW":
|
||||
continue
|
||||
if annotation_obj := tool.Ifc.get_object(rel_element):
|
||||
tool.Drawing.setup_annotation_object(annotation_obj, "STAIR_ARROW", stair_obj)
|
||||
|
||||
|
||||
class BIM_OT_add_clever_stair(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "mesh.add_clever_stair"
|
||||
@@ -368,7 +384,7 @@ class AddStair(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
class CancelEditingStair(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.cancel_editing_stair"
|
||||
bl_label = "Cancel editing Stair"
|
||||
bl_label = "Cancel Editing Stair"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def _execute(self, context):
|
||||
@@ -388,7 +404,7 @@ class CancelEditingStair(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
class FinishEditingStair(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.finish_editing_stair"
|
||||
bl_label = "Finish editing stair"
|
||||
bl_label = "Finish Editing Stair"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def _execute(self, context):
|
||||
|
||||
@@ -182,7 +182,7 @@ class UpdateDataFromSverchok(bpy.types.Operator, tool.Ifc.Operator):
|
||||
# removed the part that was relying on node graph to be opened at execution
|
||||
class ImportSverchokGraph(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.import_sverchok_graph"
|
||||
bl_label = "Import sverchok graph"
|
||||
bl_label = "Import Sverchok Graph"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
filepath: bpy.props.StringProperty(
|
||||
@@ -224,7 +224,7 @@ class ImportSverchokGraph(bpy.types.Operator, tool.Ifc.Operator):
|
||||
# removed the part that was relying on node graph to be opened at execution
|
||||
class ExportSverchokGraph(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.export_sverchok_graph"
|
||||
bl_label = "Export sverchok graph"
|
||||
bl_label = "Export Sverchok Graph"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
filepath: bpy.props.StringProperty(
|
||||
|
||||
@@ -132,62 +132,9 @@ class BIM_PT_authoring(Panel):
|
||||
|
||||
def draw(self, context):
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.align_wall", icon="ANCHOR_TOP", text="Ext.").align_type = "EXTERIOR"
|
||||
row.operator("bim.align_wall", icon="ANCHOR_CENTER", text="C/L").align_type = "CENTERLINE"
|
||||
row.operator("bim.align_wall", icon="ANCHOR_BOTTOM", text="Int.").align_type = "INTERIOR"
|
||||
|
||||
|
||||
class HelpConstrTypes(Operator):
|
||||
bl_idname = "bim.help_relating_types"
|
||||
bl_label = "Construction Types Help"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Click to read some contextual help"
|
||||
|
||||
def execute(self, context):
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_popup(self, width=510)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.row().separator(factor=0.5)
|
||||
row = layout.row()
|
||||
row.alignment = "CENTER"
|
||||
row.label(text="BlenderBIM Help", icon="BLENDER")
|
||||
layout.row().separator(factor=0.5)
|
||||
|
||||
row = layout.row().row()
|
||||
row.label(text="Overview:", icon="KEYTYPE_MOVING_HOLD_VEC")
|
||||
self.draw_lines(layout, self.message_summary)
|
||||
layout.row().separator()
|
||||
|
||||
row = layout.row().row()
|
||||
row.label(text="Further support:", icon="KEYTYPE_MOVING_HOLD_VEC")
|
||||
layout.row().separator(factor=0.5)
|
||||
row = layout.row()
|
||||
op = row.operator("bim.open_upstream", text="Homepage", icon="HOME")
|
||||
op.page = "home"
|
||||
op = row.operator("bim.open_upstream", text="Docs", icon="DOCUMENTS")
|
||||
op.page = "docs"
|
||||
op = row.operator("bim.open_upstream", text="Wiki", icon="CURRENT_FILE")
|
||||
op.page = "wiki"
|
||||
op = row.operator("bim.open_upstream", text="Community", icon="COMMUNITY")
|
||||
op.page = "community"
|
||||
layout.row().separator()
|
||||
|
||||
def draw_lines(self, layout, lines):
|
||||
box = layout.box()
|
||||
for line in lines:
|
||||
row = box.row()
|
||||
row.label(text=f" {line}")
|
||||
|
||||
@property
|
||||
def message_summary(self):
|
||||
return [
|
||||
"The Construction Type Browser allows to preview and add new instances to the model.",
|
||||
"For further support, please click on the Documentation link below.",
|
||||
]
|
||||
row.operator("bim.generate_space")
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.generate_spaces_from_walls")
|
||||
|
||||
|
||||
class BIM_PT_array(bpy.types.Panel):
|
||||
@@ -224,21 +171,29 @@ class BIM_PT_array(bpy.types.Panel):
|
||||
row.operator("bim.edit_array", icon="CHECKMARK", text="").item = i
|
||||
row.operator("bim.disable_editing_array", icon="CANCEL", text="")
|
||||
row = box.row(align=True)
|
||||
row.prop(props, "use_local_space")
|
||||
row.prop(props, "method")
|
||||
row = box.row(align=True)
|
||||
row.prop(props, "use_local_space")
|
||||
row.prop(props, "sync_children")
|
||||
col = box.column()
|
||||
row = col.row(align=True)
|
||||
row.prop(props, "x")
|
||||
row.operator("bim.input_cursor_x_array", icon="CURSOR", text="")
|
||||
row = col.row(align=True)
|
||||
row.prop(props, "y")
|
||||
row.operator("bim.input_cursor_y_array", icon="CURSOR", text="")
|
||||
row = col.row(align=True)
|
||||
row.prop(props, "z")
|
||||
|
||||
row.operator("bim.input_cursor_z_array", icon="CURSOR", text="")
|
||||
else:
|
||||
row = box.row(align=True)
|
||||
row.label(text=f"{array['count']} Items", icon="MOD_ARRAY")
|
||||
name = f"{array['count']} Items ({array.get('method', 'OFFSET').capitalize()})"
|
||||
row.label(text=name, icon="MOD_ARRAY")
|
||||
row.operator("bim.enable_editing_array", icon="GREASEPENCIL", text="").item = i
|
||||
row.operator("bim.remove_array", icon="X", text="").item = i
|
||||
row = box.row(align=True)
|
||||
row.label(text=f"Use Local Space: {array.get('use_local_space', False)}")
|
||||
row = box.row(align=True)
|
||||
row.label(text=f"X: {array['x']}")
|
||||
icon = "EMPTY_ARROWS" if array.get("use_local_space", False) else "EMPTY_AXIS"
|
||||
row.label(text=f"X: {array['x']}", icon=icon)
|
||||
row.label(text=f"Y: {array['y']}")
|
||||
row.label(text=f"Z: {array['z']}")
|
||||
else:
|
||||
@@ -273,7 +228,7 @@ class BIM_PT_stair(bpy.types.Panel):
|
||||
stair_data = StairData.data["parameters"]["data_dict"]
|
||||
if props.is_editing != -1:
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.finish_editing_stair", icon="CHECKMARK", text="Finish editing")
|
||||
row.operator("bim.finish_editing_stair", icon="CHECKMARK", text="Finish Editing")
|
||||
row.operator("bim.cancel_editing_stair", icon="CANCEL", text="")
|
||||
row = self.layout.row(align=True)
|
||||
for prop_name in props.get_props_kwargs():
|
||||
@@ -373,7 +328,7 @@ class BIM_PT_window(bpy.types.Panel):
|
||||
|
||||
if props.is_editing != -1:
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.finish_editing_window", icon="CHECKMARK", text="Finish editing")
|
||||
row.operator("bim.finish_editing_window", icon="CHECKMARK", text="Finish Editing")
|
||||
row.operator("bim.cancel_editing_window", icon="CANCEL", text="")
|
||||
|
||||
general_props = props.get_general_kwargs()
|
||||
@@ -488,7 +443,7 @@ class BIM_PT_door(bpy.types.Panel):
|
||||
|
||||
if props.is_editing != -1:
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.finish_editing_door", icon="CHECKMARK", text="Finish editing")
|
||||
row.operator("bim.finish_editing_door", icon="CHECKMARK", text="Finish Editing")
|
||||
row.operator("bim.cancel_editing_door", icon="CANCEL", text="")
|
||||
|
||||
general_props = props.get_general_kwargs()
|
||||
@@ -572,7 +527,7 @@ class BIM_PT_railing(bpy.types.Panel):
|
||||
|
||||
if props.is_editing != -1:
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.finish_editing_railing", icon="CHECKMARK", text="Finish editing")
|
||||
row.operator("bim.finish_editing_railing", icon="CHECKMARK", text="Finish Editing")
|
||||
row.operator("bim.cancel_editing_railing", icon="CANCEL", text="")
|
||||
|
||||
general_props = props.get_general_kwargs()
|
||||
@@ -588,6 +543,9 @@ class BIM_PT_railing(bpy.types.Panel):
|
||||
else:
|
||||
row.operator("bim.enable_editing_railing", icon="GREASEPENCIL", text="")
|
||||
row.operator("bim.enable_editing_railing_path", icon="ANIM", text="")
|
||||
# TODO: good for preview but probably should move to .is_editing == -1
|
||||
# since it's writing to ifc
|
||||
row.operator("bim.flip_railing_path_order", icon="ARROW_LEFTRIGHT", text="")
|
||||
row.operator("bim.remove_railing", icon="X", text="")
|
||||
|
||||
box = self.layout.box()
|
||||
@@ -631,7 +589,7 @@ class BIM_PT_roof(bpy.types.Panel):
|
||||
|
||||
if props.is_editing != -1:
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.finish_editing_roof", icon="CHECKMARK", text="Finish editing")
|
||||
row.operator("bim.finish_editing_roof", icon="CHECKMARK", text="Finish Editing")
|
||||
row.operator("bim.cancel_editing_roof", icon="CANCEL", text="")
|
||||
|
||||
general_props = props.get_general_kwargs()
|
||||
@@ -656,7 +614,7 @@ class BIM_PT_roof(bpy.types.Panel):
|
||||
prop_value = round(prop_value, 5) if type(prop_value) is float else prop_value
|
||||
row = box.row(align=True)
|
||||
row.label(text=f"{props.bl_rna.properties[prop].name}")
|
||||
if prop == 'angle':
|
||||
if prop == "angle":
|
||||
prop_value = round(degrees(prop_value), 2)
|
||||
row.label(text=str(prop_value))
|
||||
else:
|
||||
|
||||
@@ -37,38 +37,6 @@ from mathutils import Vector, Matrix
|
||||
from blenderbim.bim.module.model.opening import FilledOpeningGenerator
|
||||
|
||||
|
||||
def element_listener(element, obj):
|
||||
blenderbim.bim.handler.subscribe_to(obj, "mode", mode_callback)
|
||||
|
||||
|
||||
def mode_callback(obj, data):
|
||||
for obj in set(bpy.context.selected_objects + [bpy.context.active_object]):
|
||||
if (
|
||||
not obj.data
|
||||
or not isinstance(obj.data, (bpy.types.Mesh, bpy.types.Curve, bpy.types.TextCurve))
|
||||
or not obj.BIMObjectProperties.ifc_definition_id
|
||||
or not bpy.context.scene.BIMProjectProperties.is_authoring
|
||||
):
|
||||
return
|
||||
product = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
parametric = ifcopenshell.util.element.get_psets(product).get("EPset_Parametric")
|
||||
if not parametric or parametric["Engine"] != "BlenderBIM.DumbLayer2":
|
||||
return
|
||||
if obj.mode == "EDIT":
|
||||
tool.Ifc.edit(obj)
|
||||
bm = bmesh.from_edit_mesh(obj.data)
|
||||
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
|
||||
bmesh.update_edit_mesh(obj.data)
|
||||
else:
|
||||
new_origin = obj.matrix_world @ Vector(obj.bound_box[0])
|
||||
obj.data.transform(
|
||||
Matrix.Translation(
|
||||
(obj.matrix_world.inverted().to_quaternion() @ (obj.matrix_world.translation - new_origin))
|
||||
)
|
||||
)
|
||||
obj.matrix_world.translation = new_origin
|
||||
|
||||
|
||||
class JoinWall(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.join_wall"
|
||||
bl_label = "Join Wall"
|
||||
@@ -239,6 +207,7 @@ class ChangeExtrusionDepth(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
def _execute(self, context):
|
||||
layer2_objs = []
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
@@ -251,7 +220,7 @@ class ChangeExtrusionDepth(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return
|
||||
x, y, z = extrusion.ExtrudedDirection.DirectionRatios
|
||||
x_angle = Vector((0, 1)).angle_signed(Vector((y, z)))
|
||||
extrusion.Depth = self.depth * (1 / cos(x_angle))
|
||||
extrusion.Depth = self.depth / si_conversion * (1 / cos(x_angle))
|
||||
if tool.Model.get_usage_type(element) == "LAYER2":
|
||||
for rel in element.ConnectedFrom:
|
||||
if rel.is_a() == "IfcRelConnectsElements":
|
||||
@@ -271,7 +240,7 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.change_extrusion_x_angle"
|
||||
bl_label = "Change Extrusion X Angle"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
x_angle: bpy.props.FloatProperty()
|
||||
x_angle: bpy.props.FloatProperty(name="X Angle", default=0, subtype="ANGLE")
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@@ -280,7 +249,7 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
def _execute(self, context):
|
||||
layer2_objs = []
|
||||
other_objs = []
|
||||
x_angle = radians(self.x_angle)
|
||||
x_angle = self.x_angle
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
@@ -467,11 +436,11 @@ class DumbWallGenerator:
|
||||
self.collection = bpy.context.view_layer.active_layer_collection.collection
|
||||
self.collection_obj = bpy.data.objects.get(self.collection.name)
|
||||
self.width = self.layers["thickness"]
|
||||
self.height = props.extrusion_depth * self.unit_scale
|
||||
self.length = props.length * self.unit_scale
|
||||
self.height = props.extrusion_depth
|
||||
self.length = props.length
|
||||
self.rotation = 0.0
|
||||
self.location = Vector((0, 0, 0))
|
||||
self.x_angle = 0 if tool.Cad.is_x(props.x_angle, 0, tolerance=0.001) else radians(props.x_angle)
|
||||
self.x_angle = 0 if tool.Cad.is_x(props.x_angle, 0, tolerance=0.001) else props.x_angle
|
||||
|
||||
return self.derive_from_cursor()
|
||||
|
||||
@@ -1028,7 +997,7 @@ class DumbWallJoiner:
|
||||
|
||||
self.recreate_wall(element1, wall1, axis, body)
|
||||
|
||||
def set_length(self, wall1, length):
|
||||
def set_length(self, wall1, si_length):
|
||||
element1 = tool.Ifc.get_entity(wall1)
|
||||
if not element1:
|
||||
return
|
||||
@@ -1038,8 +1007,6 @@ class DumbWallJoiner:
|
||||
axis1 = tool.Model.get_wall_axis(wall1)
|
||||
axis = copy.deepcopy(axis1["reference"])
|
||||
body = copy.deepcopy(axis1["reference"])
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
si_length = unit_scale * length
|
||||
end = (wall1.matrix_world @ Vector((si_length, 0, 0))).to_2d()
|
||||
axis[1] = end
|
||||
body[1] = end
|
||||
|
||||
@@ -69,10 +69,12 @@ def update_simple_openings(element, opening_width, opening_height):
|
||||
context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
|
||||
|
||||
extrusion = shape_builder.extrude(
|
||||
shape_builder.rectangle(size=Vector([opening_width, 0.0, opening_height])),
|
||||
shape_builder.rectangle(size=Vector([opening_width, 0.0, opening_height]).xz),
|
||||
magnitude=thickness / unit_scale,
|
||||
position=Vector([0.0, -0.1 / unit_scale, 0.0]),
|
||||
extrusion_vector=Vector([0.0, 1.0, 0.0]),
|
||||
position_x_axis=V(1, 0, 0),
|
||||
position_z_axis=V(0, -1, 0),
|
||||
extrusion_vector=V(0, 0, -1),
|
||||
)
|
||||
|
||||
new_representation = shape_builder.get_representation(context, extrusion)
|
||||
@@ -480,7 +482,7 @@ class AddWindow(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
class CancelEditingWindow(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.cancel_editing_window"
|
||||
bl_label = "Cancel editing Window"
|
||||
bl_label = "Cancel Editing Window"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def _execute(self, context):
|
||||
@@ -508,7 +510,7 @@ class CancelEditingWindow(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
class FinishEditingWindow(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.finish_editing_window"
|
||||
bl_label = "Finish editing window"
|
||||
bl_label = "Finish Editing Window"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def _execute(self, context):
|
||||
|
||||
@@ -44,6 +44,7 @@ class BimTool(WorkSpaceTool):
|
||||
# ("mesh.add_wall", {"type": 'LEFTMOUSE', "value": 'PRESS'}, {"properties": []}),
|
||||
# ("bim.sync_modeling", {"type": 'MOUSEMOVE', "value": 'ANY'}, {"properties": []}),
|
||||
("bim.hotkey", {"type": "A", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_A")]}),
|
||||
("bim.hotkey", {"type": "B", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_B")]}),
|
||||
("bim.hotkey", {"type": "C", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_C")]}),
|
||||
("bim.hotkey", {"type": "E", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_E")]}),
|
||||
("bim.hotkey", {"type": "F", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_F")]}),
|
||||
@@ -57,6 +58,7 @@ class BimTool(WorkSpaceTool):
|
||||
("bim.hotkey", {"type": "V", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_V")]}),
|
||||
("bim.hotkey", {"type": "X", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_X")]}),
|
||||
("bim.hotkey", {"type": "Y", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_Y")]}),
|
||||
("bim.hotkey", {"type": "B", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_B")]}),
|
||||
("bim.hotkey", {"type": "D", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_D")]}),
|
||||
("bim.hotkey", {"type": "E", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_E")]}),
|
||||
("bim.hotkey", {"type": "O", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_O")]}),
|
||||
@@ -68,7 +70,17 @@ class BimTool(WorkSpaceTool):
|
||||
|
||||
|
||||
def add_layout_hotkey_operator(layout, text, hotkey, description):
|
||||
op = layout.operator("bim.hotkey", text=text)
|
||||
modifiers = {
|
||||
"A": "EVENT_ALT",
|
||||
"S": "EVENT_SHIFT",
|
||||
}
|
||||
modifier, key = hotkey.split("_")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon=modifiers[modifier])
|
||||
row.label(text="", icon=f"EVENT_{key}")
|
||||
|
||||
op = row.operator("bim.hotkey", text=text)
|
||||
op.hotkey = hotkey
|
||||
op.description = description
|
||||
return op
|
||||
@@ -101,7 +113,7 @@ class BimToolUI:
|
||||
|
||||
@classmethod
|
||||
def draw_create_object_interface(cls):
|
||||
if not AuthoringData.data["relating_types_ids"]:
|
||||
if not AuthoringData.data["relating_type_id"]:
|
||||
return
|
||||
if cls.props.ifc_class == "IfcWallType":
|
||||
row = cls.layout.row(align=True)
|
||||
@@ -132,10 +144,7 @@ class BimToolUI:
|
||||
row = cls.layout.row(align=True)
|
||||
row.prop(data=cls.props, property="rl2", text="RL")
|
||||
elif cls.props.ifc_class in ("IfcSpaceType"):
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_G")
|
||||
add_layout_hotkey_operator(row, "Generate", "S_G", bpy.ops.bim.generate_space.__doc__)
|
||||
add_layout_hotkey_operator(cls.layout, "Generate", "S_G", bpy.ops.bim.generate_space.__doc__)
|
||||
|
||||
@classmethod
|
||||
def draw_edit_object_interface(cls, context):
|
||||
@@ -155,52 +164,25 @@ class BimToolUI:
|
||||
op = row.operator("bim.change_extrusion_x_angle", icon="FILE_REFRESH", text="")
|
||||
op.x_angle = cls.props.x_angle
|
||||
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_E")
|
||||
row.operator("bim.hotkey", text="Extend").hotkey = "S_E"
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_T")
|
||||
row.operator("bim.hotkey", text="Butt").hotkey = "S_T"
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_Y")
|
||||
row.operator("bim.hotkey", text="Mitre").hotkey = "S_Y"
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_M")
|
||||
add_layout_hotkey_operator(row, "Merge", "S_M", bpy.ops.bim.merge_wall.__doc__)
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_F")
|
||||
add_layout_hotkey_operator(row, "Flip", "S_F", bpy.ops.bim.flip_wall.__doc__)
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_K")
|
||||
add_layout_hotkey_operator(row, "Split", "S_K", bpy.ops.bim.split_wall.__doc__)
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_R")
|
||||
row.operator("bim.hotkey", text="Rotate 90").hotkey = "S_R"
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_G")
|
||||
add_layout_hotkey_operator(row, "Regen", "S_G", bpy.ops.bim.recalculate_wall.__doc__)
|
||||
add_layout_hotkey_operator(cls.layout, "Extend", "S_E", "")
|
||||
add_layout_hotkey_operator(cls.layout, "Butt", "S_T", "")
|
||||
add_layout_hotkey_operator(cls.layout, "Mitre", "S_Y", "")
|
||||
|
||||
add_layout_hotkey_operator(cls.layout, "Merge", "S_M", bpy.ops.bim.merge_wall.__doc__)
|
||||
add_layout_hotkey_operator(cls.layout, "Flip", "S_F", bpy.ops.bim.flip_wall.__doc__)
|
||||
add_layout_hotkey_operator(cls.layout, "Split", "S_K", bpy.ops.bim.split_wall.__doc__)
|
||||
add_layout_hotkey_operator(cls.layout, "Rotate 90", "S_R", bpy.ops.bim.rotate_90.__doc__)
|
||||
add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.recalculate_wall.__doc__)
|
||||
row.operator("bim.join_wall", icon="X", text="").join_type = ""
|
||||
|
||||
elif AuthoringData.data["active_material_usage"] == "LAYER3":
|
||||
# unnecessary check because BIM Tool is not available in EDIT mode?
|
||||
if context.active_object.mode == "OBJECT":
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_E")
|
||||
row.operator("bim.hotkey", text="Edit Profile").hotkey = "S_E"
|
||||
add_layout_hotkey_operator(cls.layout, "Edit Profile", "S_E", "")
|
||||
|
||||
row = cls.layout.row(align=True)
|
||||
row.prop(data=cls.props, property="x_angle", text="X Angle")
|
||||
op = row.operator("bim.change_extrusion_x_angle", icon="FILE_REFRESH", text="")
|
||||
op.x_angle = cls.props.x_angle
|
||||
|
||||
elif AuthoringData.data["active_material_usage"] == "PROFILE":
|
||||
row = cls.layout.row(align=True)
|
||||
row.prop(data=cls.props, property="cardinal_point", text="Axis")
|
||||
@@ -215,32 +197,26 @@ class BimToolUI:
|
||||
op = row.operator("bim.change_profile_depth", icon="FILE_REFRESH", text="")
|
||||
op.depth = cls.props.extrusion_depth
|
||||
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_E")
|
||||
row.operator("bim.hotkey", text="Extend").hotkey = "S_E"
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_ALT")
|
||||
row.label(text="", icon="EVENT_E")
|
||||
row.operator("bim.hotkey", text="Edit Axis").hotkey = "A_E"
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_T")
|
||||
row.operator("bim.hotkey", text="Butt").hotkey = "S_T"
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_Y")
|
||||
row.operator("bim.hotkey", text="Mitre").hotkey = "S_Y"
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_R")
|
||||
row.operator("bim.hotkey", text="Rotate 90").hotkey = "S_R"
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_G")
|
||||
add_layout_hotkey_operator(row, "Regen", "S_G", bpy.ops.bim.recalculate_profile.__doc__)
|
||||
add_layout_hotkey_operator(cls.layout, "Extend", "S_E", "")
|
||||
add_layout_hotkey_operator(cls.layout, "Edit Axis", "A_E", "")
|
||||
add_layout_hotkey_operator(cls.layout, "Butt", "S_T", "")
|
||||
add_layout_hotkey_operator(cls.layout, "Mitre", "S_Y", "")
|
||||
add_layout_hotkey_operator(cls.layout, "Rotate 90", "S_R", bpy.ops.bim.rotate_90.__doc__)
|
||||
add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.recalculate_profile.__doc__)
|
||||
row.operator("bim.extend_profile", icon="X", text="").join_type = ""
|
||||
|
||||
elif (
|
||||
(RailingData.is_loaded or not RailingData.load())
|
||||
and RailingData.data["parameters"]
|
||||
and not context.active_object.BIMRailingProperties.is_editing_path
|
||||
):
|
||||
# NOTE: should be above "active_representation_type" = "SweptSolid" check
|
||||
# because it could be a SweptSolid too
|
||||
add_layout_hotkey_operator(cls.layout, "Edit Railing Path", "S_E", "")
|
||||
|
||||
elif AuthoringData.data["active_representation_type"] == "SweptSolid":
|
||||
add_layout_hotkey_operator(cls.layout, "Edit Profile", "S_E", "")
|
||||
|
||||
elif AuthoringData.data["active_class"] in (
|
||||
"IfcWindow",
|
||||
"IfcWindowStandardCase",
|
||||
@@ -254,20 +230,11 @@ class BimToolUI:
|
||||
row = cls.layout.row(align=True)
|
||||
row.prop(data=cls.props, property="rl1", text="RL")
|
||||
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_G")
|
||||
add_layout_hotkey_operator(row, "Regen", "S_G", bpy.ops.bim.recalculate_fill.__doc__)
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_F")
|
||||
row.operator("bim.hotkey", text="Flip").hotkey = "S_F"
|
||||
add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.recalculate_fill.__doc__)
|
||||
add_layout_hotkey_operator(cls.layout, "Flip", "S_F", "")
|
||||
|
||||
elif AuthoringData.data["active_class"] in ("IfcSpace",):
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_G")
|
||||
add_layout_hotkey_operator(row, "Regen", "S_G", bpy.ops.bim.generate_space.__doc__)
|
||||
add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.generate_space.__doc__)
|
||||
|
||||
elif AuthoringData.data["active_class"] in (
|
||||
"IfcCableCarrierSegmentType",
|
||||
@@ -275,39 +242,22 @@ class BimToolUI:
|
||||
"IfcDuctSegmentType",
|
||||
"IfcPipeSegmentType",
|
||||
):
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="Extend", icon="EVENT_E")
|
||||
|
||||
elif (
|
||||
(RailingData.is_loaded or not RailingData.load())
|
||||
and RailingData.data["parameters"]
|
||||
and not context.active_object.BIMRailingProperties.is_editing_path
|
||||
):
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_E")
|
||||
row.operator("bim.hotkey", text="Edit Railing Path").hotkey = "S_E"
|
||||
add_layout_hotkey_operator(cls.layout, "Extend", "S_E", "")
|
||||
|
||||
elif (
|
||||
(RoofData.is_loaded or not RoofData.load())
|
||||
and RoofData.data["parameters"]
|
||||
and not context.active_object.BIMRoofProperties.is_editing_path
|
||||
):
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_E")
|
||||
row.operator("bim.hotkey", text="Edit Roof Path").hotkey = "S_E"
|
||||
add_layout_hotkey_operator(cls.layout, "Edit Roof Path", "S_E", "")
|
||||
|
||||
elif DecoratorData.get_ifc_text_data(bpy.context.object):
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_E")
|
||||
row.operator("bim.hotkey", text="Edit text").hotkey = "S_E"
|
||||
add_layout_hotkey_operator(cls.layout, "Edit Text", "S_E", "")
|
||||
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_O")
|
||||
|
||||
if len(context.selected_objects) == 2:
|
||||
row.operator("bim.add_opening", text="Apply Void")
|
||||
else:
|
||||
@@ -320,31 +270,22 @@ class BimToolUI:
|
||||
else:
|
||||
row.operator("bim.show_openings", icon="HIDE_OFF", text="")
|
||||
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="Align")
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="Align Exterior", icon="EVENT_X")
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="Align Centerline", icon="EVENT_C")
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="Align Interior", icon="EVENT_V")
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="Mirror", icon="EVENT_M")
|
||||
row.label(text="", icon="EVENT_B")
|
||||
row.prop(cls.props, "boundary_class", text="")
|
||||
row.operator("bim.add_boundary", text="Add Boundary")
|
||||
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="Mode")
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_ALT")
|
||||
row.label(text="", icon="EVENT_O")
|
||||
add_layout_hotkey_operator(row, "Void", "A_O", "Show / edit openings")
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_ALT")
|
||||
row.label(text="", icon="EVENT_D")
|
||||
row.operator("bim.hotkey", text="Decomposition").hotkey = "A_D"
|
||||
cls.layout.row(align=True).label(text="Align")
|
||||
add_layout_hotkey_operator(cls.layout, "Align Exterior", "S_X", "")
|
||||
add_layout_hotkey_operator(cls.layout, "Align Centerline", "S_C", "")
|
||||
add_layout_hotkey_operator(cls.layout, "Align Interior", "S_V", "")
|
||||
add_layout_hotkey_operator(cls.layout, "Mirror", "S_M", bpy.ops.bim.mirror_elements.__doc__)
|
||||
|
||||
cls.layout.row(align=True).label(text="Mode")
|
||||
add_layout_hotkey_operator(cls.layout, "Void", "A_O", "Toggle openings")
|
||||
add_layout_hotkey_operator(cls.layout, "Decomposition", "A_D", "Select decomposition")
|
||||
add_layout_hotkey_operator(cls.layout, "Boundaries", "A_B", "Toggle boundaries")
|
||||
|
||||
@classmethod
|
||||
def draw_header_interface(cls):
|
||||
@@ -369,7 +310,7 @@ class BimToolUI:
|
||||
prop_with_search(row, cls.props, "ifc_class", text="")
|
||||
|
||||
row = cls.layout.row(align=True)
|
||||
if AuthoringData.data["relating_types_ids"]:
|
||||
if AuthoringData.data["relating_type_id"]:
|
||||
row.label(text="", icon="FILE_3D")
|
||||
prop_with_search(row, cls.props, "relating_type_id", text="")
|
||||
else:
|
||||
@@ -475,6 +416,16 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if not bpy.context.selected_objects:
|
||||
return
|
||||
|
||||
# NOTE: placing it before the other operations because railing can also be SweptSolid
|
||||
# and it might conflict with one of the conditions below
|
||||
if (
|
||||
(RailingData.is_loaded or not RailingData.load())
|
||||
and RailingData.data["parameters"]
|
||||
and not bpy.context.active_object.BIMRailingProperties.is_editing_path
|
||||
):
|
||||
bpy.ops.bim.enable_editing_railing_path()
|
||||
return
|
||||
|
||||
selected_usages = {}
|
||||
for obj in bpy.context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
@@ -483,8 +434,12 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
continue
|
||||
usage = tool.Model.get_usage_type(element)
|
||||
if not usage:
|
||||
obj.select_set(False)
|
||||
continue
|
||||
representation = tool.Geometry.get_active_representation(obj)
|
||||
if representation and representation.RepresentationType == "SweptSolid":
|
||||
usage = "SWEPTSOLID"
|
||||
else:
|
||||
obj.select_set(False)
|
||||
continue
|
||||
selected_usages.setdefault(usage, []).append(obj)
|
||||
|
||||
if len(bpy.context.selected_objects) == 1:
|
||||
@@ -498,7 +453,9 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
elif self.active_material_usage == "PROFILE":
|
||||
# Extend PROFILE to cursor
|
||||
bpy.ops.bim.extend_profile(join_type="T")
|
||||
|
||||
else:
|
||||
# Edit SWEPTSOLID profile (assuming single profile for now)
|
||||
bpy.ops.bim.enable_editing_extrusion_profile()
|
||||
elif self.active_material_usage == "LAYER2" and selected_usages.get("PROFILE", []):
|
||||
# Extend PROFILEs to LAYER2
|
||||
[o.select_set(False) for o in selected_usages.get("LAYER3", [])]
|
||||
@@ -523,15 +480,6 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
[o.select_set(False) for o in selected_usages.get("LAYER2", [])]
|
||||
bpy.ops.bim.extend_profile(join_type="T")
|
||||
|
||||
elif (
|
||||
(RailingData.is_loaded or not RailingData.load())
|
||||
and RailingData.data["parameters"]
|
||||
and not bpy.context.active_object.BIMRailingProperties.is_editing_path
|
||||
):
|
||||
# undo the unselection done above because railing has no usage type 🙃
|
||||
bpy.context.object.select_set(True)
|
||||
bpy.ops.bim.enable_editing_railing_path()
|
||||
|
||||
elif (
|
||||
(RoofData.is_loaded or not RoofData.load())
|
||||
and RoofData.data["parameters"]
|
||||
@@ -573,7 +521,13 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if self.active_material_usage == "LAYER2":
|
||||
bpy.ops.bim.merge_wall()
|
||||
else:
|
||||
bpy.ops.bim.mirror_elements()
|
||||
if len(bpy.context.selected_objects) == 1:
|
||||
self.report(
|
||||
{"INFO"},
|
||||
"At least two objects must be selected: an object to be mirrored, and a mirror axis as the active object.",
|
||||
)
|
||||
else:
|
||||
bpy.ops.bim.mirror_elements()
|
||||
|
||||
def hotkey_S_R(self):
|
||||
if not bpy.context.selected_objects:
|
||||
@@ -624,6 +578,9 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
elif self.active_material_usage == "PROFILE":
|
||||
bpy.ops.bim.extend_profile(join_type="V")
|
||||
|
||||
def hotkey_S_B(self):
|
||||
bpy.ops.bim.add_boundary()
|
||||
|
||||
def hotkey_S_O(self):
|
||||
if len(bpy.context.selected_objects) == 2:
|
||||
bpy.ops.bim.add_opening()
|
||||
@@ -634,6 +591,14 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
self.props.y = self.y
|
||||
self.props.z = self.z
|
||||
|
||||
def hotkey_A_B(self):
|
||||
if not bpy.context.selected_objects:
|
||||
return
|
||||
if AuthoringData.data["has_visible_boundaries"]:
|
||||
bpy.ops.bim.hide_boundaries()
|
||||
else:
|
||||
bpy.ops.bim.show_boundaries()
|
||||
|
||||
def hotkey_A_D(self):
|
||||
if not bpy.context.selected_objects:
|
||||
return
|
||||
|
||||
@@ -102,7 +102,7 @@ class ExecuteIfcPatch(bpy.types.Operator):
|
||||
|
||||
class UpdateIfcPatchArguments(bpy.types.Operator):
|
||||
bl_idname = "bim.update_ifc_patch_arguments"
|
||||
bl_label = "Update IFC Patch arguments"
|
||||
bl_label = "Update IFC Patch Arguments"
|
||||
recipe: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
@@ -136,6 +136,7 @@ class EnableEditingArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
props = context.scene.BIMProfileProperties
|
||||
profile = tool.Ifc.get().by_id(props.active_profile_id)
|
||||
obj = tool.Model.import_profile(profile)
|
||||
tool.Ifc.link(profile, obj)
|
||||
bpy.context.scene.collection.objects.link(obj)
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
|
||||
@@ -812,7 +812,6 @@ class ExportIFC(bpy.types.Operator):
|
||||
bl_idname = "export_ifc.bim"
|
||||
bl_label = "Export IFC"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Export the IFC project"
|
||||
filename_ext = ".ifc"
|
||||
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml;*.ifcjson", options={"HIDDEN"})
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
@@ -892,6 +891,12 @@ class ExportIFC(bpy.types.Operator):
|
||||
blenderbim.bim.handler.purge_module_data()
|
||||
return {"FINISHED"}
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
if properties.should_save_as:
|
||||
return "Export the IFC project to a selected file"
|
||||
return "Export the IFC project to this file"
|
||||
|
||||
|
||||
class ImportIFC(bpy.types.Operator):
|
||||
bl_idname = "import_ifc.bim"
|
||||
|
||||
@@ -96,7 +96,10 @@ class MaterialPsetsData(Data):
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.data = {"psets": cls.psetqtos(tool.Ifc.get_entity(bpy.context.active_object.active_material))}
|
||||
cls.data = {
|
||||
"ifc_definition_id": bpy.context.active_object.active_material.BIMObjectProperties.ifc_definition_id,
|
||||
"psets": cls.psetqtos(tool.Ifc.get_entity(bpy.context.active_object.active_material)),
|
||||
}
|
||||
cls.is_loaded = True
|
||||
|
||||
|
||||
|
||||
@@ -248,6 +248,7 @@ class EditPset(bpy.types.Operator, Operator):
|
||||
|
||||
def _execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
objects = tool.Blender.get_selected_objects()
|
||||
props = get_pset_props(context, self.obj, self.obj_type)
|
||||
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, self.obj, self.obj_type)
|
||||
element = tool.Ifc.get().by_id(ifc_definition_id)
|
||||
@@ -275,29 +276,42 @@ class EditPset(bpy.types.Operator, Operator):
|
||||
e[value_name] for e in prop.enumerated_value.enumerated_values if e.is_selected
|
||||
]
|
||||
|
||||
if pset.is_a() in ("IfcPropertySet", "IfcMaterialProperties", "IfcProfileProperties"):
|
||||
ifcopenshell.api.run(
|
||||
"pset.edit_pset",
|
||||
self.file,
|
||||
pset=pset,
|
||||
name=props.active_pset_name,
|
||||
properties=properties,
|
||||
pset_template=blenderbim.bim.schema.ifc.psetqto.get_by_name(props.active_pset_name),
|
||||
)
|
||||
else:
|
||||
for key, value in properties.items():
|
||||
if isinstance(value, float):
|
||||
properties[key] = round(value, 4)
|
||||
ifcopenshell.api.run(
|
||||
"pset.edit_qto",
|
||||
self.file,
|
||||
qto=pset,
|
||||
name=props.active_pset_name,
|
||||
properties=properties,
|
||||
)
|
||||
bpy.ops.bim.load_cost_item_quantities()
|
||||
|
||||
for obj in objects:
|
||||
element = blenderbim.tool.Ifc.get_entity(obj)
|
||||
pset_list = element.IsDefinedBy
|
||||
copy_of_properties = properties.copy()
|
||||
|
||||
for rel in pset_list:
|
||||
rel_name = rel.RelatingPropertyDefinition.Name
|
||||
if rel.is_a("IfcRelDefinesByProperties") and rel_name == props.active_pset_name:
|
||||
rel_ID = rel.RelatingPropertyDefinition.id()
|
||||
rel_pset = rel.RelatingPropertyDefinition
|
||||
if rel_pset.is_a() in ("IfcPropertySet", "IfcMaterialProperties", "IfcProfileProperties"):
|
||||
ifcopenshell.api.run(
|
||||
"pset.edit_pset",
|
||||
self.file,
|
||||
pset=rel_pset,
|
||||
name=props.active_pset_name,
|
||||
properties=copy_of_properties,
|
||||
pset_template=blenderbim.bim.schema.ifc.psetqto.get_by_name(props.active_pset_name),
|
||||
)
|
||||
else:
|
||||
for key, value in copy_of_properties.items():
|
||||
if isinstance(value, float):
|
||||
copy_of_properties[key] = round(value, 4)
|
||||
ifcopenshell.api.run(
|
||||
"pset.edit_qto",
|
||||
self.file,
|
||||
qto=rel_pset,
|
||||
name=props.active_pset_name,
|
||||
properties=copy_of_properties,
|
||||
)
|
||||
if tool.Cost.has_schedules():
|
||||
tool.Cost.update_cost_items(pset=rel_pset)
|
||||
|
||||
bpy.ops.bim.disable_pset_editing(obj=self.obj, obj_type=self.obj_type)
|
||||
tool.Blender.update_viewport()
|
||||
|
||||
|
||||
class RemovePset(bpy.types.Operator, Operator):
|
||||
@@ -380,32 +394,21 @@ class CalculateQuantity(bpy.types.Operator):
|
||||
self.qto_calculator = QtoCalculator()
|
||||
obj = context.active_object
|
||||
prop = obj.PsetProperties.properties.get(self.prop)
|
||||
prop.metadata.float_value = self.calculate_quantity(obj, context)
|
||||
quantity = self.calculate_quantity(obj, context)
|
||||
|
||||
if quantity is None:
|
||||
self.report({"ERROR"}, "Could not calculate quantity")
|
||||
return {"CANCELLED"}
|
||||
|
||||
prop.metadata.float_value = quantity
|
||||
return {"FINISHED"}
|
||||
|
||||
def calculate_quantity(self, obj, context):
|
||||
quantity = self.qto_calculator.calculate_quantity(obj.PsetProperties.active_pset_name, self.prop, obj)
|
||||
prefix, name = self.get_blender_prefix_name(context)
|
||||
quantity = ifcopenshell.util.unit.convert(quantity, None, "METRE", prefix, name)
|
||||
if quantity is None:
|
||||
return
|
||||
return round(quantity, 3)
|
||||
|
||||
def get_prefix_name(self, value):
|
||||
if "/" in value:
|
||||
return value.split("/")
|
||||
return None, value
|
||||
|
||||
def get_blender_prefix_name(self, context):
|
||||
unit_settings = context.scene.unit_settings
|
||||
if unit_settings.system == "IMPERIAL":
|
||||
if unit_settings.length_unit == "INCHES":
|
||||
return None, "inch"
|
||||
elif unit_settings.length_unit == "FEET":
|
||||
return None, "foot"
|
||||
elif unit_settings.system == "METRIC":
|
||||
if unit_settings.length_unit == "METERS":
|
||||
return None, "METRE"
|
||||
return unit_settings.length_unit[0 : -len("METERS")], "METRE"
|
||||
|
||||
|
||||
class GuessQuantity(bpy.types.Operator):
|
||||
bl_idname = "bim.guess_quantity"
|
||||
@@ -422,35 +425,7 @@ class GuessQuantity(bpy.types.Operator):
|
||||
|
||||
def guess_quantity(self, obj, context):
|
||||
quantity = self.qto_calculator.guess_quantity(self.prop, [p.name for p in obj.PsetProperties.properties], obj)
|
||||
if "area" in self.prop.lower():
|
||||
if context.scene.BIMProperties.area_unit:
|
||||
prefix, name = self.get_prefix_name(context.scene.BIMProperties.area_unit)
|
||||
quantity = ifcopenshell.util.unit.convert(quantity, None, "SQUARE_METRE", prefix, name)
|
||||
elif "volume" in self.prop.lower():
|
||||
if context.scene.BIMProperties.volume_unit:
|
||||
prefix, name = self.get_prefix_name(context.scene.BIMProperties.volume_unit)
|
||||
quantity = ifcopenshell.util.unit.convert(quantity, None, "CUBIC_METRE", prefix, name)
|
||||
else:
|
||||
prefix, name = self.get_blender_prefix_name(context)
|
||||
quantity = ifcopenshell.util.unit.convert(quantity, None, "METRE", prefix, name)
|
||||
return round(quantity, 3)
|
||||
|
||||
def get_prefix_name(self, value):
|
||||
if "/" in value:
|
||||
return value.split("/")
|
||||
return None, value
|
||||
|
||||
def get_blender_prefix_name(self, context):
|
||||
unit_settings = context.scene.unit_settings
|
||||
if unit_settings.system == "IMPERIAL":
|
||||
if unit_settings.length_unit == "INCHES":
|
||||
return None, "inch"
|
||||
elif unit_settings.length_unit == "FEET":
|
||||
return None, "foot"
|
||||
elif unit_settings.system == "METRIC":
|
||||
if unit_settings.length_unit == "METERS":
|
||||
return None, "METRE"
|
||||
return unit_settings.length_unit[0 : -len("METERS")], "METRE"
|
||||
return round(quantity, 3) if quantity is not None else None
|
||||
|
||||
|
||||
class CopyPropertyToSelection(bpy.types.Operator, Operator):
|
||||
@@ -494,7 +469,7 @@ class BIM_OT_add_property_to_edit(bpy.types.Operator):
|
||||
|
||||
|
||||
class BIM_OT_remove_property_to_edit(bpy.types.Operator):
|
||||
bl_label = "Remove property to be renamed"
|
||||
bl_label = "Remove Property to Be Renamed"
|
||||
bl_idname = "bim.remove_property_to_edit"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
index: bpy.props.IntProperty()
|
||||
@@ -510,7 +485,7 @@ class BIM_OT_remove_property_to_edit(bpy.types.Operator):
|
||||
|
||||
|
||||
class BIM_OT_clear_list(bpy.types.Operator):
|
||||
bl_label = "Clear list of properties"
|
||||
bl_label = "Clear List of Properties"
|
||||
bl_idname = "bim.clear_list"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
option: bpy.props.StringProperty()
|
||||
@@ -558,7 +533,7 @@ class BIM_OT_rename_parameters(bpy.types.Operator):
|
||||
|
||||
|
||||
class BIM_OT_add_edit_custom_property(bpy.types.Operator):
|
||||
bl_label = "Add or edit a custom property"
|
||||
bl_label = "Add or Edit a Custom Property"
|
||||
bl_idname = "bim.add_edit_custom_property"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
index: bpy.props.IntProperty()
|
||||
@@ -615,7 +590,7 @@ class BIM_OT_add_edit_custom_property(bpy.types.Operator):
|
||||
|
||||
|
||||
class BIM_OT_bulk_remove_psets(bpy.types.Operator):
|
||||
bl_label = "Bulk remove psets from selected objects"
|
||||
bl_label = "Bulk Remove Psets from Selected Objects"
|
||||
bl_idname = "bim.bulk_remove_psets"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Bulk remove psets from selected objects"
|
||||
|
||||