mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 09:21:46 +00:00
Compare commits
390 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c5d38227fc | |||
| e98ff136c2 | |||
| 02ebc80525 | |||
| 66431158b5 | |||
| dd044c8516 | |||
| 55763ea7df | |||
| 8fe92e810f | |||
| c26988b292 | |||
| 46e70c92c5 | |||
| 325c3665d9 | |||
| 9a80dc14e9 | |||
| 6bda066e1b | |||
| 912b01587b | |||
| 5d4d953320 | |||
| 11ed8cc787 | |||
| d1a7b99eda | |||
| 85bdeaeef4 | |||
| a5f0de4d19 | |||
| cb3392ec70 | |||
| 1dbe42061d | |||
| a024d5c07f | |||
| ea5e3c30b6 | |||
| ebfc8ef619 | |||
| c0d8467154 | |||
| d0adbab9c2 | |||
| 8e80f456cb | |||
| 8cd9fb9015 | |||
| d9fdc0908a | |||
| a218ce5160 | |||
| b3a6b8e07f | |||
| 52700452d9 | |||
| aab011f7bf | |||
| 5f129e2357 | |||
| 4e76f36af1 | |||
| 6f3ff51d91 | |||
| 0de0422c2e | |||
| 98de86e0ba | |||
| 142e41b3ad | |||
| 1e0dc7749d | |||
| 0f29de0391 | |||
| 9933cc1b20 | |||
| fa6192e9bc | |||
| bfd4559da8 | |||
| 01f52e069e | |||
| 2fbc9e4424 | |||
| ba97da20eb | |||
| 44f5be142f | |||
| 2f951d352b | |||
| 8b23c615c3 | |||
| 7c694e5b3e | |||
| 8eeb212003 | |||
| 7bf8d58cde | |||
| b6e24559e5 | |||
| 0d87d5927e | |||
| dfd5cc4728 | |||
| 20b7ee009d | |||
| 19a3f56941 | |||
| 3b298d44c1 | |||
| 038d3bc41c | |||
| 235e89ae14 | |||
| e2de9efc33 | |||
| 8126085037 | |||
| bbe14834e4 | |||
| 45f2d301b3 | |||
| 2ecd11d71f | |||
| b42f072bd5 | |||
| 3b3668eedf | |||
| 7f7f353131 | |||
| 53594de1ff | |||
| 4fbbfb5191 | |||
| 4e1daea838 | |||
| d8ff83da73 | |||
| a7712a923f | |||
| 01b5f8f237 | |||
| b9ad15c50f | |||
| db06cff997 | |||
| f0028c0094 | |||
| 1c954801b0 | |||
| 973e4f0064 | |||
| be5d291bd9 | |||
| cbec56c2f0 | |||
| 880d0ba753 | |||
| db85689364 | |||
| 48a0d8c508 | |||
| df4cd493ee | |||
| 74132dcd49 | |||
| 5cf1dd4249 | |||
| 29bd30d3bf | |||
| 3f8a0a91b3 | |||
| 416a572664 | |||
| 388c72360d | |||
| b841afc2e4 | |||
| f10d340254 | |||
| 0d0b91ee9b | |||
| 8032333a17 | |||
| 712ec6def3 | |||
| 0d43ef0736 | |||
| b318509cba | |||
| f3b541f00a | |||
| 4f64dcdbd4 | |||
| 68acf84e26 | |||
| aaceb923ad | |||
| 074de2741a | |||
| 0e4bc320cb | |||
| 9e9412bec0 | |||
| 7e92471ac9 | |||
| 078a7eb290 | |||
| df3876b438 | |||
| d414de450b | |||
| 9e89bb5044 | |||
| aa0c42e1ec | |||
| a4daa0e3d9 | |||
| 162ac39570 | |||
| 8c73fa69a1 | |||
| 159eeeb3a4 | |||
| eb0873996f | |||
| c85826ef2e | |||
| 627cf63a4c | |||
| 744c08e13e | |||
| dd7a6a80eb | |||
| ab807e3b9b | |||
| c35be50c83 | |||
| 7ff98d2918 | |||
| 35d66f5d8c | |||
| 3f018a4195 | |||
| 578e15adb2 | |||
| c4474455e9 | |||
| 8f43854cbf | |||
| 787149da13 | |||
| 3b31f6932a | |||
| 6c9e130ca8 | |||
| f38952dc51 | |||
| 24eb149221 | |||
| ce8598a0b4 | |||
| fb9cf75412 | |||
| 68f5df73b9 | |||
| e068c523ec | |||
| aa182bf047 | |||
| 6e9639446d | |||
| c48c1c3b32 | |||
| 14224ac538 | |||
| 58be92d3fc | |||
| 7107a91f6f | |||
| 438ed22f8f | |||
| a9dfc9a9fd | |||
| f8b09b3525 | |||
| f1ab02b311 | |||
| 25df2f6414 | |||
| 42719d3fe3 | |||
| 1f6d23c19c | |||
| 94db9ec522 | |||
| a47c57c22c | |||
| 8f4bc0ea00 | |||
| dd70b09fb1 | |||
| c930050da6 | |||
| 55a7a72e61 | |||
| ca2ae36d41 | |||
| 81b4e42ca2 | |||
| 55e61b55d8 | |||
| a99529cb1e | |||
| bf43efe509 | |||
| da7f5b015b | |||
| 3de9915389 | |||
| 418444a196 | |||
| 171251b27d | |||
| 9fef920a6f | |||
| 5bff837a61 | |||
| 05f6f4efab | |||
| e4f5b0e3d8 | |||
| 49b733f660 | |||
| 36ed618adf | |||
| 48042011e2 | |||
| 0e1939a871 | |||
| bea1e61739 | |||
| 72cd82f450 | |||
| befa059591 | |||
| 7229f7271c | |||
| 9c5970e757 | |||
| 2fe3f0a9df | |||
| 3929f02e94 | |||
| 1abf80c310 | |||
| 2c18c89807 | |||
| 1b81fa5aa1 | |||
| baa08b3221 | |||
| b9a9abd30d | |||
| 33536957d8 | |||
| d092fd45ba | |||
| 78c748df5d | |||
| 3d76de3041 | |||
| ed3e9f3347 | |||
| eb836436b9 | |||
| c34a9c52d0 | |||
| 88d96f7d57 | |||
| 69e447be1c | |||
| df09acfa31 | |||
| 6343f77c0d | |||
| 285fc2269e | |||
| 68a1da9190 | |||
| ceae7d4fdb | |||
| 371daa6c80 | |||
| 948993e066 | |||
| ec4f0e5ad6 | |||
| ef56c6ce7e | |||
| 3c02da416e | |||
| 6660bf7618 | |||
| 000e5ec29a | |||
| ef8f4df4ab | |||
| dff70f2608 | |||
| 0d7967ff62 | |||
| 4fe459dab6 | |||
| 0e5e1f7873 | |||
| 82d4829c83 | |||
| a24fab6d92 | |||
| 748055dcf1 | |||
| 3904162360 | |||
| 7ca24ede4c | |||
| 1e59b10479 | |||
| cda61bff5b | |||
| 09a9753ca7 | |||
| 1621449143 | |||
| d0ef7f69b1 | |||
| 3df95b8970 | |||
| ddd4b0e262 | |||
| 6b05b6e7fb | |||
| 9dd728ce7e | |||
| 90b40ff0dc | |||
| 3c27c52c9f | |||
| 05d4dc5666 | |||
| edaabf5bd2 | |||
| ad79c63a37 | |||
| 9461bf2129 | |||
| f2097c0c5f | |||
| d838e1808d | |||
| 5ed54d9e7c | |||
| 78aba1c7e4 | |||
| 3faaee80ec | |||
| a847b8f4e4 | |||
| 24170a8a16 | |||
| 5438331fe1 | |||
| 3c957e0cbe | |||
| 10672f3e1a | |||
| fa90e420eb | |||
| 347bb2df4b | |||
| 986ffe66f8 | |||
| c1bedec354 | |||
| 182b6c7b07 | |||
| 22edbddd84 | |||
| 50f6f44d26 | |||
| e93a697179 | |||
| f1a5805b1c | |||
| 126f530af1 | |||
| 5dfbbb8d96 | |||
| 6962e505f9 | |||
| 5c2523e752 | |||
| 635dff73b4 | |||
| c85e4e7ceb | |||
| 7ed4301c6c | |||
| 1193e7025a | |||
| cc1ee19115 | |||
| e975eac4a7 | |||
| 779fc35228 | |||
| 8e59282fe0 | |||
| 11c8d47909 | |||
| 2661999f3e | |||
| e4bfb05875 | |||
| fe14988d50 | |||
| d45bef5e70 | |||
| 82d03b98c6 | |||
| 9c1ecf3f2c | |||
| 8f6be60c6e | |||
| 81a1bd9206 | |||
| 9f7dffb29d | |||
| eee799bfc4 | |||
| 9665adb61d | |||
| e40019f3cb | |||
| b2f95bfa60 | |||
| be2c7a62fd | |||
| 38f5468f19 | |||
| 31dcbc0159 | |||
| bd81320c08 | |||
| 0707f37247 | |||
| 0993736cea | |||
| cc3e2204e2 | |||
| 8ac5f0318b | |||
| 270a61e73a | |||
| 1c057b1ce1 | |||
| e74e824417 | |||
| 73d1de5dcf | |||
| 4549be0d05 | |||
| e3f2f2175d | |||
| a604cf2b2a | |||
| d6b4c1f454 | |||
| d3258693b2 | |||
| 9eba3b6eaa | |||
| 7b75364feb | |||
| 4a4a0f09ef | |||
| 6d14c2d448 | |||
| 57edd47ed7 | |||
| aefcedf754 | |||
| 2c685c846f | |||
| 6536f7b56b | |||
| 9d3881b00e | |||
| baffbd6a32 | |||
| 9e4702c102 | |||
| b63f3f335c | |||
| 29b0bcb6b9 | |||
| 62fa0bb218 | |||
| 9e67565c71 | |||
| dee4cf591e | |||
| bc5d6ee113 | |||
| 5af3b537da | |||
| 4b159d400c | |||
| f3c012660a | |||
| b5b8b5df26 | |||
| 6d38d9b538 | |||
| ef11ae6540 | |||
| a3802c25c0 | |||
| 377bcfe6da | |||
| 63a5c0986f | |||
| 8320623210 | |||
| 90c4a2fd41 | |||
| 4ee66caaf9 | |||
| 23251de6f1 | |||
| 37287bac7e | |||
| 43a7340f5d | |||
| 2363b5e167 | |||
| 62bf8c2287 | |||
| a5a958d6e2 | |||
| 812d8508f4 | |||
| c40cc3f740 | |||
| 56040951f1 | |||
| 39b1a65dc5 | |||
| 438f35fe88 | |||
| 9240518066 | |||
| 27b71a9d07 | |||
| 0f1d675bef | |||
| 19345ff538 | |||
| 7b28bc9c68 | |||
| d67eae55df | |||
| 8e375dcf73 | |||
| a34d0cabfc | |||
| a668743d10 | |||
| b13cbbb244 | |||
| b6ae5fd344 | |||
| 1c85034e19 | |||
| b0d1823968 | |||
| f0e03c79df | |||
| 6109cdf41b | |||
| ec0873d527 | |||
| 63223172b4 | |||
| 9778253d21 | |||
| 0140b4ded1 | |||
| dca60b83a9 | |||
| 42e683375c | |||
| 66e07c2f69 | |||
| 2b067b9dec | |||
| c22d7d2f41 | |||
| ea3d489c24 | |||
| 97683a14ef | |||
| d79b912e0e | |||
| 7ec8f8b03e | |||
| 58199ef7c9 | |||
| d5c46514d3 | |||
| f400973557 | |||
| 148d6e7b1b | |||
| 74dec650a7 | |||
| 5f6e003e4e | |||
| d3df22b44b | |||
| 257208d55b | |||
| d8b9b09d14 | |||
| 6f9e14ceb0 | |||
| d854984dda | |||
| 2951721886 | |||
| 02801b32a6 | |||
| b243e04fd2 | |||
| 02902d9a41 | |||
| 5c33034455 | |||
| 7541293a3c | |||
| da7d4dac7a | |||
| 507ab8ec11 | |||
| e93ff7a6b1 | |||
| 098ae2b69f | |||
| a58a10e93d | |||
| cc8f103465 | |||
| 339ab9eeab | |||
| 6c29c53a72 | |||
| 59e11aa804 | |||
| 155a8b79da | |||
| b46ccc9b78 | |||
| 4f1e21dfb0 |
@@ -88,7 +88,6 @@ jobs:
|
||||
sudo /usr/bin/python -m pip install networkx
|
||||
sudo /usr/bin/python -m pip install tabulate
|
||||
sudo /usr/bin/python -m pip install python-dateutil
|
||||
sudo /usr/bin/python -m pip install mathutils
|
||||
|
||||
- name: Test
|
||||
run: |
|
||||
@@ -96,4 +95,4 @@ jobs:
|
||||
sudo /usr/bin/python tests.py
|
||||
cd ../src/ifcopenshell-python
|
||||
mv ifcopenshell ifcopenshell-local # Force testing on installed module
|
||||
make test
|
||||
make test-safe
|
||||
|
||||
@@ -114,7 +114,6 @@ jobs:
|
||||
sudo /usr/bin/python -m pip install https://github.com/Andrej730/aud/archive/refs/heads/master-reduced-size.zip
|
||||
sudo /usr/bin/python -m pip install tabulate
|
||||
sudo /usr/bin/python -m pip install python-dateutil
|
||||
sudo /usr/bin/python -m pip install mathutils
|
||||
|
||||
- name: Test
|
||||
run: |
|
||||
@@ -122,4 +121,4 @@ jobs:
|
||||
sudo /usr/bin/python tests.py
|
||||
cd ../src/ifcopenshell-python
|
||||
mv ifcopenshell ifcopenshell-local # Force testing on installed module
|
||||
make test
|
||||
make test-safe
|
||||
|
||||
@@ -55,7 +55,6 @@ Those marked with an asterisk are part of IfcOpenShell.
|
||||
| ifcblender | Historic Blender IFC import add-on | LGPL-3.0-or-later\* |
|
||||
| ifccityjson | Convert CityJSON to IFC | LGPL-3.0-or-later |
|
||||
| ifcclash | Clash detection library and CLI app | LGPL-3.0-or-later |
|
||||
| ifccobie | Extract IFC data for COBie handover requirements | LGPL-3.0-or-later |
|
||||
| ifcconvert | CLI app to convert IFC to many other formats | LGPL-3.0-or-later\* |
|
||||
| ifccsv | Library and CLI app to export and import schedules from IFC | LGPL-3.0-or-later |
|
||||
| ifcdiff | Compare changes between IFC models | LGPL-3.0-or-later |
|
||||
|
||||
+17
-3
@@ -58,6 +58,7 @@ option(BUILD_PACKAGE "" OFF)
|
||||
option(COLLADA_SUPPORT "Build IfcConvert with COLLADA support (requires OpenCOLLADA)." ON)
|
||||
option(GLTF_SUPPORT "Build IfcConvert with glTF support (requires json.hpp)." OFF)
|
||||
option(HDF5_SUPPORT "Enable HDF5 support (requires HDF5, zlib)" ON)
|
||||
option(WITH_PROJ "Enable output of Earth-Centered Earth-Fixed glTF output using the PROJ library" OFF)
|
||||
option(IFCXML_SUPPORT "Build IfcParse with ifcXML support (requires libxml2)." ON)
|
||||
option(USD_SUPPORT "Build IfcConvert with USD support (requires pixar's USD library)." OFF)
|
||||
|
||||
@@ -438,9 +439,13 @@ if(BUILD_IFCGEOM)
|
||||
if(WASM_BUILD)
|
||||
set(OPENCASCADE_LIBRARIES ${OPENCASCADE_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
|
||||
else()
|
||||
# OPENCASCADE_LIBRARIES repeated N times below in order to fix cyclic dependencies - use --start-group ... --end-group instead?
|
||||
# OPENCASCADE_LIBRARIES repeated N times below in order to fix cyclic dependencies
|
||||
# tfk: --start-group ... --end-group didn't work on the apple linker when last tested
|
||||
set(OPENCASCADE_LIBRARIES ${OPENCASCADE_LIBRARIES} ${OPENCASCADE_LIBRARIES} ${OPENCASCADE_LIBRARIES} ${OPENCASCADE_LIBRARIES} ${OPENCASCADE_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
|
||||
if(APPLE)
|
||||
set(OPENCASCADE_LIBRARIES ${OPENCASCADE_LIBRARIES} ${OPENCASCADE_LIBRARIES} ${OPENCASCADE_LIBRARIES} ${OPENCASCADE_LIBRARIES} ${OPENCASCADE_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
|
||||
else()
|
||||
set(OPENCASCADE_LIBRARIES -Wl,--start-group ${OPENCASCADE_LIBRARIES} -Wl,--end-group ${CMAKE_THREAD_LIBS_INIT})
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if (NOT APPLE AND NOT WIN32)
|
||||
@@ -738,7 +743,7 @@ if(NOT SCHEMA_VERSIONS)
|
||||
# super arbitrarily try to keep size down at least a little bit
|
||||
set(SCHEMA_VERSIONS "2x3" "4")
|
||||
else()
|
||||
set(SCHEMA_VERSIONS "2x3" "4" "4x1" "4x2" "4x3_rc1" "4x3_rc2" "4x3_rc3" "4x3_rc4" "4x3" "4x3_tc1" "4x3_add1")
|
||||
set(SCHEMA_VERSIONS "2x3" "4" "4x1" "4x2" "4x3_rc1" "4x3_rc2" "4x3_rc3" "4x3_rc4" "4x3" "4x3_tc1" "4x3_add1" "4x3_add2")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
@@ -934,6 +939,15 @@ if(BUILD_CONVERT OR BUILD_IFCPYTHON)
|
||||
add_library(Serializers ${SERIALIZERS_FILES})
|
||||
set_target_properties(Serializers PROPERTIES COMPILE_FLAGS "-DIFC_GEOM_EXPORTS" VERSION "${PROJECT_VERSION}" SOVERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}")
|
||||
|
||||
if (WITH_PROJ)
|
||||
target_compile_definitions(Serializers PRIVATE "WITH_PROJ")
|
||||
if (PROJ_STATIC)
|
||||
target_compile_definitions(Serializers PRIVATE "PROJ_DLL=")
|
||||
endif()
|
||||
target_include_directories(Serializers PRIVATE ${PROJ_INCLUDE_DIR} ${SQLITE_INCLUDE_DIR})
|
||||
target_link_libraries(Serializers ${PROJ_LIBRARIES})
|
||||
endif()
|
||||
|
||||
target_link_libraries(Serializers ${SERIALIZER_SCHEMA_LIBRARIES} ${OPENCOLLADA_LIBRARIES} ${USD_LIBRARIES})
|
||||
|
||||
endif(BUILD_CONVERT OR BUILD_IFCPYTHON)
|
||||
|
||||
+3
-82
@@ -1,84 +1,5 @@
|
||||
# bcf
|
||||
|
||||
A simple Python implementation of BCF.
|
||||
Manipulation of BCF-XML is available via `bcfxml.py` and manipulation of BCF-API
|
||||
is available via `bcfapi.py`.
|
||||
|
||||
It tries to support BCF-XML version 2.1 and 3.0, and BCF-API 3.0.
|
||||
|
||||
## bcfxml
|
||||
|
||||
The `bcfxml.load` function lets you read a BCF-XML file.
|
||||
It takes care of using the right version based on the "bcf.version" file contained in the BCF package.
|
||||
|
||||
The BCF files are extracted and parsed on-demand, and edits are stored in memory until you call the `save` method.
|
||||
|
||||
```python
|
||||
from bcf.bcfxml import load
|
||||
|
||||
# Load a project
|
||||
with load("/path/to/file.bcf") as bcfxml:
|
||||
project = bcfxml.project
|
||||
print(project.name)
|
||||
|
||||
# To edit a project, just modify the object directly
|
||||
bcfxml.project.name = "New name"
|
||||
|
||||
# Get a dictionary of topics
|
||||
topics = bcfxml.topics
|
||||
|
||||
for topic_guid, topic_handler in bcfxml.topics.items():
|
||||
topic = topic_handler.topic
|
||||
print("Topic guid is", topic.guid)
|
||||
print("Topic title is", topic.title)
|
||||
|
||||
# Fetch extra data about a topic
|
||||
header = topic_handler.header
|
||||
comments = topic_handler.comments
|
||||
viewpoints = topic_handler.viewpoints
|
||||
|
||||
for comment in comments:
|
||||
print(comment.guid)
|
||||
print(comment.comment)
|
||||
print(comment.author)
|
||||
|
||||
# Get a particular topic
|
||||
topic = bcfxml.get_topic(guid)
|
||||
|
||||
# Modify a topic
|
||||
topic.title = "New title"
|
||||
|
||||
bcfxml.save()
|
||||
```
|
||||
|
||||
## bcfapi
|
||||
|
||||
The `bcfapi` module lets you interact with the BCF-API standard.
|
||||
|
||||
```python
|
||||
from bcf.v3.bcfapi import FoundationClient, BcfClient
|
||||
|
||||
foundation_client = FoundationClient("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET", "OPENCDE_BASEURL")
|
||||
auth_methods = foundation_client.get_auth_methods()
|
||||
|
||||
# Our library currently only implements the authorization_code flow
|
||||
if "authorization_code" in auth_methods:
|
||||
foundation_client.login()
|
||||
|
||||
bcf_client = BcfClient(foundation_client)
|
||||
|
||||
versions = foundation_client.get_versions()
|
||||
for version in versions:
|
||||
if "3.0" in versions:
|
||||
if version["api_id"] == "bcf" and version["version_id"] == "3.0":
|
||||
bcf_client.set_version(version)
|
||||
|
||||
data = bcf_client.get_projects()
|
||||
print(data)
|
||||
project_id = data[0]["project_id"]
|
||||
print(project_id)
|
||||
data = bcf_client.get_project(project_id)
|
||||
print(data)
|
||||
data = bcf_client.get_extensions(project_id)
|
||||
print(data)
|
||||
```
|
||||
A simple Python implementation of the BCF standard. Manipulation of BCF-XML is
|
||||
available via `bcfxml.py` and manipulation of BCF-API is available via
|
||||
`bcfapi.py`.
|
||||
|
||||
+5
-11
@@ -169,7 +169,7 @@ endif
|
||||
cp -r blenderbim/* dist/blenderbim/
|
||||
|
||||
# Provides IfcOpenShell Python functionality
|
||||
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-9cc1f5f-$(PLATFORM)64.zip
|
||||
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-6c9e130-$(PLATFORM)64.zip
|
||||
cd dist/working && unzip ifcopenshell-python*
|
||||
cp -r dist/working/ifcopenshell dist/blenderbim/libs/site/packages/
|
||||
|
||||
@@ -199,8 +199,10 @@ endif
|
||||
cp -r dist/working/IfcOpenShell-0.7.0/src/ifcbimtester/bimtester dist/blenderbim/libs/site/packages/
|
||||
# Provides IFCTester functionality
|
||||
cp -r dist/working/IfcOpenShell-0.7.0/src/ifctester/ifctester dist/blenderbim/libs/site/packages/
|
||||
# Provides IFCCOBie functionality
|
||||
cp -r dist/working/IfcOpenShell-0.7.0/src/ifccobie/* dist/blenderbim/libs/site/packages/
|
||||
# Provides IFCFM functionality
|
||||
cp -r dist/working/IfcOpenShell-0.7.0/src/ifcfm/ifcfm dist/blenderbim/libs/site/packages/
|
||||
# Provides bSDD functionality
|
||||
cp -r dist/working/IfcOpenShell-0.7.0/src/bsdd/* dist/blenderbim/libs/site/packages/
|
||||
# Provides IFCDiff functionality
|
||||
cp -r dist/working/IfcOpenShell-0.7.0/src/ifcdiff/* dist/blenderbim/libs/site/packages/
|
||||
# Provides IFCCSV functionality
|
||||
@@ -592,14 +594,6 @@ endif
|
||||
cd dist/working/parse_type-0.5.2/ && cp -r parse_type ../../blenderbim/libs/site/packages/
|
||||
rm -rf dist/working
|
||||
|
||||
# Required by IFCCOBie for XLSX support
|
||||
# TODO: see if we can replace this with openpyxl which does both read/write
|
||||
mkdir dist/working
|
||||
cd dist/working && wget https://files.pythonhosted.org/packages/0c/bc/82d6783f83f65f56d8b77d052773c4a2f952fa86385f0cd54e1e006658d7/XlsxWriter-1.2.9.tar.gz
|
||||
cd dist/working && tar -xzvf XlsxWriter*
|
||||
cd dist/working/XlsxWriter-1.2.9/ && cp -r xlsxwriter ../../blenderbim/libs/site/packages/
|
||||
rm -rf dist/working
|
||||
|
||||
# Required by augin
|
||||
mkdir dist/working
|
||||
cd dist/working && wget https://files.pythonhosted.org/packages/76/b4/b7baffbda025efd5dc8fcd8d2e953e3aa939c236a484084fa8f4c3588ee9/boto3-1.17.17.tar.gz
|
||||
|
||||
@@ -30,6 +30,7 @@ modules = {
|
||||
"project": None,
|
||||
"search": None,
|
||||
"bcf": None,
|
||||
"bsdd": None,
|
||||
"root": None,
|
||||
"unit": None,
|
||||
"model": None,
|
||||
@@ -44,7 +45,7 @@ modules = {
|
||||
"void": None,
|
||||
"aggregate": None,
|
||||
"geometry": None,
|
||||
"cobie": None,
|
||||
"fm": None,
|
||||
"resource": None,
|
||||
"cost": None,
|
||||
"sequence": None,
|
||||
@@ -77,6 +78,7 @@ modules = {
|
||||
"augin": None,
|
||||
"debug": None,
|
||||
"ifcgit": None,
|
||||
"covering": 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,
|
||||
@@ -123,26 +125,33 @@ classes = [
|
||||
ui.BIM_UL_generic,
|
||||
ui.BIM_UL_topics,
|
||||
ui.BIM_ADDON_preferences,
|
||||
# Project overview
|
||||
ui.BIM_PT_project_info,
|
||||
ui.BIM_PT_project_setup,
|
||||
ui.BIM_PT_geometry,
|
||||
ui.BIM_PT_tab_grouping_and_filtering,
|
||||
# Tabs panel
|
||||
ui.BIM_PT_tabs,
|
||||
# Project overview
|
||||
ui.BIM_PT_tab_project_info,
|
||||
ui.BIM_PT_tab_project_setup,
|
||||
ui.BIM_PT_tab_geometry,
|
||||
ui.BIM_PT_tab_stakeholders,
|
||||
ui.BIM_PT_tab_grouping_and_filtering,
|
||||
# Object information
|
||||
ui.BIM_PT_tab_object_metadata,
|
||||
ui.BIM_PT_tab_misc,
|
||||
# Geometry and materials
|
||||
ui.BIM_PT_tab_placement,
|
||||
ui.BIM_PT_tab_representations,
|
||||
ui.BIM_PT_tab_geometric_relationships,
|
||||
ui.BIM_PT_tab_parametric_geometry,
|
||||
ui.BIM_PT_tab_profiles,
|
||||
ui.BIM_PT_tab_materials,
|
||||
ui.BIM_PT_tab_styles,
|
||||
# Drawings and documents
|
||||
ui.BIM_PT_tab_sheets,
|
||||
ui.BIM_PT_tab_drawings,
|
||||
ui.BIM_PT_tab_schedules,
|
||||
ui.BIM_PT_tab_references,
|
||||
# Services and systems
|
||||
ui.BIM_PT_tab_services,
|
||||
ui.BIM_PT_tab_services_object,
|
||||
ui.BIM_PT_tab_zones,
|
||||
# Structural analysis
|
||||
ui.BIM_PT_tab_structural,
|
||||
# Construction scheduling
|
||||
@@ -210,8 +219,8 @@ def register():
|
||||
|
||||
wm = bpy.context.window_manager
|
||||
if wm.keyconfigs.addon:
|
||||
km = wm.keyconfigs.addon.keymaps.new(name='Window', space_type='EMPTY')
|
||||
kmi = km.keymap_items.new('bim.switch_tab', 'TAB', 'PRESS', ctrl=True)
|
||||
km = wm.keyconfigs.addon.keymaps.new(name="Window", space_type="EMPTY")
|
||||
kmi = km.keymap_items.new("bim.switch_tab", "TAB", "PRESS", ctrl=True)
|
||||
addon_keymaps.append((km, kmi))
|
||||
|
||||
global icons
|
||||
@@ -229,12 +238,14 @@ def register():
|
||||
global last_commit_hash
|
||||
try:
|
||||
import git
|
||||
|
||||
path = Path(__file__).resolve().parent
|
||||
repo = git.Repo(str(path), search_parent_directories=True)
|
||||
last_commit_hash = repo.head.object.hexsha
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def unregister():
|
||||
global icons
|
||||
|
||||
@@ -278,6 +289,7 @@ def unregister():
|
||||
"SCENE_PT_rigid_body_world",
|
||||
"SCENE_PT_audio",
|
||||
"SCENE_PT_keying_sets",
|
||||
"SCENE_PT_simulation",
|
||||
"SCENE_PT_custom_props",
|
||||
]:
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
ISO-10303-21;
|
||||
HEADER;
|
||||
FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1');
|
||||
FILE_NAME('','2023-09-28T23:00:50',(),(),'IfcOpenShell v0.7.0-fbd8ea1ed','IfcOpenShell v0.7.0-fbd8ea1ed','');
|
||||
FILE_SCHEMA(('IFC4'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1=IFCPROPERTYSETTEMPLATE('1TLQmgUKf0I8Zi07bm9Jqn',$,'ePSet_ProjectedCRS','',.PSET_OCCURRENCEDRIVEN.,'IfcProject',(#2,#3,#4,#5,#6,#7));
|
||||
#2=IFCSIMPLEPROPERTYTEMPLATE('2cH53PtVT3v88uf_uGFblG',$,'Name','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#3=IFCSIMPLEPROPERTYTEMPLATE('3JsReA6vXDqR7aNC$haRYW',$,'Description','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||
#4=IFCSIMPLEPROPERTYTEMPLATE('0u0uEsT5HFtuX5GvozO9ba',$,'GeodeticDatum','',.P_SINGLEVALUE.,'IfcIdentifier',$,$,$,$,$,.READWRITE.);
|
||||
#5=IFCSIMPLEPROPERTYTEMPLATE('1JgEX9Fsf0$ggxYltdkqiY',$,'VerticalDatum','',.P_SINGLEVALUE.,'IfcIdentifier',$,$,$,$,$,.READWRITE.);
|
||||
#6=IFCSIMPLEPROPERTYTEMPLATE('3ahTZjYrD0MAOo5IH6QuGH',$,'MapProjection','',.P_SINGLEVALUE.,'IfcIdentifier',$,$,$,$,$,.READWRITE.);
|
||||
#7=IFCSIMPLEPROPERTYTEMPLATE('3hqJR2ghD0GAti7q4sXKj7',$,'MapZone','',.P_SINGLEVALUE.,'IfcIdentifier',$,$,$,$,$,.READWRITE.);
|
||||
#8=IFCPROPERTYSETTEMPLATE('1jGHTycOj0vA8xHfRl4TNs',$,'ePSet_MapConversion','',.PSET_OCCURRENCEDRIVEN.,'IfcProject',(#9,#10,#11,#12,#13,#15));
|
||||
#9=IFCSIMPLEPROPERTYTEMPLATE('0nxsULXo17JRrAvNteYtNR',$,'Eastings','',.P_SINGLEVALUE.,'IfcLengthMeasure',$,$,$,$,$,.READWRITE.);
|
||||
#10=IFCSIMPLEPROPERTYTEMPLATE('26RfaGJs5CqemN0Xin12XL',$,'Northings','',.P_SINGLEVALUE.,'IfcLengthMeasure',$,$,$,$,$,.READWRITE.);
|
||||
#11=IFCSIMPLEPROPERTYTEMPLATE('0ziCn3nZv3m850VEUwWH42',$,'OrthogonalHeight','',.P_SINGLEVALUE.,'IfcLengthMeasure',$,$,$,$,$,.READWRITE.);
|
||||
#12=IFCSIMPLEPROPERTYTEMPLATE('3d4BOt4In8WR119ms1SaAe',$,'XAxisAbscissa','',.P_SINGLEVALUE.,'IfcReal',$,$,$,$,$,.READWRITE.);
|
||||
#13=IFCSIMPLEPROPERTYTEMPLATE('1Q4Voqj2j84RQRI8l67m28',$,'XAxisOrdinate','',.P_SINGLEVALUE.,'IfcReal',$,$,$,$,$,.READWRITE.);
|
||||
#15=IFCSIMPLEPROPERTYTEMPLATE('0O1Hc0$XnDNOG_jPBmbW5r',$,'Scale','',.P_SINGLEVALUE.,'IfcReal',$,$,$,$,$,.READWRITE.);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
@@ -177,30 +177,20 @@ def refresh_ui_data():
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
if isinstance(tool.Ifc.get(), ifcopenshell.sqlite):
|
||||
tool.Ifc.get().clear_cache()
|
||||
|
||||
|
||||
def purge_module_data():
|
||||
from blenderbim.bim import modules
|
||||
|
||||
refresh_ui_data()
|
||||
for name, value in modules.items():
|
||||
try:
|
||||
getattr(getattr(getattr(ifcopenshell.api, name), "data"), "Data").purge()
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
# TODO: deprecate prop purge functions and refactor into data classes.
|
||||
try:
|
||||
getattr(value, "prop").purge()
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
if isinstance(tool.Ifc.get(), ifcopenshell.sqlite):
|
||||
tool.Ifc.get().clear_cache()
|
||||
|
||||
|
||||
@persistent
|
||||
def loadIfcStore(scene):
|
||||
IfcStore.purge()
|
||||
purge_module_data()
|
||||
refresh_ui_data()
|
||||
if not IfcStore.get_file():
|
||||
return
|
||||
IfcStore.get_schema()
|
||||
@@ -212,7 +202,7 @@ def undo_post(scene):
|
||||
if IfcStore.last_transaction != bpy.context.scene.BIMProperties.last_transaction:
|
||||
IfcStore.last_transaction = bpy.context.scene.BIMProperties.last_transaction
|
||||
IfcStore.undo(until_key=bpy.context.scene.BIMProperties.last_transaction)
|
||||
purge_module_data()
|
||||
refresh_ui_data()
|
||||
tool.Ifc.rebuild_element_maps()
|
||||
|
||||
|
||||
@@ -221,7 +211,7 @@ def redo_post(scene):
|
||||
if IfcStore.last_transaction != bpy.context.scene.BIMProperties.last_transaction:
|
||||
IfcStore.last_transaction = bpy.context.scene.BIMProperties.last_transaction
|
||||
IfcStore.redo(until_key=bpy.context.scene.BIMProperties.last_transaction)
|
||||
purge_module_data()
|
||||
refresh_ui_data()
|
||||
tool.Ifc.rebuild_element_maps()
|
||||
|
||||
|
||||
@@ -329,6 +319,17 @@ if getattr(bpy.types, "SCENE_PT_custom_props"):
|
||||
return tool.Blender.is_tab(context, "BLENDER")
|
||||
|
||||
|
||||
# available on Blender 4.0+
|
||||
if getattr(bpy.types, "SCENE_PT_simulation", None):
|
||||
|
||||
class Override_SCENE_PT_simulation(bpy.types.SCENE_PT_simulation):
|
||||
bl_idname = "SCENE_PT_simulation_override"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return tool.Blender.is_tab(context, "BLENDER")
|
||||
|
||||
|
||||
@persistent
|
||||
def load_post(scene):
|
||||
global global_subscription_owner
|
||||
@@ -371,6 +372,7 @@ def load_post(scene):
|
||||
"SCENE_PT_rigid_body_world",
|
||||
"SCENE_PT_audio",
|
||||
"SCENE_PT_keying_sets",
|
||||
"SCENE_PT_simulation",
|
||||
"SCENE_PT_custom_props",
|
||||
]:
|
||||
if getattr(bpy.types, panel, None):
|
||||
|
||||
@@ -260,12 +260,7 @@ def convert_property_group_from_si(property_group, skip_props=()):
|
||||
for prop_name in property_group.bl_rna.properties.keys():
|
||||
if prop_name in skip_props:
|
||||
continue
|
||||
prop_bl_rna = property_group.bl_rna.properties[prop_name]
|
||||
if prop_bl_rna.array_length > 0:
|
||||
prop_value = prop_bl_rna.default_array
|
||||
else:
|
||||
prop_value = prop_bl_rna.default
|
||||
|
||||
prop_value = tool.Blender.get_blender_prop_default_value(property_group, prop_name)
|
||||
if type(prop_value) is float:
|
||||
prop_value = prop_value * conversion_k
|
||||
elif type(prop_value) is bpy.types.bpy_prop_array:
|
||||
@@ -325,7 +320,7 @@ def draw_filter(layout, props, data, module):
|
||||
row.prop(ifc_filter, "value", text="", icon="OUTLINER")
|
||||
elif ifc_filter.type == "location":
|
||||
row = box.row(align=True)
|
||||
row.prop(ifc_filter, "name", text="", icon="PACKAGE")
|
||||
row.prop(ifc_filter, "value", text="", icon="PACKAGE")
|
||||
elif ifc_filter.type == "query":
|
||||
row = box.row(align=True)
|
||||
row.prop(ifc_filter, "name", text="", icon="POINTCLOUD_DATA")
|
||||
|
||||
@@ -20,10 +20,9 @@ import os
|
||||
import re
|
||||
import bpy
|
||||
import time
|
||||
import json
|
||||
import bmesh
|
||||
import shutil
|
||||
import logging
|
||||
import threading
|
||||
import mathutils
|
||||
import numpy as np
|
||||
import multiprocessing
|
||||
@@ -31,7 +30,6 @@ import ifcopenshell
|
||||
import ifcopenshell.geom
|
||||
import ifcopenshell.util.unit
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.selector
|
||||
import ifcopenshell.util.geolocation
|
||||
import blenderbim.tool as tool
|
||||
from itertools import chain, accumulate
|
||||
@@ -39,16 +37,6 @@ from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.module.drawing.prop import ANNOTATION_TYPES_DATA
|
||||
|
||||
|
||||
class FileCopy(threading.Thread):
|
||||
def __init__(self, file_path, destination):
|
||||
threading.Thread.__init__(self)
|
||||
self.file_path = file_path
|
||||
self.destination = destination
|
||||
|
||||
def run(self):
|
||||
shutil.copy(self.file_path, self.destination)
|
||||
|
||||
|
||||
class MaterialCreator:
|
||||
def __init__(self, ifc_import_settings, ifc_importer):
|
||||
self.mesh = None
|
||||
@@ -108,7 +96,9 @@ class MaterialCreator:
|
||||
has_parsed = True
|
||||
elif hasattr(element, "RepresentationMaps"):
|
||||
for representation_map in element.RepresentationMaps:
|
||||
if self.parse_representation(representation_map.MappedRepresentation):
|
||||
if not representation_map.MappedRepresentation:
|
||||
has_parsed = True # Accommodate invalid IFC data from Revit
|
||||
elif self.parse_representation(representation_map.MappedRepresentation):
|
||||
has_parsed = True
|
||||
return has_parsed
|
||||
|
||||
@@ -158,12 +148,15 @@ class MaterialCreator:
|
||||
faces_remap = None
|
||||
texture_map = None
|
||||
if coordinates.is_a("IfcIndexedPolygonalTextureMap"):
|
||||
faces_remap = [[coordinates_remap[i-1] for i in tex_coord_index.TexCoordsOf.CoordIndex]
|
||||
for tex_coord_index in coordinates.TexCoordIndices]
|
||||
faces_remap = [
|
||||
[coordinates_remap[i - 1] for i in tex_coord_index.TexCoordsOf.CoordIndex]
|
||||
for tex_coord_index in coordinates.TexCoordIndices
|
||||
]
|
||||
texture_map = [tex_coord_index.TexCoordIndex for tex_coord_index in coordinates.TexCoordIndices]
|
||||
elif coordinates.is_a("IfcIndexedTriangleTextureMap"):
|
||||
faces_remap = [[coordinates_remap[i-1] for i in triangle_face]
|
||||
for triangle_face in coordinates.MappedTo.CoordIndex]
|
||||
faces_remap = [
|
||||
[coordinates_remap[i - 1] for i in triangle_face] for triangle_face in coordinates.MappedTo.CoordIndex
|
||||
]
|
||||
texture_map = coordinates.TexCoordIndex
|
||||
|
||||
# apply uv to each face
|
||||
@@ -178,7 +171,7 @@ class MaterialCreator:
|
||||
)
|
||||
# apply uv to each loop
|
||||
for loop, i in zip(bface.loops, texCoordIndex):
|
||||
loop[uv_layer].uv = coordinates.TexCoords.TexCoordsList[i-1]
|
||||
loop[uv_layer].uv = coordinates.TexCoords.TexCoordsList[i - 1]
|
||||
|
||||
# Finish up, write the bmesh back to the mesh
|
||||
bm.to_mesh(self.mesh)
|
||||
@@ -223,20 +216,8 @@ class IfcImporter:
|
||||
self.ifc_import_settings = ifc_import_settings
|
||||
self.diff = None
|
||||
self.file = None
|
||||
self.settings = ifcopenshell.geom.settings()
|
||||
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_body_2d = ifcopenshell.geom.settings()
|
||||
self.settings_body_2d.set_deflection_tolerance(self.ifc_import_settings.deflection_tolerance)
|
||||
self.settings_body_2d.set_angular_tolerance(self.ifc_import_settings.angular_tolerance)
|
||||
self.settings_body_2d.set(self.settings_body_2d.STRICT_TOLERANCE, True)
|
||||
self.settings_body_2d.set(self.settings_body_2d.INCLUDE_CURVES, True)
|
||||
self.settings_native = ifcopenshell.geom.settings()
|
||||
self.settings_native.set(self.settings_native.INCLUDE_CURVES, True)
|
||||
self.settings_plan_2d = ifcopenshell.geom.settings()
|
||||
self.settings_plan_2d.set(self.settings_plan_2d.INCLUDE_CURVES, True)
|
||||
self.settings_plan_2d.set(self.settings_plan_2d.STRICT_TOLERANCE, True)
|
||||
self.context_settings = []
|
||||
self.contexts = []
|
||||
self.project = None
|
||||
self.has_existing_project = False
|
||||
self.collections = {}
|
||||
@@ -275,6 +256,8 @@ class IfcImporter:
|
||||
self.profile_code("Calculate unit scale")
|
||||
self.calculate_model_offset()
|
||||
self.profile_code("Calculate model offset")
|
||||
self.predict_dense_mesh()
|
||||
self.profile_code("Predict dense mesh")
|
||||
self.set_units()
|
||||
self.profile_code("Set units")
|
||||
self.create_project()
|
||||
@@ -318,7 +301,9 @@ class IfcImporter:
|
||||
self.profile_code("Merging by colour")
|
||||
self.set_default_context()
|
||||
self.profile_code("Setting default context")
|
||||
self.setup_viewport_camera()
|
||||
if self.ifc_import_settings.should_setup_viewport_camera:
|
||||
self.setup_viewport_camera()
|
||||
self.setup_arrays()
|
||||
self.update_progress(100)
|
||||
bpy.context.window_manager.progress_end()
|
||||
|
||||
@@ -341,33 +326,83 @@ class IfcImporter:
|
||||
return abs(coords[0]) > limit or abs(coords[1]) > limit or abs(coords[2]) > limit
|
||||
|
||||
def process_context_filter(self):
|
||||
# Facetation is to accommodate broken Revit files
|
||||
# See https://forums.buildingsmart.org/t/suggestions-on-how-to-improve-clarity-of-representation-context-usage-in-documentation/3663/6?u=moult
|
||||
self.body_contexts = [
|
||||
c.id()
|
||||
for c in self.file.by_type("IfcGeometricRepresentationSubContext")
|
||||
if c.ContextIdentifier in ["Body", "Facetation"]
|
||||
]
|
||||
# Ideally, all representations should be in a subcontext, but some BIM programs don't do this correctly
|
||||
if not self.body_contexts:
|
||||
self.body_contexts.extend(
|
||||
[
|
||||
c.id()
|
||||
for c in self.file.by_type("IfcGeometricRepresentationContext", include_subtypes=False)
|
||||
if c.ContextType == "Model"
|
||||
]
|
||||
)
|
||||
if self.body_contexts:
|
||||
self.settings.set_context_ids(self.body_contexts)
|
||||
# Annotation ContextType is to accommodate broken Revit files
|
||||
# See https://github.com/Autodesk/revit-ifc/issues/187
|
||||
self.plan_contexts = [
|
||||
c.id()
|
||||
for c in self.file.by_type("IfcGeometricRepresentationContext")
|
||||
if c.ContextType in ["Plan", "Annotation"] or c.ContextIdentifier == "Annotation"
|
||||
type_priority = ["Model", "Plan", "Annotation"]
|
||||
identifier_priority = [
|
||||
"Body",
|
||||
"Body-FallBack",
|
||||
"Facetation",
|
||||
"FootPrint",
|
||||
"Profile",
|
||||
"Surface",
|
||||
"Reference",
|
||||
"Axis",
|
||||
"Clearance",
|
||||
"Box",
|
||||
"Lighting",
|
||||
"Annotation",
|
||||
"CoG",
|
||||
]
|
||||
if self.plan_contexts:
|
||||
self.settings_plan_2d.set_context_ids(self.plan_contexts)
|
||||
target_view_priority = [
|
||||
"MODEL_VIEW",
|
||||
"PLAN_VIEW",
|
||||
"REFLECTED_PLAN_VIEW",
|
||||
"ELEVATION_VIEW",
|
||||
"SECTION_VIEW",
|
||||
"GRAPH_VIEW",
|
||||
"SKETCH_VIEW",
|
||||
"USERDEFINED",
|
||||
"NOTDEFINED",
|
||||
]
|
||||
|
||||
def sort_context(context):
|
||||
priority = []
|
||||
if context.ContextType in type_priority:
|
||||
priority.append(len(type_priority) - type_priority.index(context.ContextType))
|
||||
else:
|
||||
priority.append(0)
|
||||
return tuple(priority)
|
||||
|
||||
def sort_subcontext(context):
|
||||
priority = []
|
||||
|
||||
if context.ContextType in type_priority:
|
||||
priority.append(len(type_priority) - type_priority.index(context.ContextType))
|
||||
else:
|
||||
priority.append(0)
|
||||
|
||||
if context.ContextIdentifier in identifier_priority:
|
||||
priority.append(len(identifier_priority) - identifier_priority.index(context.ContextIdentifier))
|
||||
else:
|
||||
priority.append(0)
|
||||
|
||||
if context.TargetView in target_view_priority:
|
||||
priority.append(len(target_view_priority) - target_view_priority.index(context.TargetView))
|
||||
else:
|
||||
priority.append(0)
|
||||
|
||||
priority.append(context.TargetScale or 0) # Big then small
|
||||
|
||||
return tuple(priority)
|
||||
|
||||
# Ideally, all representations should be in a subcontext, but some BIM programs don't do this correctly
|
||||
self.contexts = sorted(
|
||||
self.file.by_type("IfcGeometricRepresentationSubContext"), key=sort_subcontext, reverse=True
|
||||
) + sorted(
|
||||
self.file.by_type("IfcGeometricRepresentationContext", include_subtypes=False),
|
||||
key=sort_context,
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
for context in self.contexts:
|
||||
settings = ifcopenshell.geom.settings()
|
||||
settings.set_deflection_tolerance(self.ifc_import_settings.deflection_tolerance)
|
||||
settings.set_angular_tolerance(self.ifc_import_settings.angular_tolerance)
|
||||
settings.set(settings.STRICT_TOLERANCE, True)
|
||||
settings.set(settings.INCLUDE_CURVES, True)
|
||||
settings.set_context_ids([context.id()])
|
||||
self.context_settings.append(settings)
|
||||
|
||||
def process_element_filter(self):
|
||||
offset = self.ifc_import_settings.element_offset
|
||||
@@ -438,13 +473,51 @@ class IfcImporter:
|
||||
or getattr(element, "HasOpenings", None)
|
||||
):
|
||||
return
|
||||
representations = self.get_transformed_body_representations(element.Representation.Representations)
|
||||
|
||||
representation = None
|
||||
representation_priority = None
|
||||
context = None
|
||||
|
||||
for rep in element.Representation.Representations:
|
||||
if rep.ContextOfItems in self.contexts:
|
||||
rep_priority = self.contexts.index(rep.ContextOfItems)
|
||||
if representation is None or rep_priority < representation_priority:
|
||||
representation = rep
|
||||
representation_priority = rep_priority
|
||||
context = rep.ContextOfItems
|
||||
|
||||
if not representation:
|
||||
return
|
||||
|
||||
matrix = np.eye(4)
|
||||
representation_id = None
|
||||
|
||||
rep = representation
|
||||
while True:
|
||||
if len(rep.Items) == 1 and rep.Items[0].is_a("IfcMappedItem"):
|
||||
rep_matrix = ifcopenshell.util.placement.get_mappeditem_transformation(rep.Items[0])
|
||||
if not np.allclose(rep_matrix, np.eye(4)):
|
||||
matrix = rep_matrix @ matrix
|
||||
if representation_id is None:
|
||||
representation_id = rep.id()
|
||||
rep = rep.Items[0].MappingSource.MappedRepresentation
|
||||
else:
|
||||
if representation_id is None:
|
||||
representation_id = rep.id()
|
||||
break
|
||||
resolved_representation = ifcopenshell.util.representation.resolve_representation(representation)
|
||||
|
||||
matrix[0][3] *= self.unit_scale
|
||||
matrix[1][3] *= self.unit_scale
|
||||
matrix[2][3] *= self.unit_scale
|
||||
|
||||
# Single swept disk solids (e.g. rebar) are better natively represented as beveled curves
|
||||
if self.is_native_swept_disk_solid(element, representations):
|
||||
if self.is_native_swept_disk_solid(element, resolved_representation):
|
||||
self.native_data[element.GlobalId] = {
|
||||
"representations": representations,
|
||||
"representation": self.get_body_representation(element.Representation.Representations),
|
||||
"matrix": matrix,
|
||||
"context": context,
|
||||
"geometry_id": representation_id,
|
||||
"representation": resolved_representation,
|
||||
"type": "IfcSweptDiskSolid",
|
||||
}
|
||||
return True
|
||||
@@ -453,51 +526,53 @@ class IfcImporter:
|
||||
return False # Performance improvements only occur on edge cases currently
|
||||
|
||||
# FacetedBreps (without voids) are meshes. See #841.
|
||||
if self.is_native_faceted_brep(representations):
|
||||
if self.is_native_faceted_brep(resolved_representation):
|
||||
self.native_data[element.GlobalId] = {
|
||||
"representations": representations,
|
||||
"representation": self.get_body_representation(element.Representation.Representations),
|
||||
"matrix": matrix,
|
||||
"context": context,
|
||||
"geometry_id": representation_id,
|
||||
"representation": resolved_representation,
|
||||
"type": "IfcFacetedBrep",
|
||||
}
|
||||
return True
|
||||
|
||||
if self.is_native_face_based_surface_model(representations):
|
||||
if self.is_native_face_based_surface_model(resolved_representation):
|
||||
self.native_data[element.GlobalId] = {
|
||||
"representations": representations,
|
||||
"representation": self.get_body_representation(element.Representation.Representations),
|
||||
"matrix": matrix,
|
||||
"context": context,
|
||||
"geometry_id": representation_id,
|
||||
"representation": resolved_representation,
|
||||
"type": "IfcFaceBasedSurfaceModel",
|
||||
}
|
||||
return True
|
||||
|
||||
def is_native_swept_disk_solid(self, element, representations):
|
||||
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"):
|
||||
if tool.Blender.Modifier.is_railing(element):
|
||||
return False
|
||||
return True
|
||||
elif len(items) and ( # See #2508 why we accommodate for invalid IFCs here
|
||||
items[0].is_a("IfcSweptDiskSolid")
|
||||
and len({i.is_a() for i in items}) == 1
|
||||
and len({i.Radius for i in items}) == 1
|
||||
):
|
||||
if tool.Blender.Modifier.is_railing(element):
|
||||
return False
|
||||
return True
|
||||
def is_native_swept_disk_solid(self, element, representation):
|
||||
items = [i["item"] for i in ifcopenshell.util.representation.resolve_items(representation)]
|
||||
if len(items) == 1 and items[0].is_a("IfcSweptDiskSolid"):
|
||||
if tool.Blender.Modifier.is_railing(element):
|
||||
return False
|
||||
return True
|
||||
elif len(items) and ( # See #2508 why we accommodate for invalid IFCs here
|
||||
items[0].is_a("IfcSweptDiskSolid")
|
||||
and len({i.is_a() for i in items}) == 1
|
||||
and len({i.Radius for i in items}) == 1
|
||||
):
|
||||
if tool.Blender.Modifier.is_railing(element):
|
||||
return False
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_native_faceted_brep(self, representations):
|
||||
for representation in representations:
|
||||
for i in representation["raw"].Items:
|
||||
if i.is_a() != "IfcFacetedBrep":
|
||||
return False
|
||||
def is_native_faceted_brep(self, representation):
|
||||
# TODO handle mapped items
|
||||
for i in representation.Items:
|
||||
if i.is_a() != "IfcFacetedBrep":
|
||||
return False
|
||||
return True
|
||||
|
||||
def is_native_face_based_surface_model(self, representations):
|
||||
for representation in representations:
|
||||
for i in representation["raw"].Items:
|
||||
if i.is_a() != "IfcFaceBasedSurfaceModel":
|
||||
return False
|
||||
def is_native_face_based_surface_model(self, representation):
|
||||
for i in representation.Items:
|
||||
if i.is_a() != "IfcFaceBasedSurfaceModel":
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_products_from_shape_representation(self, element):
|
||||
@@ -509,6 +584,26 @@ class IfcImporter:
|
||||
products.extend(self.get_products_from_shape_representation(inverse_element))
|
||||
return products
|
||||
|
||||
def predict_dense_mesh(self):
|
||||
threshold = 10000 # Just from experience.
|
||||
|
||||
faces = [len(e.CfsFaces) for e in self.file.by_type("IfcClosedShell")]
|
||||
if faces and max(faces) > threshold:
|
||||
self.ifc_import_settings.should_use_native_meshes = True
|
||||
return
|
||||
|
||||
if self.file.schema == "IFC2X3":
|
||||
return
|
||||
|
||||
faces = [len(e.Faces) for e in self.file.by_type("IfcPolygonalFaceSet")]
|
||||
if faces and max(faces) > threshold:
|
||||
self.ifc_import_settings.should_use_native_meshes = True
|
||||
return
|
||||
|
||||
faces = [len(e.CoordIndex) for e in self.file.by_type("IfcTriangulatedFaceSet")]
|
||||
if faces and max(faces) > threshold:
|
||||
self.ifc_import_settings.should_use_native_meshes = True
|
||||
|
||||
def calculate_model_offset(self):
|
||||
props = bpy.context.scene.BIMGeoreferenceProperties
|
||||
if props.has_blender_offset:
|
||||
@@ -575,13 +670,9 @@ class IfcImporter:
|
||||
return
|
||||
if not self.does_element_likely_have_geometry_far_away(element):
|
||||
continue
|
||||
try:
|
||||
shape = ifcopenshell.geom.create_shape(self.settings, element)
|
||||
except:
|
||||
try:
|
||||
shape = ifcopenshell.geom.create_shape(self.settings_body_2d, element)
|
||||
except:
|
||||
continue
|
||||
shape = self.create_generic_shape(element)
|
||||
if not shape:
|
||||
continue
|
||||
m = shape.transformation.matrix.data
|
||||
mat = np.array(
|
||||
([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])
|
||||
@@ -653,7 +744,7 @@ class IfcImporter:
|
||||
self.ifc_import_settings.logger.error("An invalid grid was found %s", grid)
|
||||
continue
|
||||
if grid.Representation:
|
||||
shape = ifcopenshell.geom.create_shape(self.settings_plan_2d, grid)
|
||||
shape = self.create_generic_shape(grid)
|
||||
grid_obj = self.create_product(grid, shape)
|
||||
if bpy.context.preferences.addons["blenderbim"].preferences.lock_grids_on_import:
|
||||
grid_obj.lock_location = (True, True, True)
|
||||
@@ -672,7 +763,7 @@ class IfcImporter:
|
||||
|
||||
def create_grid_axes(self, axes, grid_collection, grid_obj):
|
||||
for axis in axes:
|
||||
shape = ifcopenshell.geom.create_shape(self.settings_plan_2d, axis.AxisCurve)
|
||||
shape = self.create_generic_shape(axis.AxisCurve)
|
||||
mesh = self.create_mesh(axis, shape)
|
||||
obj = bpy.data.objects.new(f"IfcGridAxis/{axis.AxisTag}", mesh)
|
||||
if bpy.context.preferences.addons["blenderbim"].preferences.lock_grids_on_import:
|
||||
@@ -692,27 +783,21 @@ class IfcImporter:
|
||||
self.ifc_import_settings.logger.info("Creating object %s", element)
|
||||
mesh = None
|
||||
if self.ifc_import_settings.should_load_geometry:
|
||||
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
||||
if not representation:
|
||||
representation = ifcopenshell.util.representation.get_representation(element, "Plan", "Annotation")
|
||||
if not representation:
|
||||
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Annotation")
|
||||
if representation:
|
||||
for context in self.contexts:
|
||||
representation = ifcopenshell.util.representation.get_representation(element, context)
|
||||
if not representation:
|
||||
continue
|
||||
mesh_name = "{}/{}".format(representation.ContextOfItems.id(), representation.id())
|
||||
mesh = self.meshes.get(mesh_name)
|
||||
if mesh is None:
|
||||
shape = None
|
||||
try:
|
||||
shape = ifcopenshell.geom.create_shape(self.settings, representation)
|
||||
except:
|
||||
try:
|
||||
shape = ifcopenshell.geom.create_shape(self.settings_plan_2d, representation)
|
||||
except:
|
||||
self.ifc_import_settings.logger.error("Failed to generate shape for %s", element)
|
||||
shape = self.create_generic_shape(representation)
|
||||
if shape:
|
||||
mesh = self.create_mesh(element, shape)
|
||||
tool.Loader.link_mesh(shape, mesh)
|
||||
self.meshes[mesh_name] = mesh
|
||||
else:
|
||||
self.ifc_import_settings.logger.error("Failed to generate shape for %s", element)
|
||||
break
|
||||
obj = bpy.data.objects.new(tool.Loader.get_name(element), mesh)
|
||||
self.link_element(element, obj)
|
||||
self.material_creator.create(element, obj, mesh)
|
||||
@@ -736,20 +821,16 @@ class IfcImporter:
|
||||
checkpoint = time.time()
|
||||
self.incrementally_merge_objects()
|
||||
native_data = self.native_data[element.GlobalId]
|
||||
representation = native_data["representation"]
|
||||
if not representation:
|
||||
continue
|
||||
context_id = representation.ContextOfItems.id() if hasattr(representation, "ContextOfItems") else 0
|
||||
mesh_name = f"{context_id}/{representation.id()}"
|
||||
mesh_name = f"{native_data['context'].id()}/{native_data['geometry_id']}"
|
||||
mesh = self.meshes.get(mesh_name)
|
||||
if mesh is None:
|
||||
if native_data["type"] == "IfcSweptDiskSolid":
|
||||
mesh = self.create_native_swept_disk_solid(element, mesh_name)
|
||||
mesh = self.create_native_swept_disk_solid(element, mesh_name, native_data)
|
||||
elif native_data["type"] == "IfcFacetedBrep":
|
||||
mesh = self.create_native_faceted_brep(element, mesh_name)
|
||||
mesh = self.create_native_faceted_brep(element, mesh_name, native_data)
|
||||
elif native_data["type"] == "IfcFaceBasedSurfaceModel":
|
||||
mesh = self.create_native_faceted_brep(element, mesh_name)
|
||||
tool.Ifc.link(representation, mesh)
|
||||
mesh = self.create_native_faceted_brep(element, mesh_name, native_data)
|
||||
tool.Ifc.link(tool.Ifc.get().by_id(native_data["geometry_id"]), mesh)
|
||||
mesh.name = mesh_name
|
||||
self.meshes[mesh_name] = mesh
|
||||
self.create_product(element, mesh=mesh)
|
||||
@@ -761,21 +842,25 @@ class IfcImporter:
|
||||
def create_elements(self):
|
||||
self.create_generic_elements(self.elements)
|
||||
|
||||
def create_generic_shape(self, element):
|
||||
for settings in self.context_settings:
|
||||
try:
|
||||
result = ifcopenshell.geom.create_shape(settings, element)
|
||||
if result:
|
||||
return result
|
||||
except:
|
||||
pass
|
||||
|
||||
def create_generic_elements(self, elements):
|
||||
if isinstance(self.file, ifcopenshell.sqlite):
|
||||
return self.create_generic_sqlite_elements(elements)
|
||||
|
||||
# Based on my experience in viewing BIM models, representations are prioritised as follows:
|
||||
# 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.
|
||||
if self.ifc_import_settings.should_load_geometry:
|
||||
products = self.create_products(elements)
|
||||
elements -= products
|
||||
products = self.create_products(elements, settings=self.settings_body_2d)
|
||||
elements -= products
|
||||
products = self.create_products(elements, settings=self.settings_plan_2d)
|
||||
elements -= products
|
||||
for settings in self.context_settings:
|
||||
if not elements:
|
||||
break
|
||||
products = self.create_products(elements, settings=settings)
|
||||
elements -= products
|
||||
products = self.create_pointclouds(elements)
|
||||
elements -= products
|
||||
|
||||
@@ -833,9 +918,6 @@ class IfcImporter:
|
||||
self.create_product(element, mesh=mesh)
|
||||
|
||||
def create_products(self, products, settings=None):
|
||||
if settings is None:
|
||||
settings = self.settings
|
||||
|
||||
results = set()
|
||||
if not products:
|
||||
return results
|
||||
@@ -872,16 +954,8 @@ class IfcImporter:
|
||||
shape = iterator.get()
|
||||
if shape:
|
||||
product = self.file.by_id(shape.id)
|
||||
if self.body_contexts:
|
||||
self.create_product(product, shape)
|
||||
results.add(product)
|
||||
else:
|
||||
if shape.context not in ["Body", "Facetation"] and IfcStore.get_element(shape.id):
|
||||
# We only load a single context, and we prioritise the Body context. See #1290.
|
||||
pass
|
||||
else:
|
||||
self.create_product(product, shape)
|
||||
results.add(product)
|
||||
self.create_product(product, shape)
|
||||
results.add(product)
|
||||
if not iterator.next():
|
||||
break
|
||||
print("Done creating geometry")
|
||||
@@ -909,10 +983,10 @@ class IfcImporter:
|
||||
self.structural_collection.children.link(self.structural_connection_collection)
|
||||
self.project["blender"].children.link(self.structural_collection)
|
||||
|
||||
self.create_products(self.file.by_type("IfcStructuralCurveMember"), settings=self.settings_plan_2d)
|
||||
self.create_products(self.file.by_type("IfcStructuralCurveConnection"), settings=self.settings_plan_2d)
|
||||
self.create_products(self.file.by_type("IfcStructuralSurfaceMember"), settings=self.settings_plan_2d)
|
||||
self.create_products(self.file.by_type("IfcStructuralSurfaceConnection"), settings=self.settings_plan_2d)
|
||||
self.create_generic_elements(set(self.file.by_type("IfcStructuralCurveMember")))
|
||||
self.create_generic_elements(set(self.file.by_type("IfcStructuralCurveConnection")))
|
||||
self.create_generic_elements(set(self.file.by_type("IfcStructuralSurfaceMember")))
|
||||
self.create_generic_elements(set(self.file.by_type("IfcStructuralSurfaceConnection")))
|
||||
self.create_structural_point_connections()
|
||||
|
||||
def create_structural_point_connections(self):
|
||||
@@ -1061,7 +1135,7 @@ class IfcImporter:
|
||||
elif style.is_a("IfcPresentationStyleAssignment"):
|
||||
styles.extend(style.Styles)
|
||||
|
||||
def create_native_faceted_brep(self, element, mesh_name):
|
||||
def create_native_faceted_brep(self, element, mesh_name, native_data):
|
||||
# TODO: georeferencing?
|
||||
# co [x y z x y z x y z ...]
|
||||
# vertex_index [i i i i i ...]
|
||||
@@ -1078,10 +1152,11 @@ class IfcImporter:
|
||||
"material_ids": [],
|
||||
}
|
||||
|
||||
for representation in element.Representation.Representations:
|
||||
if representation.ContextOfItems.id() not in self.body_contexts:
|
||||
continue
|
||||
self.convert_representation(representation)
|
||||
for item in native_data["representation"].Items:
|
||||
if item.is_a() == "IfcFacetedBrep":
|
||||
self.convert_representation_item_faceted_brep(item)
|
||||
elif item.is_a() == "IfcFaceBasedSurfaceModel":
|
||||
self.convert_representation_item_face_based_surface_model(item)
|
||||
|
||||
mesh = bpy.data.meshes.new("Native")
|
||||
|
||||
@@ -1098,19 +1173,33 @@ class IfcImporter:
|
||||
)
|
||||
verts = [None] * len(self.mesh_data["co"])
|
||||
for i in range(0, len(self.mesh_data["co"]), 3):
|
||||
verts[i], verts[i + 1], verts[i + 2] = ifcopenshell.util.geolocation.enh2xyz(
|
||||
self.mesh_data["co"][i] * self.unit_scale,
|
||||
self.mesh_data["co"][i + 1] * self.unit_scale,
|
||||
self.mesh_data["co"][i + 2] * self.unit_scale,
|
||||
offset_point[0] * self.unit_scale,
|
||||
offset_point[1] * self.unit_scale,
|
||||
offset_point[2] * self.unit_scale,
|
||||
float(props.blender_x_axis_abscissa),
|
||||
float(props.blender_x_axis_ordinate),
|
||||
verts[i], verts[i + 1], verts[i + 2], _ = native_data["matrix"] @ mathutils.Vector(
|
||||
(
|
||||
*ifcopenshell.util.geolocation.enh2xyz(
|
||||
self.mesh_data["co"][i] * self.unit_scale,
|
||||
self.mesh_data["co"][i + 1] * self.unit_scale,
|
||||
self.mesh_data["co"][i + 2] * self.unit_scale,
|
||||
offset_point[0] * self.unit_scale,
|
||||
offset_point[1] * self.unit_scale,
|
||||
offset_point[2] * self.unit_scale,
|
||||
float(props.blender_x_axis_abscissa),
|
||||
float(props.blender_x_axis_ordinate),
|
||||
),
|
||||
1,
|
||||
)
|
||||
)
|
||||
mesh["has_cartesian_point_offset"] = True
|
||||
else:
|
||||
verts = [c * self.unit_scale for c in self.mesh_data["co"]]
|
||||
verts = [None] * len(self.mesh_data["co"])
|
||||
for i in range(0, len(self.mesh_data["co"]), 3):
|
||||
verts[i], verts[i + 1], verts[i + 2], _ = native_data["matrix"] @ mathutils.Vector(
|
||||
(
|
||||
self.mesh_data["co"][i] * self.unit_scale,
|
||||
self.mesh_data["co"][i + 1] * self.unit_scale,
|
||||
self.mesh_data["co"][i + 2] * self.unit_scale,
|
||||
1,
|
||||
)
|
||||
)
|
||||
mesh["has_cartesian_point_offset"] = False
|
||||
|
||||
mesh.vertices.add(self.mesh_data["total_verts"])
|
||||
@@ -1120,25 +1209,13 @@ class IfcImporter:
|
||||
mesh.polygons.add(self.mesh_data["total_polygons"])
|
||||
mesh.polygons.foreach_set("loop_start", self.mesh_data["loop_start"])
|
||||
mesh.polygons.foreach_set("loop_total", self.mesh_data["loop_total"])
|
||||
mesh.polygons.foreach_set("use_smooth", [0] * self.mesh_data["total_polygons"])
|
||||
mesh.update()
|
||||
|
||||
mesh["ios_materials"] = self.mesh_data["materials"]
|
||||
mesh["ios_material_ids"] = self.mesh_data["material_ids"]
|
||||
return mesh
|
||||
|
||||
def convert_representation(self, representation):
|
||||
for item in representation.Items:
|
||||
self.convert_representation_item(item)
|
||||
|
||||
def convert_representation_item(self, item):
|
||||
if item.is_a("IfcMappedItem"):
|
||||
# mapping_target = matrix
|
||||
self.convert_representation(item.MappingSource.MappedRepresentation)
|
||||
elif item.is_a() == "IfcFacetedBrep":
|
||||
self.convert_representation_item_faceted_brep(item)
|
||||
elif item.is_a() == "IfcFaceBasedSurfaceModel":
|
||||
self.convert_representation_item_face_based_surface_model(item)
|
||||
|
||||
def convert_representation_item_face_based_surface_model(self, item):
|
||||
mesh = item.get_info_2(recursive=True)
|
||||
for face_set in mesh["FbsmFaces"]:
|
||||
@@ -1233,63 +1310,39 @@ class IfcImporter:
|
||||
self.mesh_data["loop_start"].extend(loop_start)
|
||||
# list(di1.keys())
|
||||
|
||||
def create_native_swept_disk_solid(self, element, mesh_name):
|
||||
def create_native_swept_disk_solid(self, element, mesh_name, native_data):
|
||||
# TODO: georeferencing?
|
||||
curve = bpy.data.curves.new(mesh_name, type="CURVE")
|
||||
curve.dimensions = "3D"
|
||||
curve.resolution_u = 2
|
||||
polyline = curve.splines.new("POLY")
|
||||
|
||||
for representation in self.native_data[element.GlobalId]["representations"]:
|
||||
for item in representation["raw"].Items:
|
||||
# TODO: support inner radius, start param, and end param
|
||||
geometry = ifcopenshell.geom.create_shape(self.settings_native, item.Directrix)
|
||||
e = geometry.edges
|
||||
v = geometry.verts
|
||||
vertices = [[v[i], v[i + 1], v[i + 2], 1] for i in range(0, len(v), 3)]
|
||||
edges = [[e[i], e[i + 1]] for i in range(0, len(e), 2)]
|
||||
v2 = None
|
||||
for edge in edges:
|
||||
v1 = vertices[edge[0]]
|
||||
if v1 != v2:
|
||||
polyline = curve.splines.new("POLY")
|
||||
polyline.points[-1].co = representation["matrix"] @ mathutils.Vector(v1)
|
||||
v2 = vertices[edge[1]]
|
||||
polyline.points.add(1)
|
||||
polyline.points[-1].co = representation["matrix"] @ mathutils.Vector(v2)
|
||||
for item_data in ifcopenshell.util.representation.resolve_items(native_data["representation"]):
|
||||
item = item_data["item"]
|
||||
matrix = item_data["matrix"]
|
||||
matrix[0][3] *= self.unit_scale
|
||||
matrix[1][3] *= self.unit_scale
|
||||
matrix[2][3] *= self.unit_scale
|
||||
# TODO: support inner radius, start param, and end param
|
||||
geometry = self.create_generic_shape(item.Directrix)
|
||||
e = geometry.edges
|
||||
v = geometry.verts
|
||||
vertices = [list(matrix @ [v[i], v[i + 1], v[i + 2], 1]) for i in range(0, len(v), 3)]
|
||||
edges = [[e[i], e[i + 1]] for i in range(0, len(e), 2)]
|
||||
v2 = None
|
||||
for edge in edges:
|
||||
v1 = vertices[edge[0]]
|
||||
if v1 != v2:
|
||||
polyline = curve.splines.new("POLY")
|
||||
polyline.points[-1].co = native_data["matrix"] @ mathutils.Vector(v1)
|
||||
v2 = vertices[edge[1]]
|
||||
polyline.points.add(1)
|
||||
polyline.points[-1].co = native_data["matrix"] @ mathutils.Vector(v2)
|
||||
|
||||
curve.bevel_depth = self.unit_scale * item.Radius
|
||||
curve.use_fill_caps = True
|
||||
return curve
|
||||
|
||||
def create_native_annotation(self, element, mesh_name):
|
||||
# TODO: georeferencing?
|
||||
curve = bpy.data.curves.new(mesh_name, type="CURVE")
|
||||
curve.dimensions = "3D"
|
||||
curve.resolution_u = 2
|
||||
polyline = curve.splines.new("POLY")
|
||||
|
||||
for representation in self.native_data[element.GlobalId]["representations"]:
|
||||
for item in representation["raw"].Items:
|
||||
# TODO: support inner radius, start param, and end param
|
||||
geometry = ifcopenshell.geom.create_shape(self.settings_native, item.Directrix)
|
||||
e = geometry.edges
|
||||
v = geometry.verts
|
||||
vertices = [[v[i], v[i + 1], v[i + 2], 1] for i in range(0, len(v), 3)]
|
||||
edges = [[e[i], e[i + 1]] for i in range(0, len(e), 2)]
|
||||
v2 = None
|
||||
for edge in edges:
|
||||
v1 = vertices[edge[0]]
|
||||
if v1 != v2:
|
||||
polyline = curve.splines.new("POLY")
|
||||
polyline.points[-1].co = representation["matrix"] @ mathutils.Vector(v1)
|
||||
v2 = vertices[edge[1]]
|
||||
polyline.points.add(1)
|
||||
polyline.points[-1].co = representation["matrix"] @ mathutils.Vector(v2)
|
||||
|
||||
curve.bevel_depth = self.unit_scale * item.Radius
|
||||
return curve
|
||||
|
||||
def merge_by_class(self):
|
||||
merge_set = {}
|
||||
id_set = {}
|
||||
@@ -1354,7 +1407,8 @@ class IfcImporter:
|
||||
context_override = {}
|
||||
context_override["object"] = context_override["active_object"] = target
|
||||
context_override["selected_objects"] = context_override["selected_editable_objects"] = objs
|
||||
bpy.ops.object.join(context_override)
|
||||
with bpy.context.temp_override(**context_override):
|
||||
bpy.ops.object.join()
|
||||
target.data.name += "-merge"
|
||||
for ifc_definition_id in id_set[group_name][1:]:
|
||||
del self.added_data[ifc_definition_id]
|
||||
@@ -1411,11 +1465,10 @@ class IfcImporter:
|
||||
types_collection.hide_viewport = False
|
||||
bpy.context.view_layer.objects.active = last_obj
|
||||
|
||||
context_override = {}
|
||||
bpy.ops.object.editmode_toggle(context_override)
|
||||
bpy.ops.mesh.tris_convert_to_quads(context_override)
|
||||
bpy.ops.mesh.normals_make_consistent(context_override)
|
||||
bpy.ops.object.editmode_toggle(context_override)
|
||||
bpy.ops.object.editmode_toggle()
|
||||
bpy.ops.mesh.tris_convert_to_quads()
|
||||
bpy.ops.mesh.normals_make_consistent()
|
||||
bpy.ops.object.editmode_toggle()
|
||||
|
||||
types_collection.hide_viewport = True
|
||||
bpy.context.view_layer.objects.active = last_obj
|
||||
@@ -1691,41 +1744,6 @@ class IfcImporter:
|
||||
result[2][3] *= self.unit_scale
|
||||
return result
|
||||
|
||||
def get_body_representation(self, representations):
|
||||
for representation in representations:
|
||||
if (
|
||||
representation.RepresentationIdentifier == "Body"
|
||||
and representation.RepresentationType == "MappedRepresentation"
|
||||
):
|
||||
if len(representation.Items) > 1:
|
||||
return representation
|
||||
return self.get_body_representation([representation.Items[0].MappingSource.MappedRepresentation])
|
||||
elif representation.RepresentationIdentifier == "Body":
|
||||
return representation
|
||||
|
||||
def get_transformed_body_representations(self, representations, matrix=None):
|
||||
if matrix is None:
|
||||
matrix = mathutils.Matrix()
|
||||
results = []
|
||||
for representation in representations:
|
||||
if (
|
||||
representation.RepresentationIdentifier == "Body"
|
||||
and representation.RepresentationType == "MappedRepresentation"
|
||||
):
|
||||
for item in representation.Items:
|
||||
# TODO: Confirm if this transformation is right
|
||||
transform = self.get_axis2placement(item.MappingSource.MappingOrigin)
|
||||
if item.MappingTarget:
|
||||
transform = transform @ self.get_cartesiantransformationoperator(item.MappingTarget)
|
||||
results.extend(
|
||||
self.get_transformed_body_representations(
|
||||
[item.MappingSource.MappedRepresentation], transform @ matrix
|
||||
)
|
||||
)
|
||||
elif representation.RepresentationIdentifier == "Body":
|
||||
results.append({"raw": representation, "matrix": self.scale_matrix(matrix)})
|
||||
return results
|
||||
|
||||
def scale_matrix(self, matrix):
|
||||
matrix[0][3] *= self.unit_scale
|
||||
matrix[1][3] *= self.unit_scale
|
||||
@@ -1908,7 +1926,7 @@ class IfcImporter:
|
||||
|
||||
def link_element(self, element, obj):
|
||||
self.added_data[element.id()] = obj
|
||||
IfcStore.link_element(element, obj)
|
||||
tool.Ifc.link(element, obj)
|
||||
|
||||
def set_matrix_world(self, obj, matrix_world):
|
||||
obj.matrix_world = matrix_world
|
||||
@@ -1921,6 +1939,15 @@ class IfcImporter:
|
||||
bpy.ops.view3d.view_selected()
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
|
||||
def setup_arrays(self):
|
||||
for element in self.file.by_type("IfcElement"):
|
||||
pset_data = ifcopenshell.util.element.get_pset(element, "BBIM_Array")
|
||||
if not pset_data or not pset_data.get("Data", None): # skip array children
|
||||
continue
|
||||
for i in range(len(json.loads(pset_data["Data"]))):
|
||||
tool.Blender.Modifier.Array.set_children_lock_state(element, i, True)
|
||||
tool.Blender.Modifier.Array.constrain_children_to_parent(element)
|
||||
|
||||
|
||||
class IfcImportSettings:
|
||||
def __init__(self):
|
||||
@@ -1943,6 +1970,7 @@ class IfcImportSettings:
|
||||
self.element_limit = 30000
|
||||
self.has_filter = None
|
||||
self.should_filter_spatial_elements = True
|
||||
self.should_setup_viewport_camera = True
|
||||
self.elements = set()
|
||||
self.collection_mode = "DECOMPOSITION"
|
||||
|
||||
|
||||
@@ -202,6 +202,7 @@ class BIM_OT_select_aggregate(bpy.types.Operator):
|
||||
aggregate_obj = tool.Ifc.get_object(aggregate)
|
||||
if aggregate_obj in context.selectable_objects:
|
||||
aggregate_obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = aggregate_obj
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
@@ -62,14 +62,14 @@ class BoundaryDecorator:
|
||||
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 = gpu.shader.from_builtin("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")
|
||||
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
|
||||
selected_vertices = []
|
||||
selected_edges = []
|
||||
|
||||
@@ -31,7 +31,7 @@ class BIM_PT_SceneBoundaries(Panel):
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_geometry"
|
||||
bl_parent_id = "BIM_PT_tab_geometry"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# 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 bpy
|
||||
from . import ui, prop, operator
|
||||
|
||||
classes = (
|
||||
operator.GetBSDDClassificationProperties,
|
||||
operator.LoadBSDDDomains,
|
||||
operator.SearchBSDDClassifications,
|
||||
operator.SetActiveBSDDDomain,
|
||||
prop.BSDDDomain,
|
||||
prop.BSDDClassification,
|
||||
prop.BSDDPset,
|
||||
prop.BIMBSDDProperties,
|
||||
ui.BIM_UL_bsdd_domains,
|
||||
ui.BIM_UL_bsdd_classifications,
|
||||
ui.BIM_PT_bsdd,
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
bpy.types.Scene.BIMBSDDProperties = bpy.props.PointerProperty(type=prop.BIMBSDDProperties)
|
||||
|
||||
|
||||
def unregister():
|
||||
del bpy.types.Scene.BIMBSDDProperties
|
||||
@@ -0,0 +1,132 @@
|
||||
# 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 os
|
||||
import bpy
|
||||
import bsdd
|
||||
import json
|
||||
import ifcopenshell
|
||||
import blenderbim.tool as tool
|
||||
|
||||
|
||||
class LoadBSDDDomains(bpy.types.Operator):
|
||||
bl_idname = "bim.load_bsdd_domains"
|
||||
bl_label = "Load bSDD Domains"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.BIMBSDDProperties
|
||||
props.domains.clear()
|
||||
client = bsdd.Client()
|
||||
for domain in sorted(client.Domain(), key=lambda x: x["name"]):
|
||||
new = props.domains.add()
|
||||
new.name = domain["name"]
|
||||
new.namespace_uri = domain["namespaceUri"]
|
||||
new.default_language_code = domain["defaultLanguageCode"]
|
||||
new.organization_name_owner = domain["organizationNameOwner"]
|
||||
new.status = domain["status"]
|
||||
new.version = domain["version"]
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SetActiveBSDDDomain(bpy.types.Operator):
|
||||
bl_idname = "bim.set_active_bsdd_domain"
|
||||
bl_label = "Load bSDD Domains"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
name: bpy.props.StringProperty()
|
||||
uri: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.BIMBSDDProperties
|
||||
props.active_domain = self.name
|
||||
props.active_uri = self.uri
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SearchBSDDClassifications(bpy.types.Operator):
|
||||
bl_idname = "bim.search_bsdd_classifications"
|
||||
bl_label = "Search bSDD Classifications"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.BIMBSDDProperties
|
||||
props.classifications.clear()
|
||||
client = bsdd.Client()
|
||||
related_ifc_entities = []
|
||||
if len(props.keyword) < 3:
|
||||
return {"FINISHED"}
|
||||
if props.should_filter_ifc_class and context.active_object:
|
||||
element = tool.Ifc.get_entity(context.active_object)
|
||||
if element:
|
||||
related_ifc_entities = [element.is_a()]
|
||||
results = client.ClassificationSearchOpen(props.keyword, DomainNamespaceUris=[props.active_uri], RelatedIfcEntities=related_ifc_entities)
|
||||
for result in sorted(results["classifications"], key=lambda x: x["referenceCode"]):
|
||||
new = props.classifications.add()
|
||||
new.name = result["name"]
|
||||
new.reference_code = result["referenceCode"]
|
||||
new.description = result.get("description", "")
|
||||
new.namespace_uri = result["namespaceUri"]
|
||||
new.domain_name = result["domainName"]
|
||||
new.domain_namespace_uri = result["domainNamespaceUri"]
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class GetBSDDClassificationProperties(bpy.types.Operator):
|
||||
bl_idname = "bim.get_bsdd_classification_properties"
|
||||
bl_label = "Search bSDD Classifications"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
bprops = context.scene.BIMBSDDProperties
|
||||
bprops.classification_psets.clear()
|
||||
bsdd_classification = bprops.classifications[bprops.active_classification_index]
|
||||
client = bsdd.Client()
|
||||
data = client.Classification(bsdd_classification.namespace_uri)
|
||||
|
||||
properties = data.get("classificationProperties", None)
|
||||
if not properties:
|
||||
return {"FINISHED"}
|
||||
|
||||
psets = {}
|
||||
|
||||
for prop in properties:
|
||||
if prop.get("propertyDomainName") != "IFC":
|
||||
continue
|
||||
pset = prop.get("propertySet", None)
|
||||
if not pset:
|
||||
continue
|
||||
psets.setdefault(pset, {})
|
||||
|
||||
predefined_value = prop.get("predefinedValue")
|
||||
if predefined_value:
|
||||
possible_values = [predefined_value]
|
||||
else:
|
||||
possible_values = prop.get("possibleValues", []) or []
|
||||
possible_values = [v["value"] for v in possible_values]
|
||||
|
||||
psets[pset][prop["name"]] = possible_values
|
||||
|
||||
for pset_name, pset in psets.items():
|
||||
new = bprops.classification_psets.add()
|
||||
new.name = pset_name
|
||||
for name, values in pset.items():
|
||||
new2 = new.properties.add()
|
||||
new2.name = name
|
||||
new2.enum_items = json.dumps(values)
|
||||
new2.data_type = "enum"
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,66 @@
|
||||
# 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 bpy
|
||||
from bpy.types import PropertyGroup
|
||||
from blenderbim.bim.prop import Attribute, StrProperty
|
||||
from bpy.props import (
|
||||
PointerProperty,
|
||||
StringProperty,
|
||||
EnumProperty,
|
||||
BoolProperty,
|
||||
IntProperty,
|
||||
FloatProperty,
|
||||
FloatVectorProperty,
|
||||
CollectionProperty,
|
||||
)
|
||||
|
||||
|
||||
class BSDDDomain(PropertyGroup):
|
||||
name: StringProperty(name="Name")
|
||||
namespace_uri: StringProperty(name="URI")
|
||||
default_language_code: StringProperty(name="Language")
|
||||
organization_name_owner: StringProperty(name="Organization")
|
||||
status: StringProperty(name="Status")
|
||||
version: StringProperty(name="Version")
|
||||
|
||||
|
||||
class BSDDClassification(PropertyGroup):
|
||||
name: StringProperty(name="Name")
|
||||
reference_code: StringProperty(name="Reference Code")
|
||||
description: StringProperty(name="Description")
|
||||
namespace_uri: StringProperty(name="Namespace URI")
|
||||
domain_name: StringProperty(name="Domain Name")
|
||||
domain_namespace_uri: StringProperty(name="Domain Namespace URI")
|
||||
|
||||
|
||||
class BSDDPset(PropertyGroup):
|
||||
name: StringProperty(name="Name")
|
||||
properties: CollectionProperty(name="Properties", type=Attribute)
|
||||
|
||||
|
||||
class BIMBSDDProperties(PropertyGroup):
|
||||
active_domain: StringProperty(name="Active Domain")
|
||||
active_uri: StringProperty(name="Active URI")
|
||||
domains: CollectionProperty(name="Domains", type=BSDDDomain)
|
||||
active_domain_index: IntProperty(name="Active Domain Index")
|
||||
classifications: CollectionProperty(name="Classifications", type=BSDDClassification)
|
||||
active_classification_index: IntProperty(name="Active Classification Index")
|
||||
keyword: StringProperty(name="Keyword")
|
||||
should_filter_ifc_class: BoolProperty(name="Filter Active IFC Class", default=True)
|
||||
classification_psets: CollectionProperty(name="Classification Psets", type=BSDDPset)
|
||||
@@ -0,0 +1,71 @@
|
||||
# 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 blenderbim.tool as tool
|
||||
from bpy.types import Panel, UIList
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
|
||||
|
||||
class BIM_PT_bsdd(Panel):
|
||||
bl_label = "buildingSMART Data Dictionary"
|
||||
bl_idname = "BIM_PT_bsdd"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_tab_project_setup"
|
||||
|
||||
def draw(self, context):
|
||||
props = context.scene.BIMBSDDProperties
|
||||
if props.active_domain:
|
||||
row = self.layout.row()
|
||||
row.label(text="Active: " + props.active_domain, icon="URL")
|
||||
else:
|
||||
row = self.layout.row()
|
||||
row.label(text="No Active bSDD Domain", icon="ERROR")
|
||||
|
||||
if len(props.domains):
|
||||
self.layout.template_list(
|
||||
"BIM_UL_bsdd_domains",
|
||||
"",
|
||||
props,
|
||||
"domains",
|
||||
props,
|
||||
"active_domain_index",
|
||||
)
|
||||
else:
|
||||
row = self.layout.row()
|
||||
row.operator("bim.load_bsdd_domains")
|
||||
|
||||
|
||||
class BIM_UL_bsdd_domains(UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
if item:
|
||||
row = layout.row(align=True)
|
||||
row.label(text=f"{item.name} ({item.organization_name_owner})")
|
||||
op = row.operator("bim.set_active_bsdd_domain", text="", icon="RESTRICT_SELECT_OFF")
|
||||
op.name = item.name
|
||||
op.uri = item.namespace_uri
|
||||
|
||||
|
||||
class BIM_UL_bsdd_classifications(UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
if item:
|
||||
row = layout.row(align=True)
|
||||
row.label(text=item.reference_code)
|
||||
row.label(text=item.name)
|
||||
@@ -336,8 +336,15 @@ class SelectIfcClashResults(bpy.types.Operator):
|
||||
else:
|
||||
element_file = self.file
|
||||
|
||||
element = element_file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
if element.GlobalId in global_ids:
|
||||
try:
|
||||
element = element_file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
except:
|
||||
continue
|
||||
|
||||
global_id = getattr(element, "GlobalId", None)
|
||||
if not global_id:
|
||||
continue
|
||||
if global_id in global_ids:
|
||||
obj.select_set(True)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -21,7 +21,9 @@ from . import ui, prop, operator
|
||||
|
||||
classes = (
|
||||
operator.AddClassification,
|
||||
operator.AddClassificationFromBSDD,
|
||||
operator.AddClassificationReference,
|
||||
operator.AddClassificationReferenceFromBSDD,
|
||||
operator.ChangeClassificationLevel,
|
||||
operator.DisableEditingClassification,
|
||||
operator.DisableEditingClassificationReference,
|
||||
|
||||
@@ -54,6 +54,25 @@ class AddClassification(bpy.types.Operator, tool.Ifc.Operator):
|
||||
)
|
||||
|
||||
|
||||
class AddClassificationFromBSDD(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_classification_from_bsdd"
|
||||
bl_label = "Add Classification From bSDD"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMBSDDProperties
|
||||
domain = [d for d in props.domains if d.name == props.active_domain][0]
|
||||
for element in tool.Ifc.get().by_type("IfcClassification"):
|
||||
if element.Name == props.active_domain or element.Location == domain.namespace_uri:
|
||||
return
|
||||
classification = ifcopenshell.api.run(
|
||||
"classification.add_classification", tool.Ifc.get(), classification=props.active_domain
|
||||
)
|
||||
classification.Source = domain.organization_name_owner
|
||||
classification.Location = domain.namespace_uri
|
||||
classification.Edition = domain.version
|
||||
|
||||
|
||||
class EnableEditingClassification(bpy.types.Operator):
|
||||
bl_idname = "bim.enable_editing_classification"
|
||||
bl_label = "Enable Editing Classification"
|
||||
@@ -254,6 +273,70 @@ class AddClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
|
||||
)
|
||||
|
||||
|
||||
class AddClassificationReferenceFromBSDD(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_classification_reference_from_bsdd"
|
||||
bl_label = "Add Classification Reference From bSDD"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
obj_type: bpy.props.StringProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
if self.obj_type == "Object":
|
||||
if context.selected_objects:
|
||||
objects = [o.name for o in context.selected_objects]
|
||||
else:
|
||||
objects = [context.active_object.name]
|
||||
else:
|
||||
objects = [self.obj]
|
||||
props = context.scene.BIMClassificationProperties
|
||||
bprops = context.scene.BIMBSDDProperties
|
||||
|
||||
bsdd_classification = bprops.classifications[bprops.active_classification_index]
|
||||
|
||||
classification = None
|
||||
for element in tool.Ifc.get().by_type("IfcClassification"):
|
||||
if (
|
||||
element.Name == bsdd_classification.domain_name
|
||||
or element.Location == bsdd_classification.domain_namespace_uri
|
||||
):
|
||||
classification = element
|
||||
break
|
||||
|
||||
if not classification:
|
||||
classification = ifcopenshell.api.run(
|
||||
"classification.add_classification", tool.Ifc.get(), classification=bsdd_classification.domain_name
|
||||
)
|
||||
classification.Location = bsdd_classification.domain_namespace_uri
|
||||
|
||||
for obj in objects:
|
||||
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, obj, self.obj_type)
|
||||
if not ifc_definition_id:
|
||||
continue
|
||||
element = tool.Ifc.get().by_id(ifc_definition_id)
|
||||
reference = ifcopenshell.api.run(
|
||||
"classification.add_reference",
|
||||
tool.Ifc.get(),
|
||||
product=element,
|
||||
classification=classification,
|
||||
identification=bsdd_classification.reference_code,
|
||||
name=bsdd_classification.name,
|
||||
)
|
||||
reference.Location = bsdd_classification.namespace_uri
|
||||
|
||||
for classification_pset in bprops.classification_psets:
|
||||
pset = ifcopenshell.util.element.get_pset(element, classification_pset.name)
|
||||
if pset:
|
||||
pset = tool.Ifc.get().by_id(pset["id"])
|
||||
else:
|
||||
pset = ifcopenshell.api.run(
|
||||
"pset.add_pset", tool.Ifc.get(), product=element, name=classification_pset.name
|
||||
)
|
||||
properties = {}
|
||||
for prop in classification_pset.properties:
|
||||
properties[prop.name] = prop.get_value()
|
||||
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties=properties)
|
||||
|
||||
|
||||
class ChangeClassificationLevel(bpy.types.Operator):
|
||||
bl_idname = "bim.change_classification_level"
|
||||
bl_label = "Change Classification Level"
|
||||
|
||||
@@ -48,6 +48,15 @@ class ClassificationReference(PropertyGroup):
|
||||
|
||||
|
||||
class BIMClassificationProperties(PropertyGroup):
|
||||
classification_source: EnumProperty(
|
||||
items=[
|
||||
("FILE", "IFC File", ""),
|
||||
("BSDD", "buildingSMART Data Dictionary", ""),
|
||||
("MANUAL", "Manual Entry", ""),
|
||||
],
|
||||
name="Classification Source",
|
||||
default="FILE",
|
||||
)
|
||||
available_classifications: EnumProperty(items=get_available_classifications, name="Available Classifications")
|
||||
classification_attributes: CollectionProperty(name="Classification Attributes", type=Attribute)
|
||||
active_classification_id: IntProperty(name="Active Classification Id")
|
||||
|
||||
@@ -36,7 +36,7 @@ class BIM_PT_classifications(Panel):
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_project_setup"
|
||||
bl_parent_id = "BIM_PT_tab_project_setup"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@@ -48,6 +48,42 @@ class BIM_PT_classifications(Panel):
|
||||
|
||||
self.props = context.scene.BIMClassificationProperties
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Source", icon="OUTLINER")
|
||||
row.prop(self.props, "classification_source", text="")
|
||||
|
||||
if self.props.classification_source == "FILE":
|
||||
self.draw_add_file_ui(context)
|
||||
elif self.props.classification_source == "BSDD":
|
||||
self.draw_add_bsdd_ui(context)
|
||||
elif self.props.classification_source == "MANUAL":
|
||||
self.draw_add_manual_ui(context)
|
||||
|
||||
for classification in ClassificationsData.data["classifications"]:
|
||||
if self.props.active_classification_id == classification["id"]:
|
||||
self.draw_editable_ui()
|
||||
else:
|
||||
self.draw_ui(classification)
|
||||
|
||||
def draw_add_manual_ui(self, context):
|
||||
row = self.layout.row()
|
||||
row.label(text="TODO", icon="ERROR")
|
||||
|
||||
def draw_add_bsdd_ui(self, context):
|
||||
self.bprops = context.scene.BIMBSDDProperties
|
||||
|
||||
if not self.bprops.active_domain:
|
||||
row = self.layout.row()
|
||||
row.label(text="No Active bSDD Domain", icon="ERROR")
|
||||
return
|
||||
|
||||
row = self.layout.row()
|
||||
row.label(text="Active: " + self.bprops.active_domain, icon="URL")
|
||||
|
||||
row = self.layout.row()
|
||||
row.operator("bim.add_classification_from_bsdd", icon="ADD")
|
||||
|
||||
def draw_add_file_ui(self, context):
|
||||
if ClassificationsData.data["has_classification_file"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.props, "available_classifications", text="")
|
||||
@@ -58,12 +94,6 @@ class BIM_PT_classifications(Panel):
|
||||
row.label(text="No Active Classification Library")
|
||||
row.operator("bim.load_classification_library", text="", icon="IMPORT")
|
||||
|
||||
for classification in ClassificationsData.data["classifications"]:
|
||||
if self.props.active_classification_id == classification["id"]:
|
||||
self.draw_editable_ui()
|
||||
else:
|
||||
self.draw_ui(classification)
|
||||
|
||||
def draw_editable_ui(self):
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.edit_classification", text="Save changes", icon="CHECKMARK")
|
||||
@@ -84,6 +114,7 @@ class ReferenceUI:
|
||||
obj = context.active_object
|
||||
self.oprops = obj.BIMObjectProperties
|
||||
self.sprops = context.scene.BIMClassificationProperties
|
||||
self.bprops = context.scene.BIMBSDDProperties
|
||||
self.props = obj.BIMClassificationReferenceProperties
|
||||
self.file = IfcStore.get_file()
|
||||
|
||||
@@ -100,14 +131,76 @@ class ReferenceUI:
|
||||
self.draw_reference_ui(reference)
|
||||
|
||||
def draw_add_ui(self, context):
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Source", icon="OUTLINER")
|
||||
row.prop(self.sprops, "classification_source", text="")
|
||||
|
||||
if self.sprops.classification_source == "FILE":
|
||||
self.draw_add_file_ui(context)
|
||||
elif self.sprops.classification_source == "BSDD":
|
||||
self.draw_add_bsdd_ui(context)
|
||||
elif self.sprops.classification_source == "MANUAL":
|
||||
self.draw_add_manual_ui(context)
|
||||
|
||||
def draw_add_manual_ui(self, context):
|
||||
row = self.layout.row()
|
||||
row.label(text="TODO", icon="ERROR")
|
||||
|
||||
def draw_add_bsdd_ui(self, context):
|
||||
if not self.bprops.active_domain:
|
||||
row = self.layout.row()
|
||||
row.label(text="No Active bSDD Domain", icon="ERROR")
|
||||
return
|
||||
|
||||
row = self.layout.row()
|
||||
row.label(text="Active: " + self.bprops.active_domain, icon="URL")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.bprops, "keyword", text="")
|
||||
row.operator("bim.search_bsdd_classifications", text="", icon="VIEWZOOM")
|
||||
|
||||
row = self.layout.row()
|
||||
row.prop(self.bprops, "should_filter_ifc_class")
|
||||
|
||||
if len(self.bprops.classifications):
|
||||
self.layout.template_list(
|
||||
"BIM_UL_bsdd_classifications",
|
||||
"",
|
||||
self.bprops,
|
||||
"classifications",
|
||||
self.bprops,
|
||||
"active_classification_index",
|
||||
)
|
||||
else:
|
||||
row = self.layout.row()
|
||||
row.label(text="No Search Results")
|
||||
|
||||
if self.bprops.active_classification_index < len(self.bprops.classifications):
|
||||
row = self.layout.row(align=True)
|
||||
op = row.operator(
|
||||
"bim.add_classification_reference_from_bsdd", text="Add Classification Reference", icon="ADD"
|
||||
)
|
||||
op.obj = self.obj
|
||||
op.obj_type = self.obj_type
|
||||
row.operator("bim.get_bsdd_classification_properties", text="", icon="COPY_ID")
|
||||
|
||||
if len(self.bprops.classification_psets):
|
||||
for pset in self.bprops.classification_psets:
|
||||
box = self.layout.box()
|
||||
row = box.row()
|
||||
row.label(text=pset.name, icon="COPY_ID")
|
||||
blenderbim.bim.helper.draw_attributes(pset.properties, box)
|
||||
|
||||
def draw_add_file_ui(self, context):
|
||||
if not self.data.data["active_classification_library"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="No Active Classification Library")
|
||||
row.label(text="No Active Classification Library", icon="ERROR")
|
||||
row.operator("bim.load_classification_library", text="", icon="IMPORT")
|
||||
return
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=f"Active Classification Library: {self.data.data['active_classification_library']}")
|
||||
#row.prop(self.sprops, "available_classifications", text="")
|
||||
# row.prop(self.sprops, "available_classifications", text="")
|
||||
if not self.sprops.available_library_references:
|
||||
op = row.operator("bim.change_classification_level", text="", icon="GREASEPENCIL")
|
||||
op.parent_id = int(self.sprops.available_classifications)
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2020, 2021 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 bpy
|
||||
import os
|
||||
import logging
|
||||
import ifcopenshell
|
||||
import json
|
||||
import webbrowser
|
||||
import tempfile
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
|
||||
|
||||
class SelectCobieIfcFile(bpy.types.Operator):
|
||||
bl_idname = "bim.select_cobie_ifc_file"
|
||||
bl_label = "Select COBie IFC File"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.COBieProperties.cobie_ifc_file = self.filepath
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class SelectCobieJsonFile(bpy.types.Operator):
|
||||
bl_idname = "bim.select_cobie_json_file"
|
||||
bl_label = "Select COBie JSON File"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.COBieProperties.cobie_json_file = self.filepath
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class ExecuteIfcCobie(bpy.types.Operator):
|
||||
bl_idname = "bim.execute_ifc_cobie"
|
||||
bl_label = "Execute IFCCOBie"
|
||||
file_format: bpy.props.StringProperty()
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
props = context.scene.COBieProperties
|
||||
return props.should_load_from_memory or props.cobie_ifc_file
|
||||
|
||||
def execute(self, context):
|
||||
from cobie import IfcCobieParser
|
||||
|
||||
props = context.scene.COBieProperties
|
||||
|
||||
if props.should_load_from_memory:
|
||||
output_dir = tempfile.gettempdir()
|
||||
else:
|
||||
output_dir = os.path.dirname(props.cobie_ifc_file)
|
||||
|
||||
output = os.path.join(output_dir, "output")
|
||||
logger = logging.getLogger("IFCtoCOBie")
|
||||
fh = logging.FileHandler(os.path.join(output_dir, "cobie.log"))
|
||||
fh.setLevel(logging.DEBUG)
|
||||
fh.setFormatter(logging.Formatter("%(asctime)s : %(levelname)s : %(message)s"))
|
||||
logger = logging.getLogger("IFCtoCOBie")
|
||||
logger.addHandler(fh)
|
||||
selector = ifcopenshell.util.selector.Selector()
|
||||
if props.cobie_json_file:
|
||||
with open(props.cobie_json_file, "r") as f:
|
||||
custom_data = json.load(f)
|
||||
else:
|
||||
custom_data = {}
|
||||
parser = IfcCobieParser(logger, selector)
|
||||
|
||||
ifc_file = IfcStore.get_file()
|
||||
|
||||
if not (ifc_file and props.should_load_from_memory):
|
||||
ifc_file = props.cobie_ifc_file
|
||||
|
||||
parser.parse(
|
||||
ifc_file,
|
||||
props.cobie_types,
|
||||
props.cobie_components,
|
||||
custom_data,
|
||||
)
|
||||
if self.file_format == "xlsx":
|
||||
from cobie import CobieXlsWriter
|
||||
|
||||
writer = CobieXlsWriter(parser, output)
|
||||
writer.write()
|
||||
webbrowser.open("file://" + output + "." + self.file_format)
|
||||
elif self.file_format == "ods":
|
||||
from cobie import CobieOdsWriter
|
||||
|
||||
writer = CobieOdsWriter(parser, output)
|
||||
writer.write()
|
||||
webbrowser.open("file://" + output + "." + self.file_format)
|
||||
else:
|
||||
from cobie import CobieCsvWriter
|
||||
|
||||
writer = CobieCsvWriter(parser, output_dir)
|
||||
writer.write()
|
||||
webbrowser.open("file://" + output_dir)
|
||||
webbrowser.open("file://" + output_dir + "/cobie.log")
|
||||
return {"FINISHED"}
|
||||
@@ -29,7 +29,7 @@ class BIM_PT_constraints(Panel):
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_project_setup"
|
||||
bl_parent_id = "BIM_PT_tab_project_setup"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
|
||||
@@ -29,7 +29,7 @@ class BIM_PT_context(bpy.types.Panel):
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_geometry"
|
||||
bl_parent_id = "BIM_PT_tab_geometry"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
|
||||
@@ -136,13 +136,11 @@ class CostSchedulesData:
|
||||
data["UnitBasisUnitSymbol"] = "U"
|
||||
if cost_value.Category == "*":
|
||||
is_sum = True
|
||||
cost_quantity = data["TotalCostQuantity"] or 1
|
||||
if has_unit_basis:
|
||||
data["TotalCost"] = data["TotalAppliedValue"] / data["UnitBasisValueComponent"]
|
||||
data["TotalCost"] = data["TotalAppliedValue"] * cost_quantity / data["UnitBasisValueComponent"]
|
||||
else:
|
||||
if data["TotalCostQuantity"] is not None:
|
||||
data["TotalCost"] = data["TotalAppliedValue"] * data["TotalCostQuantity"]
|
||||
else:
|
||||
data["TotalCost"] = data["TotalAppliedValue"]
|
||||
data["TotalCost"] = data["TotalAppliedValue"] * cost_quantity
|
||||
if is_sum:
|
||||
data["TotalAppliedValue"] = None
|
||||
|
||||
@@ -160,8 +158,10 @@ class CostSchedulesData:
|
||||
unit = ifcopenshell.util.unit.get_property_unit(quantity, tool.Ifc.get())
|
||||
if unit:
|
||||
data["UnitSymbol"] = ifcopenshell.util.unit.get_unit_symbol(unit)
|
||||
else:
|
||||
data["UnitSymbol"] = "U"
|
||||
if quantity.is_a("IfcPhysicalSimpleQuantity"):
|
||||
measure_class = quantity.wrapped_data.declaration().as_entity().attribute_by_index(3).type_of_attribute().declared_type().name()
|
||||
if "Count" in measure_class:
|
||||
data["UnitSymbol"] = "U"
|
||||
|
||||
# same_unit_nested_cost_item = set()
|
||||
# data["DerivedTotalCostQuantity"] = None
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2020, 2021 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 bpy
|
||||
from . import prop, workspace
|
||||
|
||||
classes = (
|
||||
prop.BIMCoveringProperties,
|
||||
workspace.Hotkey,
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
if not bpy.app.background:
|
||||
bpy.utils.register_tool(workspace.CoveringTool, after={"bim.structural_tool"}, separator=False, group=False)
|
||||
bpy.types.Scene.BIMCoveringProperties = bpy.props.PointerProperty(type=prop.BIMCoveringProperties)
|
||||
# bpy.types.Object.BIMObjectSpatialProperties = bpy.props.PointerProperty(type=prop.BIMObjectSpatialProperties)
|
||||
# bpy.types.Scene.BIMSpatialManagerProperties = bpy.props.PointerProperty(type=prop.BIMSpatialManagerProperties)
|
||||
|
||||
|
||||
def unregister():
|
||||
if not bpy.app.background:
|
||||
bpy.utils.unregister_tool(workspace.CoveringTool)
|
||||
del bpy.types.Scene.BIMCoveringProperties
|
||||
# del bpy.types.Object.BIMObjectSpatialProperties
|
||||
# del bpy.types.Scene.BIMSpatialManagerProperties
|
||||
Binary file not shown.
+7
-6
@@ -17,6 +17,8 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
from blenderbim.bim.prop import StrProperty, Attribute
|
||||
#from blenderbim.bim.module.spatial.data import SpatialData
|
||||
from bpy.types import PropertyGroup
|
||||
from bpy.props import (
|
||||
PointerProperty,
|
||||
@@ -28,11 +30,10 @@ from bpy.props import (
|
||||
FloatVectorProperty,
|
||||
CollectionProperty,
|
||||
)
|
||||
#import blenderbim.tool as tool
|
||||
#import blenderbim.core.geometry
|
||||
#import ifcopenshell
|
||||
|
||||
|
||||
class COBieProperties(PropertyGroup):
|
||||
cobie_ifc_file: StringProperty(default="", name="COBie IFC File")
|
||||
cobie_types: StringProperty(default=".COBieType", name="COBie Types")
|
||||
cobie_components: StringProperty(default=".COBie", name="COBie Components")
|
||||
cobie_json_file: StringProperty(default="", name="COBie JSON File")
|
||||
should_load_from_memory: BoolProperty(default=False, name="Load from Memory")
|
||||
class BIMCoveringProperties(PropertyGroup):
|
||||
ceiling_height: bpy.props.FloatProperty(name="ceiling_height", default=2.7, subtype="DISTANCE", description="Ceiling height")
|
||||
@@ -0,0 +1,272 @@
|
||||
# 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 ifcopenshell
|
||||
import blenderbim.tool as tool
|
||||
from blenderbim.bim.helper import prop_with_search
|
||||
from blenderbim.bim.module.model.data import AuthoringData
|
||||
from bpy.types import WorkSpaceTool
|
||||
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 CoveringTool(WorkSpaceTool):
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_context_mode = "OBJECT"
|
||||
bl_idname = "bim.covering_tool"
|
||||
bl_label = "Covering Tool"
|
||||
bl_description = "Create and edit coverings"
|
||||
bl_icon = os.path.join(os.path.dirname(__file__), "ops.authoring.covering")
|
||||
bl_widget = None
|
||||
ifc_element_type = "IfcCoveringType"
|
||||
bl_keymap = tool.Blender.get_default_selection_keypmap() + (
|
||||
("bim.covering_hotkey", {"type": "A", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_A")]}),
|
||||
("bim.covering_hotkey", {"type": "G", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_G")]}),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def draw_settings(cls, context, layout, ws_tool):
|
||||
CoveringToolUI.draw(context, layout, ifc_element_type=cls.ifc_element_type)
|
||||
|
||||
def add_layout_hotkey(layout, text, hotkey, description):
|
||||
args = ["covering", layout, text, hotkey, description]
|
||||
tool.Blender.add_layout_hotkey_operator(*args)
|
||||
|
||||
class CoveringToolUI:
|
||||
@classmethod
|
||||
def draw(cls, context, layout, ifc_element_type = None):
|
||||
cls.layout = layout
|
||||
cls.props = context.scene.BIMModelProperties
|
||||
cls.covering_props = context.scene.BIMCoveringProperties
|
||||
|
||||
|
||||
row = cls.layout.row(align=True)
|
||||
if not tool.Ifc.get():
|
||||
row.label(text="No IFC Project", icon="ERROR")
|
||||
return
|
||||
|
||||
if not AuthoringData.is_loaded:
|
||||
AuthoringData.load(ifc_element_type)
|
||||
elif AuthoringData.data["ifc_element_type"] != ifc_element_type:
|
||||
AuthoringData.load(ifc_element_type)
|
||||
|
||||
if context.region.type == "TOOL_HEADER":
|
||||
cls.draw_header_interface()
|
||||
elif context.region.type in ("UI", "WINDOW"):
|
||||
cls.draw_basic_bim_tool_interface()
|
||||
|
||||
cls.draw_default_interface()
|
||||
|
||||
|
||||
|
||||
@classmethod
|
||||
def draw_header_interface(cls):
|
||||
cls.draw_type_selection_interface()
|
||||
|
||||
@classmethod
|
||||
def draw_default_interface(cls):
|
||||
row = cls.layout.row(align=True)
|
||||
row.prop(data=cls.props, property="rl3", text="RL")
|
||||
row = cls.layout.row(align=True)
|
||||
row.prop(data=cls.covering_props, property="ceiling_height", text="Ceiling Height")
|
||||
if AuthoringData.data["ifc_classes"]:
|
||||
active_obj = bpy.context.active_object
|
||||
element = tool.Ifc.get_entity(active_obj)
|
||||
collection = bpy.context.view_layer.active_layer_collection.collection
|
||||
collection_obj = collection.BIMCollectionProperties.obj
|
||||
|
||||
relating_type_id = int(cls.props.relating_type_id)
|
||||
type_material_usage = ifcopenshell.util.element.get_material(tool.Ifc.get().by_id(relating_type_id)).is_a()
|
||||
|
||||
# PLEASE KEEP COMMENTS AS A REMINDER
|
||||
# elif element and bpy.context.selected_objects and element.is_a("IfcSpace"):
|
||||
# op. = row.operator("bim.add_istance_flooring_from_spaces"):
|
||||
|
||||
if (type_material_usage == "IfcMaterialLayerSet" and
|
||||
not bpy.context.selected_objects):
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_A")
|
||||
if tool.Ifc.get_entity(collection_obj):
|
||||
if AuthoringData.data["predefined_type"] == "FLOORING":
|
||||
op = row.operator("bim.add_instance_flooring_covering_from_cursor")
|
||||
elif AuthoringData.data["predefined_type"] == "CEILING":
|
||||
op = row.operator("bim.add_instance_ceiling_covering_from_cursor")
|
||||
else:
|
||||
op = row.operator("bim.add_constr_type_instance", text="Add")
|
||||
op.from_invoke = True
|
||||
if cls.props.relating_type_id.isnumeric():
|
||||
op.relating_type_id = int(cls.props.relating_type_id)
|
||||
|
||||
else:
|
||||
op = row.operator("bim.add_constr_type_instance", text="Add")
|
||||
op.from_invoke = True
|
||||
if cls.props.relating_type_id.isnumeric():
|
||||
op.relating_type_id = int(cls.props.relating_type_id)
|
||||
|
||||
elif (AuthoringData.data["predefined_type"] == "FLOORING" and
|
||||
type_material_usage == "IfcMaterialLayerSet" and
|
||||
element and
|
||||
bpy.context.selected_objects and
|
||||
element.is_a("IfcWall")):
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_A")
|
||||
op = row.operator("bim.add_instance_flooring_coverings_from_walls")
|
||||
|
||||
elif (AuthoringData.data["predefined_type"] == "CEILING" and
|
||||
type_material_usage == "IfcMaterialLayerSet" and
|
||||
element and
|
||||
bpy.context.selected_objects and
|
||||
element.is_a("IfcWall")):
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_A")
|
||||
op = row.operator("bim.add_instance_ceiling_coverings_from_walls")
|
||||
|
||||
elif (element and
|
||||
bpy.context.selected_objects and
|
||||
element.is_a("IfcCovering") and
|
||||
# AuthoringData.data["predefined_type"] == "FLOORING" and
|
||||
AuthoringData.data["active_material_usage"] == "LAYER3"):
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_G")
|
||||
op = row.operator("bim.regen_selected_covering_object")
|
||||
|
||||
|
||||
|
||||
# elif AuthoringData.data["predefined_type"] == "CEILING":
|
||||
# row = cls.layout.row(align=True)
|
||||
# row.prop(data=cls.props, property="ceiling_height", text="ceiling height")
|
||||
# if element and bpy.context.selected_objects and element.is_a("IfcWall"):
|
||||
# op = row.operator("bim.add_instance_ceiling_coverings_from_walls")
|
||||
# elif element and bpy.context.selected_objects and element.is_a("IfcSpace"):
|
||||
# op. = row.operator("bim.add_istance_flooring_from_spaces"):
|
||||
# else:
|
||||
# op = row.operator("bim.add_instance_ceiling_from_cursor")
|
||||
# op = row.operator("bim.add_constr_type_instance", text="Add")
|
||||
# op.from_invoke = True
|
||||
# if cls.props.relating_type_id.isnumeric():
|
||||
# op.relating_type_id = int(cls.props.relating_type_id)
|
||||
else:
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_A")
|
||||
op = row.operator("bim.add_constr_type_instance", text="Add")
|
||||
op.from_invoke = True
|
||||
if cls.props.relating_type_id.isnumeric():
|
||||
op.relating_type_id = int(cls.props.relating_type_id)
|
||||
|
||||
@classmethod
|
||||
def draw_type_selection_interface(cls):
|
||||
# shared by both sidebar and header
|
||||
row = cls.layout.row(align=True)
|
||||
if AuthoringData.data["ifc_classes"]:
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="FILE_3D")
|
||||
prop_with_search(row, cls.props, "relating_type_id", text="")
|
||||
row.operator("bim.launch_type_manager", icon="LIGHTPROBE_GRID", text="")
|
||||
else:
|
||||
row.label(text=f"No {AuthoringData.data['ifc_element_type']} Found", icon="ERROR")
|
||||
row = cls.layout.row()
|
||||
row.operator("bim.launch_type_manager", icon="LIGHTPROBE_GRID", text="Launch Type Manager")
|
||||
|
||||
@classmethod
|
||||
def draw_basic_bim_tool_interface(cls):
|
||||
cls.draw_type_selection_interface()
|
||||
|
||||
if AuthoringData.data["ifc_classes"]:
|
||||
if cls.props.ifc_class:
|
||||
box = cls.layout.box()
|
||||
if AuthoringData.data["type_thumbnail"]:
|
||||
box.template_icon(icon_value=AuthoringData.data["type_thumbnail"], scale=5)
|
||||
else:
|
||||
op = box.operator("bim.load_type_thumbnails", text="Load Thumbnails", icon="FILE_REFRESH")
|
||||
op.ifc_class = cls.props.ifc_class
|
||||
|
||||
|
||||
class Hotkey(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.covering_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.BIMCoveringProperties
|
||||
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.BIMSpatialProperties
|
||||
return self.execute(context)
|
||||
|
||||
def draw(self, context):
|
||||
pass
|
||||
|
||||
def hotkey_S_A(self):
|
||||
active_obj = bpy.context.active_object
|
||||
element = tool.Ifc.get_entity(active_obj)
|
||||
collection = bpy.context.view_layer.active_layer_collection.collection
|
||||
collection_obj = collection.BIMCollectionProperties.obj
|
||||
|
||||
if AuthoringData.data["predefined_type"] == "FLOORING":
|
||||
if element and bpy.context.selected_objects and element.is_a("IfcWall"):
|
||||
bpy.ops.bim.add_instance_flooring_coverings_from_walls()
|
||||
elif tool.Ifc.get_entity(collection_obj):
|
||||
bpy.ops.bim.add_instance_flooring_covering_from_cursor()
|
||||
else:
|
||||
bpy.ops.bim.add_constr_type_instance()
|
||||
elif AuthoringData.data["predefined_type"] == "CEILING":
|
||||
if element and bpy.context.selected_objects and element.is_a("IfcWall"):
|
||||
bpy.ops.bim.add_instance_ceiling_coverings_from_walls()
|
||||
elif tool.Ifc.get_entity(collection_obj):
|
||||
bpy.ops.bim.add_instance_ceiling_covering_from_cursor()
|
||||
else:
|
||||
bpy.ops.bim.add_constr_type_instance()
|
||||
else:
|
||||
bpy.ops.bim.add_constr_type_instance()
|
||||
|
||||
def hotkey_S_G(self):
|
||||
active_obj = bpy.context.active_object
|
||||
element = tool.Ifc.get_entity(active_obj)
|
||||
if (element and
|
||||
bpy.context.selected_objects and
|
||||
element.is_a("IfcCovering") and
|
||||
AuthoringData.data["active_material_usage"] == "LAYER3"):
|
||||
bpy.ops.bim.regen_selected_covering_object()
|
||||
|
||||
@@ -22,11 +22,10 @@ import json
|
||||
import ifccsv
|
||||
import logging
|
||||
import tempfile
|
||||
import webbrowser
|
||||
import ifcopenshell
|
||||
import blenderbim.tool as tool
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.handler import purge_module_data
|
||||
from blenderbim.bim.handler import refresh_ui_data
|
||||
|
||||
|
||||
class AddCsvAttribute(bpy.types.Operator):
|
||||
@@ -208,6 +207,7 @@ class ExportIfcCsv(bpy.types.Operator):
|
||||
empty=props.empty_value,
|
||||
bool_true=props.true_value,
|
||||
bool_false=props.false_value,
|
||||
concat=props.concat_value,
|
||||
sort=sort,
|
||||
groups=groups,
|
||||
summaries=summaries,
|
||||
@@ -251,7 +251,7 @@ class ImportIfcCsv(bpy.types.Operator):
|
||||
)
|
||||
if not props.should_load_from_memory:
|
||||
ifc_file.write(props.csv_ifc_file)
|
||||
purge_module_data()
|
||||
refresh_ui_data()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
@@ -72,6 +72,7 @@ class CsvProperties(PropertyGroup):
|
||||
empty_value: StringProperty(default="-", name="Empty String Value")
|
||||
true_value: StringProperty(default="YES", name="True Value")
|
||||
false_value: StringProperty(default="NO", name="False Value")
|
||||
concat_value: StringProperty(default=", ", name="Concat Value")
|
||||
csv_delimiter: EnumProperty(
|
||||
items=[
|
||||
(";", ";", ""),
|
||||
|
||||
@@ -79,6 +79,8 @@ class BIM_PT_ifccsv(Panel):
|
||||
row.prop(props, "true_value")
|
||||
row = layout.row()
|
||||
row.prop(props, "false_value")
|
||||
row = layout.row()
|
||||
row.prop(props, "concat_value")
|
||||
layout.use_property_split = False
|
||||
|
||||
blenderbim.bim.helper.draw_filter(self.layout, props, SearchData, "csv")
|
||||
|
||||
@@ -20,23 +20,26 @@ import bpy
|
||||
from . import ui, prop, operator
|
||||
|
||||
classes = (
|
||||
operator.ConvertToBlender,
|
||||
operator.CopyDebugInformation,
|
||||
operator.CreateAllShapes,
|
||||
operator.CreateShapeFromStepId,
|
||||
operator.InspectFromObject,
|
||||
operator.InspectFromStepId,
|
||||
operator.OverrideDisplayType,
|
||||
operator.ParseExpress,
|
||||
operator.PrintIfcFile,
|
||||
operator.PrintObjectPlacement,
|
||||
operator.ProfileImportIFC,
|
||||
operator.PurgeHdf5Cache,
|
||||
operator.PurgeIfcLinks,
|
||||
operator.ConvertToBlender,
|
||||
operator.RewindInspector,
|
||||
operator.SelectExpressFile,
|
||||
operator.SelectHighPolygonMeshes,
|
||||
operator.SelectHighestPolygonMeshes,
|
||||
operator.ValidateIfcFile,
|
||||
operator.PrintUnusedElementStats,
|
||||
operator.PurgeUnusedElementsByClass,
|
||||
prop.BIMDebugProperties,
|
||||
ui.BIM_PT_debug,
|
||||
)
|
||||
|
||||
@@ -116,7 +116,7 @@ class PurgeIfcLinks(bpy.types.Operator):
|
||||
context.scene.BIMProperties.ifc_file = ""
|
||||
context.scene.BIMDebugProperties.attributes.clear()
|
||||
IfcStore.purge()
|
||||
blenderbim.bim.handler.purge_module_data()
|
||||
blenderbim.bim.handler.refresh_ui_data()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -136,7 +136,7 @@ class ConvertToBlender(bpy.types.Operator):
|
||||
m.BIMMaterialProperties.ifc_style_id = False
|
||||
bpy.context.scene.BIMProperties.ifc_file = ""
|
||||
IfcStore.purge()
|
||||
blenderbim.bim.handler.purge_module_data()
|
||||
blenderbim.bim.handler.refresh_ui_data()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -218,7 +218,7 @@ class CreateAllShapes(bpy.types.Operator):
|
||||
len(shape.geometry.edges),
|
||||
len(shape.geometry.faces),
|
||||
)
|
||||
print(f"Failures: {len(failures)}")
|
||||
self.report({"INFO"}, f"Failed shapes: {len(failures)}, check the system console for details.")
|
||||
for failure in failures:
|
||||
print(failure)
|
||||
return {"FINISHED"}
|
||||
@@ -443,3 +443,55 @@ class PurgeHdf5Cache(bpy.types.Operator):
|
||||
def execute(self, context):
|
||||
core.purge_hdf5_cache(tool.Debug)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class OverrideDisplayType(bpy.types.Operator):
|
||||
bl_idname = "bim.override_display_type"
|
||||
bl_label = "Override Display Type"
|
||||
display: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
for obj in context.selected_objects:
|
||||
obj.display_type = self.display
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class PrintUnusedElementStats(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.print_unused_elements_stats"
|
||||
bl_label = "Purge Unused Elements Stats"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = (
|
||||
"Print all unused elements in current IFC project in system console, not limited to the selected class"
|
||||
)
|
||||
|
||||
ignore_contexts: bpy.props.BoolProperty(name="Ignore Contexts", default=True)
|
||||
ignore_relationships: bpy.props.BoolProperty(name="Ignore Relationships", default=True)
|
||||
ignore_types: bpy.props.BoolProperty(name="Ignore Types", default=True)
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMDebugProperties
|
||||
# ignore some classes that could have zero 0 inverse references by their nature
|
||||
ignore_classes = []
|
||||
if self.ignore_contexts:
|
||||
ignore_classes += ["IfcRepresentationContext"]
|
||||
if self.ignore_relationships:
|
||||
ignore_classes += ["IfcRelationship"]
|
||||
if self.ignore_types:
|
||||
ignore_classes += ["IfcTypeProduct"]
|
||||
|
||||
unused_elements = tool.Debug.print_unused_elements_stats(props.ifc_class_purge, ignore_classes)
|
||||
self.report({"INFO"}, f"{unused_elements} unused elements found, check the system console for the details.")
|
||||
|
||||
|
||||
class PurgeUnusedElementsByClass(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.purge_unused_elements_by_class"
|
||||
bl_label = "Purge Unused Elements By Class"
|
||||
bl_description = (
|
||||
"Will find all elements of class that have no inverse refernces and will remove them, use very carefully."
|
||||
)
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMDebugProperties
|
||||
purged_elements = core.purge_unused_elements(tool.Ifc, tool.Debug, props.ifc_class_purge)
|
||||
self.report({"INFO"}, f"{purged_elements} unused elements found and removed.")
|
||||
|
||||
@@ -40,3 +40,14 @@ class BIMDebugProperties(PropertyGroup):
|
||||
inverse_attributes: CollectionProperty(name="Inverse Attributes", type=Attribute)
|
||||
inverse_references: CollectionProperty(name="Inverse References", type=Attribute)
|
||||
express_file: StringProperty(name="Express File")
|
||||
display_type: EnumProperty(
|
||||
items=[
|
||||
("BOUNDS", "Bounds", ""),
|
||||
("WIRE", "Wire", ""),
|
||||
("SOLID", "Solid", ""),
|
||||
("TEXTURED", "Textured", ""),
|
||||
],
|
||||
name="Display Type",
|
||||
default="BOUNDS",
|
||||
)
|
||||
ifc_class_purge: StringProperty(name="Unused Elements IFC Class", default="")
|
||||
|
||||
@@ -83,6 +83,10 @@ class BIM_PT_debug(Panel):
|
||||
).percentile = context.scene.BIMDebugProperties.percentile_of_polygons
|
||||
row.prop(props, "percentile_of_polygons", text="")
|
||||
|
||||
row = layout.split(factor=0.5, align=True)
|
||||
row.prop(props, "display_type", text="")
|
||||
row.operator("bim.override_display_type").display = context.scene.BIMDebugProperties.display_type
|
||||
|
||||
if context.active_object and context.active_object.data:
|
||||
mprops = context.active_object.data.BIMMeshProperties
|
||||
row = layout.row()
|
||||
|
||||
@@ -22,6 +22,7 @@ import ifccsv
|
||||
import ifcopenshell
|
||||
import blenderbim.bim.handler
|
||||
import blenderbim.tool as tool
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
|
||||
|
||||
class SelectDiffJsonFile(bpy.types.Operator):
|
||||
@@ -51,8 +52,27 @@ class VisualiseDiff(bpy.types.Operator):
|
||||
diff = json.load(file)
|
||||
for obj in context.visible_objects:
|
||||
obj.color = (1.0, 1.0, 1.0, 1.0)
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
|
||||
if not obj.BIMObjectProperties.ifc_definition_id:
|
||||
continue
|
||||
|
||||
ifc_file = ""
|
||||
for scene in obj.users_scene:
|
||||
if scene.BIMProperties.ifc_file:
|
||||
ifc_file = scene.BIMProperties.ifc_file
|
||||
if scene.library:
|
||||
break
|
||||
|
||||
if ifc_file:
|
||||
if ifc_file not in IfcStore.session_files:
|
||||
IfcStore.session_files[ifc_file] = ifcopenshell.open(ifc_file)
|
||||
element_file = IfcStore.session_files[ifc_file]
|
||||
else:
|
||||
element_file = ifc_file
|
||||
|
||||
try:
|
||||
element = element_file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
except:
|
||||
continue
|
||||
global_id = getattr(element, "GlobalId", None)
|
||||
if not global_id:
|
||||
|
||||
@@ -29,7 +29,7 @@ class BIM_PT_documents(Panel):
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_project_setup"
|
||||
bl_parent_id = "BIM_PT_tab_project_setup"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
|
||||
@@ -92,10 +92,10 @@ classes = (
|
||||
prop.BIMTextProperties,
|
||||
prop.BIMAssignedProductProperties,
|
||||
prop.BIMAnnotationProperties,
|
||||
ui.BIM_PT_camera,
|
||||
ui.BIM_PT_drawing_underlay,
|
||||
ui.BIM_PT_sheets,
|
||||
ui.BIM_PT_drawings,
|
||||
ui.BIM_PT_camera,
|
||||
ui.BIM_PT_drawing_underlay,
|
||||
ui.BIM_PT_schedules,
|
||||
ui.BIM_PT_references,
|
||||
ui.BIM_PT_product_assignments,
|
||||
|
||||
@@ -123,6 +123,8 @@ class DrawingsData:
|
||||
[(str(s.id()), s.Name or "Unnamed", "") for s in tool.Ifc.get().by_type("IfcBuildingStorey")]
|
||||
)
|
||||
return results
|
||||
elif bpy.context.scene.DocProperties.target_view in ["MODEL_VIEW"]:
|
||||
return [(h.upper(), h, "") for h in ["Orthographic", "Perspective"]]
|
||||
return [(h.upper(), h, "") for h in ["North", "South", "East", "West"]]
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -52,15 +52,20 @@ class profile_consequential:
|
||||
cls.test_name = test_name
|
||||
|
||||
@classmethod
|
||||
def log(cls):
|
||||
def log(cls, args=[]):
|
||||
if cls.start_time is not None:
|
||||
cls.lines.append(f"{cls.test_name}\t{timer() - cls.start_time:.10f}")
|
||||
args = "" if not args else "\t" + "\t".join(args)
|
||||
cls.lines.append(f"{cls.test_name}\t{timer() - cls.start_time:.10f}{args}")
|
||||
|
||||
@classmethod
|
||||
def stop(cls):
|
||||
cls.log()
|
||||
cls.start_time = None
|
||||
print("\n".join(cls.lines))
|
||||
lines = "\n".join(cls.lines)
|
||||
print(lines)
|
||||
import pyperclip
|
||||
|
||||
pyperclip.copy(lines)
|
||||
cls.lines = []
|
||||
|
||||
|
||||
@@ -156,9 +161,9 @@ class BaseDecorator:
|
||||
os.path.join(bpy.context.scene.BIMProperties.data_dir, "fonts", "OpenGost Type B TT.ttf")
|
||||
)
|
||||
|
||||
# 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")
|
||||
# POLYLINE_UNIFORM_COLOR is good for smoothed lines since `bgl.enable(GL_LINE_SMOOTH)` is deprecated
|
||||
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
|
||||
self.base_shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
|
||||
def get_camera_width_mm(self):
|
||||
# Horrific prototype code to ensure bgl draws at drawing scales
|
||||
@@ -385,8 +390,6 @@ class BaseDecorator:
|
||||
# 0 is the default font, but we're fancier than that
|
||||
font_id = self.font_id
|
||||
|
||||
dpi = context.preferences.system.dpi
|
||||
|
||||
color = context.preferences.addons["blenderbim"].preferences.decorations_colour
|
||||
|
||||
ang = -Vector((1, 0)).angle_signed(text_dir)
|
||||
@@ -406,7 +409,7 @@ class BaseDecorator:
|
||||
font_size_px = int(0.004118616 * mm_to_px) * font_size_mm / 2.5
|
||||
pos = pos - line_no * font_size_px * rotation_matrix[1]
|
||||
|
||||
blf.size(font_id, font_size_px, dpi)
|
||||
blf.size(font_id, font_size_px)
|
||||
|
||||
if box_alignment or center or vcenter:
|
||||
w, h = blf.dimensions(font_id, text)
|
||||
@@ -1558,15 +1561,15 @@ class CutDecorator:
|
||||
gpu.state.point_size_set(1)
|
||||
gpu.state.blend_set("ALPHA")
|
||||
|
||||
# 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")
|
||||
# POLYLINE_UNIFORM_COLOR is good for smoothed lines since `bgl.enable(GL_LINE_SMOOTH)` is deprecated
|
||||
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
|
||||
self.line_shader.bind()
|
||||
# POLYLINE_UNIFORM_COLOR specific uniforms
|
||||
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
|
||||
self.line_shader.uniform_float("lineWidth", 2.0)
|
||||
|
||||
# general shader
|
||||
self.shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
|
||||
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
self.shader.bind()
|
||||
|
||||
black = (0, 0, 0, 1)
|
||||
@@ -1926,7 +1929,7 @@ class DecorationsHandler:
|
||||
if cls.installed:
|
||||
cls.uninstall()
|
||||
handler = cls()
|
||||
# NOTE: we USE POST_PIXEL here so that we can draw use both 3D_POLYLINE_UNIFORM_COLOR
|
||||
# NOTE: we USE POST_PIXEL here so that we can use both POLYLINE_UNIFORM_COLOR
|
||||
# and drawing text in the same handler. BUT this means that we supply coordinates in WINSPACE
|
||||
cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_PIXEL")
|
||||
|
||||
@@ -1944,6 +1947,7 @@ class DecorationsHandler:
|
||||
self.decorators[object_type] = self.decorators["FALL"]
|
||||
|
||||
def get_objects_and_decorators(self, collection):
|
||||
# TODO: do it in data instead of the handler for performance?
|
||||
results = []
|
||||
|
||||
for obj in collection.all_objects:
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
import bpy
|
||||
import blenderbim.bim.module.drawing.decoration as decoration
|
||||
import blenderbim.tool as tool
|
||||
from bpy.app.handlers import persistent
|
||||
|
||||
|
||||
@@ -37,10 +38,28 @@ def depsgraph_update_pre_handler(scene):
|
||||
def set_active_camera_resolution(scene):
|
||||
if not scene.camera or "/" not in scene.camera.name or not scene.DocProperties.drawings:
|
||||
return
|
||||
props = scene.camera.data.BIMCameraProperties
|
||||
ortho_scale = max((props.width, props.height))
|
||||
aspect_ratio = props.width / props.height
|
||||
if (
|
||||
scene.render.resolution_x != scene.camera.data.BIMCameraProperties.raster_x
|
||||
or scene.render.resolution_y != scene.camera.data.BIMCameraProperties.raster_y
|
||||
(scene.camera.data.ortho_scale != ortho_scale)
|
||||
or (scene.render.resolution_x / scene.render.resolution_y != aspect_ratio)
|
||||
):
|
||||
scene.render.resolution_x = scene.camera.data.BIMCameraProperties.raster_x
|
||||
scene.render.resolution_y = scene.camera.data.BIMCameraProperties.raster_y
|
||||
scene.camera.data.ortho_scale = ortho_scale
|
||||
|
||||
diagram_scale = tool.Drawing.get_diagram_scale(scene.camera)
|
||||
scale_ratio = tool.Drawing.get_scale_ratio(diagram_scale["Scale"])
|
||||
|
||||
if props.width > props.height:
|
||||
aspect_ratio = props.height / props.width
|
||||
raster_x = ortho_scale * scale_ratio * props.dpi / 0.0254
|
||||
raster_y = ortho_scale * aspect_ratio * scale_ratio * props.dpi / 0.0254
|
||||
else:
|
||||
aspect_ratio = props.width / props.height
|
||||
raster_x = ortho_scale * aspect_ratio * scale_ratio * props.dpi / 0.0254
|
||||
raster_y = ortho_scale * scale_ratio * props.dpi / 0.0254
|
||||
|
||||
scene.render.resolution_x = scene.camera.data.BIMCameraProperties.raster_x = int(raster_x)
|
||||
scene.render.resolution_y = scene.camera.data.BIMCameraProperties.raster_y = int(raster_y)
|
||||
|
||||
current_drawing = scene.DocProperties.drawings[scene.DocProperties.current_drawing_index]
|
||||
|
||||
@@ -182,8 +182,12 @@ def format_distance(
|
||||
|
||||
# Check values and compose string
|
||||
if inches == 12:
|
||||
feet += 1
|
||||
inches = 0
|
||||
if unit_length == "INCHES":
|
||||
inches = 12
|
||||
if unit_length == "FEET":
|
||||
feet += 1
|
||||
inches = 0
|
||||
|
||||
|
||||
if not isArea:
|
||||
add_inches = bool(inches) or not suppress_zero_inches
|
||||
|
||||
@@ -26,7 +26,6 @@ import shutil
|
||||
import hashlib
|
||||
import shapely
|
||||
import subprocess
|
||||
import webbrowser
|
||||
import numpy as np
|
||||
import multiprocessing
|
||||
import ifcopenshell
|
||||
@@ -260,7 +259,9 @@ class CreateDrawing(bpy.types.Operator):
|
||||
with profile("Combine SVG layers"):
|
||||
svg_path = self.combine_svgs(context, underlay_svg, linework_svg, annotation_svg)
|
||||
|
||||
tool.Drawing.open_with_user_command(context.preferences.addons["blenderbim"].preferences.svg_command, svg_path)
|
||||
tool.Drawing.open_with_user_command(
|
||||
context.preferences.addons["blenderbim"].preferences.svg_command, svg_path
|
||||
)
|
||||
|
||||
if self.print_all:
|
||||
bpy.ops.bim.activate_drawing(drawing=original_drawing_id, camera_view_point=False)
|
||||
@@ -535,7 +536,7 @@ class CreateDrawing(bpy.types.Operator):
|
||||
root = etree.fromstring(results)
|
||||
|
||||
group = root.find("{http://www.w3.org/2000/svg}g")
|
||||
if not group:
|
||||
if group is None:
|
||||
with open(svg_path, "wb") as svg:
|
||||
svg.write(etree.tostring(root))
|
||||
|
||||
@@ -919,7 +920,7 @@ class CreateDrawing(bpy.types.Operator):
|
||||
join_criteria = join_criteria.split(",")
|
||||
else:
|
||||
# Drawing convention states that same objects classes with the same material are merged when cut.
|
||||
join_criteria = ["class", "material.Name", 'r"Pset.*Common"."Status"']
|
||||
join_criteria = ["class", "material.Name", "/Pset_.*Common/.Status", "EPset_Status.Status"]
|
||||
|
||||
group = root.find("{http://www.w3.org/2000/svg}g")
|
||||
joined_paths = {}
|
||||
|
||||
@@ -312,7 +312,6 @@ class DocProperties(PropertyGroup):
|
||||
should_use_underlay_cache: BoolProperty(name="Use Underlay Cache", default=False)
|
||||
should_use_linework_cache: BoolProperty(name="Use Linework Cache", default=False)
|
||||
should_use_annotation_cache: BoolProperty(name="Use Annotation Cache", default=False)
|
||||
should_extract: BoolProperty(name="Should Extract", default=True)
|
||||
is_editing_drawings: BoolProperty(name="Is Editing Drawings", default=False)
|
||||
is_editing_schedules: BoolProperty(name="Is Editing Schedules", default=False)
|
||||
is_editing_references: BoolProperty(name="Is Editing References", default=False)
|
||||
@@ -375,6 +374,9 @@ class BIMCameraProperties(PropertyGroup):
|
||||
custom_scale_denominator: bpy.props.StringProperty(default="100", update=update_diagram_scale)
|
||||
raster_x: IntProperty(name="Raster X", default=1000)
|
||||
raster_y: IntProperty(name="Raster Y", default=1000)
|
||||
dpi: IntProperty(name="DPI", default=75)
|
||||
width: FloatProperty(name="Width", default=50, subtype="DISTANCE")
|
||||
height: FloatProperty(name="Height", default=50, subtype="DISTANCE")
|
||||
is_nts: BoolProperty(name="Is NTS", update=update_is_nts)
|
||||
active_drawing_style_index: IntProperty(name="Active Drawing Style Index")
|
||||
|
||||
|
||||
@@ -402,12 +402,27 @@ class Scheduler:
|
||||
else:
|
||||
wrapped_lines = text_lines
|
||||
|
||||
for line_number, text_line in enumerate(wrapped_lines[::-1]):
|
||||
if box_alignment.startswith("top"):
|
||||
dy_dir = 1
|
||||
line_number = 0
|
||||
elif box_alignment.startswith("bot"):
|
||||
dy_dir = -1
|
||||
line_number = 0
|
||||
else: # middle row
|
||||
# the idea is that the middle row should always stay in the center
|
||||
# e.g. dy offset for 3 lines is: 1, 0, -1
|
||||
# for 2 lines: 0.5, -0.5
|
||||
dy_dir = -1
|
||||
line_number = (len(wrapped_lines) - 1) / 2
|
||||
|
||||
for text_line in wrapped_lines[::dy_dir]:
|
||||
# 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"})
|
||||
tspan.update({"dy": f"{line_number}em"})
|
||||
text_tag.add(tspan)
|
||||
line_number += dy_dir
|
||||
|
||||
self.svg.add(text_tag)
|
||||
|
||||
def convert_to_mm(self, value):
|
||||
|
||||
@@ -288,9 +288,9 @@ class BaseShader:
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
# 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")
|
||||
# POLYLINE_UNIFORM_COLOR is good for smoothed lines since `bgl.enable(GL_LINE_SMOOTH)` is deprecated
|
||||
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
|
||||
self.base_shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
|
||||
def get_shader(self):
|
||||
"""Returns shader for this type"""
|
||||
|
||||
@@ -160,7 +160,8 @@ class SheetBuilder:
|
||||
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["x"] = str(x + readjust.x)
|
||||
# negate y offset because view-title comes AFTER foreground
|
||||
image.attrib["y"] = str(y - readjust.y)
|
||||
else:
|
||||
image.attrib["x"] = str(x + readjust.x)
|
||||
@@ -345,8 +346,12 @@ class SheetBuilder:
|
||||
def build_drawings(self, root, sheet):
|
||||
for view in root.findall('{http://www.w3.org/2000/svg}g[@data-type="drawing"]'):
|
||||
drawing_id = int(view.attrib["data-id"])
|
||||
reference = tool.Ifc.get().by_id(int(view.attrib["data-id"]))
|
||||
drawing = tool.Ifc.get().by_id(view.attrib["data-drawing"])
|
||||
try:
|
||||
reference = tool.Ifc.get().by_id(int(view.attrib["data-id"]))
|
||||
drawing = tool.Ifc.get().by_id(view.attrib["data-drawing"])
|
||||
except:
|
||||
# Perhaps the SVG has outdated content or is edited externally which we cannot control.
|
||||
continue
|
||||
|
||||
images = view.findall("{http://www.w3.org/2000/svg}image")
|
||||
|
||||
@@ -387,8 +392,12 @@ class SheetBuilder:
|
||||
|
||||
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"]))
|
||||
try:
|
||||
reference = tool.Ifc.get().by_id(int(view.attrib["data-id"]))
|
||||
schedule = tool.Ifc.get().by_id(int(view.attrib["data-schedule"]))
|
||||
except:
|
||||
# Perhaps the SVG has outdated content or is edited externally which we cannot control.
|
||||
continue
|
||||
|
||||
images = view.findall("{http://www.w3.org/2000/svg}image")
|
||||
|
||||
|
||||
@@ -160,7 +160,7 @@ class SvgWriter:
|
||||
def find_xml_symbol_by_id(self, id):
|
||||
tree = ET.parse(self.resource_paths["Symbols"])
|
||||
xml_symbol = tree.find(f'.//*[@id="{id}"]')
|
||||
return External(xml_symbol) if xml_symbol else None
|
||||
return External(xml_symbol) if xml_symbol is not None else None
|
||||
|
||||
def add_patterns(self):
|
||||
path = self.resource_paths["Patterns"]
|
||||
|
||||
@@ -31,28 +31,28 @@ from blenderbim.bim.module.drawing.prop import ANNOTATION_TYPES_DATA
|
||||
|
||||
|
||||
class BIM_PT_camera(Panel):
|
||||
bl_label = "Drawing Generation"
|
||||
bl_label = "Active Drawing"
|
||||
bl_idname = "BIM_PT_camera"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "data"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.camera and hasattr(context.active_object.data, "BIMCameraProperties")
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_tab_drawings"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
if "/" not in context.active_object.name:
|
||||
layout.label(text="This is not a BIM camera.")
|
||||
if not (context.scene.camera and hasattr(context.scene.camera.data, "BIMCameraProperties")):
|
||||
row = self.layout.row()
|
||||
row.label(text="No Active Drawing", icon="ERROR")
|
||||
return
|
||||
|
||||
layout.use_property_split = True
|
||||
dprops = context.scene.DocProperties
|
||||
props = context.active_object.data.BIMCameraProperties
|
||||
if "/" not in context.scene.camera.name:
|
||||
self.layout.label(text="This is not a BIM camera.")
|
||||
return
|
||||
|
||||
col = layout.column(align=True)
|
||||
self.layout.use_property_split = True
|
||||
dprops = context.scene.DocProperties
|
||||
props = context.scene.camera.data.BIMCameraProperties
|
||||
|
||||
col = self.layout.column(align=True)
|
||||
row = col.row(align=True)
|
||||
row.prop(props, "has_underlay", icon="OUTLINER_OB_IMAGE")
|
||||
row.prop(dprops, "should_use_underlay_cache", text="", icon="FILE_REFRESH")
|
||||
@@ -63,32 +63,36 @@ 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 = self.layout.row()
|
||||
row.prop(props, "calculate_shapely_surfaces")
|
||||
row = layout.row()
|
||||
row = self.layout.row()
|
||||
row.prop(props, "calculate_svgfill_surfaces")
|
||||
|
||||
row = layout.row()
|
||||
row.prop(dprops, "should_extract")
|
||||
row = self.layout.row()
|
||||
row.prop(props, "width")
|
||||
row = self.layout.row()
|
||||
row.prop(props, "height")
|
||||
|
||||
row = layout.row()
|
||||
row.prop(props, "raster_x")
|
||||
row = layout.row()
|
||||
row.prop(props, "raster_y")
|
||||
row = self.layout.row()
|
||||
row.prop(context.scene.camera.data, "clip_end", text="Depth")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(props, "diagram_scale")
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "diagram_scale", text="Scale")
|
||||
row.prop(props, "is_nts", text="", icon="MOD_EDGESPLIT")
|
||||
|
||||
if props.diagram_scale == "CUSTOM":
|
||||
row = layout.row(align=True)
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "custom_scale_numerator", text="Custom Scale")
|
||||
row.prop(props, "custom_scale_denominator", text="")
|
||||
|
||||
row = layout.row(align=True)
|
||||
if props.has_underlay:
|
||||
row = self.layout.row()
|
||||
row.prop(props, "dpi")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.create_drawing", text="Create Drawing", icon="OUTPUT")
|
||||
op = row.operator("bim.open_drawing", icon="URL", text="")
|
||||
op.view = context.active_object.name.split("/")[1]
|
||||
op.view = context.scene.camera.name.split("/")[1]
|
||||
|
||||
|
||||
class BIM_PT_drawing_underlay(Panel):
|
||||
@@ -97,12 +101,12 @@ class BIM_PT_drawing_underlay(Panel):
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "data"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_camera"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.camera and hasattr(context.active_object.data, "BIMCameraProperties")
|
||||
return context.scene.camera and hasattr(context.active_object.data, "BIMCameraProperties")
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
@@ -164,10 +168,8 @@ class BIM_PT_drawings(Panel):
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return tool.Blender.is_tab(context, "DRAWINGS") and tool.Ifc.get()
|
||||
bl_parent_id = "BIM_PT_tab_drawings"
|
||||
bl_options = {"HIDE_HEADER"}
|
||||
|
||||
def draw(self, context):
|
||||
if not DrawingsData.is_loaded:
|
||||
@@ -245,10 +247,8 @@ class BIM_PT_schedules(Panel):
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return tool.Blender.is_tab(context, "DRAWINGS") and tool.Ifc.get()
|
||||
bl_parent_id = "BIM_PT_tab_schedules"
|
||||
bl_options = {"HIDE_HEADER"}
|
||||
|
||||
def draw(self, context):
|
||||
if not DocumentsData.is_loaded:
|
||||
@@ -297,10 +297,8 @@ class BIM_PT_references(Panel):
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return tool.Blender.is_tab(context, "DRAWINGS") and tool.Ifc.get()
|
||||
bl_parent_id = "BIM_PT_tab_references"
|
||||
bl_options = {"HIDE_HEADER"}
|
||||
|
||||
def draw(self, context):
|
||||
if not DocumentsData.is_loaded:
|
||||
@@ -341,10 +339,8 @@ class BIM_PT_sheets(Panel):
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return tool.Blender.is_tab(context, "DRAWINGS") and tool.Ifc.get()
|
||||
bl_parent_id = "BIM_PT_tab_sheets"
|
||||
bl_options = {"HIDE_HEADER"}
|
||||
|
||||
def draw(self, context):
|
||||
if not SheetsData.is_loaded:
|
||||
|
||||
+9
-8
@@ -1,5 +1,5 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
|
||||
# Copyright (C) 2023 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
@@ -20,17 +20,18 @@ import bpy
|
||||
from . import ui, prop, operator
|
||||
|
||||
classes = (
|
||||
operator.SelectCobieIfcFile,
|
||||
operator.SelectCobieJsonFile,
|
||||
operator.ExecuteIfcCobie,
|
||||
prop.COBieProperties,
|
||||
ui.BIM_PT_cobie,
|
||||
operator.ExecuteIfcFM,
|
||||
operator.ExecuteIfcFMFederate,
|
||||
operator.SelectFMIfcFile,
|
||||
operator.SelectFMSpreadsheetFiles,
|
||||
prop.BIMFMProperties,
|
||||
ui.BIM_PT_fm,
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
bpy.types.Scene.COBieProperties = bpy.props.PointerProperty(type=prop.COBieProperties)
|
||||
bpy.types.Scene.BIMFMProperties = bpy.props.PointerProperty(type=prop.BIMFMProperties)
|
||||
|
||||
|
||||
def unregister():
|
||||
del bpy.types.Scene.COBieProperties
|
||||
del bpy.types.Scene.BIMFMProperties
|
||||
@@ -0,0 +1,157 @@
|
||||
# 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 os
|
||||
import bpy
|
||||
import json
|
||||
import ifcfm
|
||||
import logging
|
||||
import tempfile
|
||||
import ifcopenshell
|
||||
import blenderbim.tool as tool
|
||||
|
||||
|
||||
class SelectFMIfcFile(bpy.types.Operator):
|
||||
bl_idname = "bim.select_fm_ifc_file"
|
||||
bl_label = "Select FM IFC File"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filename_ext = ".ifc"
|
||||
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"})
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
files: bpy.props.CollectionProperty(name="File Path", type=bpy.types.OperatorFileListElement)
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.BIMFMProperties
|
||||
props.ifc_files.clear()
|
||||
dirname = os.path.dirname(self.filepath)
|
||||
for f in self.files:
|
||||
new = props.ifc_files.add()
|
||||
new.name = os.path.join(dirname, f.name)
|
||||
props.ifc_file = self.filepath
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class ExecuteIfcFM(bpy.types.Operator):
|
||||
bl_idname = "bim.execute_ifcfm"
|
||||
bl_label = "Execute IfcFM"
|
||||
file_format: bpy.props.StringProperty()
|
||||
filter_glob: bpy.props.StringProperty(default="*.csv;*.ods;*.xlsx", options={"HIDDEN"})
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
props = context.scene.BIMFMProperties
|
||||
return props.should_load_from_memory or props.ifc_file
|
||||
|
||||
def invoke(self, context, event):
|
||||
props = context.scene.BIMFMProperties
|
||||
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, f".{props.format}")
|
||||
WindowManager = context.window_manager
|
||||
WindowManager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.BIMFMProperties
|
||||
ifc_file = tool.Ifc.get()
|
||||
filepaths = []
|
||||
if not (ifc_file and props.should_load_from_memory):
|
||||
if len(props.ifc_files):
|
||||
ifc_files = [ifcopenshell.open(f.name) for f in props.ifc_files]
|
||||
filepaths = [f.name for f in props.ifc_files]
|
||||
else:
|
||||
ifc_files = [ifcopenshell.open(props.ifc_file)]
|
||||
else:
|
||||
ifc_files = [ifc_file]
|
||||
|
||||
for i, ifc_file in enumerate(ifc_files):
|
||||
if filepaths:
|
||||
dirname = os.path.dirname(self.filepath)
|
||||
prefix, _ = os.path.splitext(os.path.basename(filepaths[i]))
|
||||
basename = os.path.basename(self.filepath)
|
||||
filepath = os.path.join(dirname, f"{prefix}-{basename}")
|
||||
else:
|
||||
filepath = self.filepath
|
||||
parser = ifcfm.Parser(preset=props.engine)
|
||||
parser.parse(ifc_file)
|
||||
writer = ifcfm.Writer(parser)
|
||||
writer.write()
|
||||
if props.format == "csv":
|
||||
writer.write_csv("tmp/")
|
||||
elif props.format == "ods":
|
||||
writer.write_ods(filepath)
|
||||
elif props.format == "xlsx":
|
||||
writer.write_xlsx(filepath)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SelectFMSpreadsheetFiles(bpy.types.Operator):
|
||||
bl_idname = "bim.select_fm_spreadsheet_files"
|
||||
bl_label = "Select FM Spreadsheet Files"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filter_glob: bpy.props.StringProperty(default="*.ods;*.xlsx", options={"HIDDEN"})
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
files: bpy.props.CollectionProperty(name="File Path", type=bpy.types.OperatorFileListElement)
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.BIMFMProperties
|
||||
props.spreadsheet_files.clear()
|
||||
dirname = os.path.dirname(self.filepath)
|
||||
for f in self.files:
|
||||
new = props.spreadsheet_files.add()
|
||||
new.name = os.path.join(dirname, f.name)
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class ExecuteIfcFMFederate(bpy.types.Operator):
|
||||
bl_idname = "bim.execute_ifcfm_federate"
|
||||
bl_label = "Execute IfcFM"
|
||||
file_format: bpy.props.StringProperty()
|
||||
filter_glob: bpy.props.StringProperty(default="*.ods;*.xlsx", options={"HIDDEN"})
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
props = context.scene.BIMFMProperties
|
||||
return props.spreadsheet_files
|
||||
|
||||
def invoke(self, context, event):
|
||||
props = context.scene.BIMFMProperties
|
||||
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, f".{props.format}")
|
||||
WindowManager = context.window_manager
|
||||
WindowManager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.BIMFMProperties
|
||||
parser = ifcfm.Parser(preset=props.engine)
|
||||
parser.federate([f.name for f in props.spreadsheet_files])
|
||||
writer = ifcfm.Writer(parser)
|
||||
writer.write()
|
||||
if props.format == "ods":
|
||||
writer.write_ods(self.filepath)
|
||||
elif props.format == "xlsx":
|
||||
writer.write_xlsx(self.filepath)
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,57 @@
|
||||
# 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 bpy
|
||||
from blenderbim.bim.prop import StrProperty
|
||||
from bpy.types import PropertyGroup
|
||||
from bpy.props import (
|
||||
PointerProperty,
|
||||
StringProperty,
|
||||
EnumProperty,
|
||||
BoolProperty,
|
||||
IntProperty,
|
||||
FloatProperty,
|
||||
FloatVectorProperty,
|
||||
CollectionProperty,
|
||||
)
|
||||
|
||||
|
||||
class BIMFMProperties(PropertyGroup):
|
||||
ifc_file: StringProperty(default="", name="IFC File")
|
||||
ifc_files: CollectionProperty(name="IFC Files", type=StrProperty)
|
||||
spreadsheet_files: CollectionProperty(name="Spreadsheets", type=StrProperty)
|
||||
should_load_from_memory: BoolProperty(default=False, name="Load from Memory")
|
||||
engine: EnumProperty(
|
||||
items=[
|
||||
("aohbsem", "AOH-BSEM", ""),
|
||||
("basic", "Basic", ""),
|
||||
("cobie24", "COBie 2.4", ""),
|
||||
("cobie3", "COBie 3", ""),
|
||||
],
|
||||
name="Engine",
|
||||
default="cobie24",
|
||||
)
|
||||
format: EnumProperty(
|
||||
items=[
|
||||
("csv", "csv", ""),
|
||||
("xlsx", "xlsx", ""),
|
||||
("ods", "ods", ""),
|
||||
],
|
||||
name="Format",
|
||||
default="ods",
|
||||
)
|
||||
+18
-17
@@ -1,5 +1,5 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
|
||||
# Copyright (C) 2023 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
@@ -21,9 +21,9 @@ from bpy.types import Panel
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
|
||||
|
||||
class BIM_PT_cobie(Panel):
|
||||
bl_label = "COBie"
|
||||
bl_idname = "BIM_PT_cobie"
|
||||
class BIM_PT_fm(Panel):
|
||||
bl_label = "Facility Management"
|
||||
bl_idname = "BIM_PT_fm"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
@@ -34,7 +34,7 @@ class BIM_PT_cobie(Panel):
|
||||
layout.use_property_split = True
|
||||
|
||||
scene = context.scene
|
||||
props = scene.COBieProperties
|
||||
props = scene.BIMFMProperties
|
||||
|
||||
if IfcStore.get_file():
|
||||
row = layout.row()
|
||||
@@ -42,22 +42,23 @@ class BIM_PT_cobie(Panel):
|
||||
|
||||
if not IfcStore.get_file() or not props.should_load_from_memory:
|
||||
row = layout.row(align=True)
|
||||
row.prop(props, "cobie_ifc_file")
|
||||
row.operator("bim.select_cobie_ifc_file", icon="FILE_FOLDER", text="")
|
||||
if len(props.ifc_files) > 1:
|
||||
row.label(text=f"{len(props.ifc_files)} Files Selected")
|
||||
else:
|
||||
row.prop(props, "ifc_file")
|
||||
row.operator("bim.select_fm_ifc_file", icon="FILE_FOLDER", text="")
|
||||
|
||||
row = layout.row()
|
||||
row.prop(props, "cobie_types")
|
||||
row.prop(props, "engine")
|
||||
row = layout.row()
|
||||
row.prop(props, "cobie_components")
|
||||
row.prop(props, "format")
|
||||
|
||||
row = layout.row()
|
||||
op = row.operator("bim.execute_ifcfm", text="Convert To Spreadsheet")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(props, "cobie_json_file")
|
||||
row.operator("bim.select_cobie_json_file", icon="FILE_FOLDER", text="")
|
||||
row.label(text=f"{len(props.spreadsheet_files)} Spreadsheets Selected")
|
||||
row.operator("bim.select_fm_spreadsheet_files", icon="FILE_FOLDER", text="")
|
||||
|
||||
row = layout.row()
|
||||
op = row.operator("bim.execute_ifc_cobie", text="CSV")
|
||||
op.file_format = "csv"
|
||||
op = row.operator("bim.execute_ifc_cobie", text="ODS")
|
||||
op.file_format = "ods"
|
||||
op = row.operator("bim.execute_ifc_cobie", text="XLSX")
|
||||
op.file_format = "xlsx"
|
||||
op = row.operator("bim.execute_ifcfm_federate", text="Merge Spreadsheets")
|
||||
@@ -22,7 +22,10 @@ from . import ui, prop, operator
|
||||
classes = (
|
||||
operator.AddRepresentation,
|
||||
operator.CopyRepresentation,
|
||||
operator.DisableEditingRepresentationItems,
|
||||
operator.EditObjectPlacement,
|
||||
operator.EnableEditingRepresentationItems,
|
||||
operator.FlipObject,
|
||||
operator.GetRepresentationIfcParameters,
|
||||
operator.OverrideDelete,
|
||||
operator.OverrideDuplicateMove,
|
||||
@@ -32,6 +35,7 @@ classes = (
|
||||
operator.OverrideDuplicateMoveLinkedMacro,
|
||||
operator.OverrideDuplicateMoveMacro,
|
||||
operator.OverrideJoin,
|
||||
operator.OverrideMeshSeparate,
|
||||
operator.OverrideModeSetEdit,
|
||||
operator.OverrideModeSetObject,
|
||||
operator.OverrideOriginSet,
|
||||
@@ -45,14 +49,20 @@ classes = (
|
||||
operator.SwitchRepresentation,
|
||||
operator.UpdateParametricRepresentation,
|
||||
operator.UpdateRepresentation,
|
||||
prop.RepresentationItem,
|
||||
prop.BIMObjectGeometryProperties,
|
||||
prop.BIMGeometryProperties,
|
||||
ui.BIM_PT_derived_placements,
|
||||
ui.BIM_PT_placement,
|
||||
ui.BIM_PT_representations,
|
||||
ui.BIM_PT_representation_items,
|
||||
ui.BIM_PT_connections,
|
||||
ui.BIM_PT_mesh,
|
||||
ui.BIM_PT_derived_coordinates,
|
||||
ui.BIM_PT_workarounds,
|
||||
ui.BIM_MT_object_set_origin,
|
||||
ui.BIM_MT_separate,
|
||||
ui.BIM_MT_hotkey_separate,
|
||||
ui.BIM_UL_representation_items,
|
||||
)
|
||||
|
||||
|
||||
@@ -69,10 +79,10 @@ def register():
|
||||
|
||||
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)
|
||||
bpy.types.VIEW3D_MT_object.append(ui.object_menu)
|
||||
bpy.types.OUTLINER_MT_object.append(ui.outliner_menu)
|
||||
bpy.types.VIEW3D_MT_object_context_menu.append(ui.object_menu)
|
||||
bpy.types.VIEW3D_MT_edit_mesh.append(ui.edit_mesh_menu)
|
||||
wm = bpy.context.window_manager
|
||||
if wm.keyconfigs.addon:
|
||||
km = wm.keyconfigs.addon.keymaps.new(name="Object Mode", space_type="EMPTY")
|
||||
@@ -82,7 +92,9 @@ def register():
|
||||
addon_keymaps.append((km, kmi))
|
||||
kmi = km.keymap_items.new("bim.override_object_duplicate_move_linked_macro", "D", "PRESS", alt=True)
|
||||
addon_keymaps.append((km, kmi))
|
||||
kmi = km.keymap_items.new("bim.override_object_duplicate_move_aggregate_macro", "D", "PRESS", ctrl=True, shift=True)
|
||||
kmi = km.keymap_items.new(
|
||||
"bim.override_object_duplicate_move_aggregate_macro", "D", "PRESS", ctrl=True, shift=True
|
||||
)
|
||||
addon_keymaps.append((km, kmi))
|
||||
kmi = km.keymap_items.new("bim.override_paste_buffer", "V", "PRESS", ctrl=True)
|
||||
addon_keymaps.append((km, kmi))
|
||||
@@ -97,6 +109,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")
|
||||
addon_keymaps.append((km, kmi))
|
||||
kmi = km.keymap_items.new("wm.call_menu", "P", "PRESS")
|
||||
kmi.properties.name = ui.BIM_MT_hotkey_separate.bl_idname
|
||||
addon_keymaps.append((km, kmi))
|
||||
|
||||
km = wm.keyconfigs.addon.keymaps.new(name="Curve", space_type="EMPTY")
|
||||
kmi = km.keymap_items.new("bim.override_mode_set_object", "TAB", "PRESS")
|
||||
@@ -113,9 +128,9 @@ def register():
|
||||
|
||||
def unregister():
|
||||
bpy.types.VIEW3D_MT_object.remove(ui.object_menu)
|
||||
bpy.types.OBJECT_PT_transform.remove(ui.BIM_PT_transform)
|
||||
bpy.types.OUTLINER_MT_object.remove(ui.outliner_menu)
|
||||
bpy.types.VIEW3D_MT_object_context_menu.remove(ui.outliner_menu)
|
||||
bpy.types.VIEW3D_MT_edit_mesh.remove(ui.edit_mesh_menu)
|
||||
del bpy.types.Scene.BIMGeometryProperties
|
||||
del bpy.types.Object.BIMGeometryProperties
|
||||
wm = bpy.context.window_manager
|
||||
|
||||
@@ -23,8 +23,10 @@ from mathutils import Vector
|
||||
|
||||
|
||||
def refresh():
|
||||
DerivedPlacementsData.is_loaded = False
|
||||
PlacementData.is_loaded = False
|
||||
DerivedCoordinatesData.is_loaded = False
|
||||
RepresentationsData.is_loaded = False
|
||||
RepresentationItemsData.is_loaded = False
|
||||
ConnectionsData.is_loaded = False
|
||||
|
||||
|
||||
@@ -91,6 +93,50 @@ class RepresentationsData:
|
||||
return results
|
||||
|
||||
|
||||
class RepresentationItemsData:
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.data = {
|
||||
"total_items": cls.total_items(),
|
||||
"active_surface_style": cls.active_surface_style(),
|
||||
"active_layer": cls.active_layer(),
|
||||
}
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def total_items(cls):
|
||||
active_representation_id = None
|
||||
result = 0
|
||||
if bpy.context.active_object.data and hasattr(bpy.context.active_object.data, "BIMMeshProperties"):
|
||||
active_representation_id = bpy.context.active_object.data.BIMMeshProperties.ifc_definition_id
|
||||
element = tool.Ifc.get().by_id(active_representation_id)
|
||||
if not element.is_a("IfcShapeRepresentation"):
|
||||
return 0
|
||||
queue = list(element.Items)
|
||||
while queue:
|
||||
item = queue.pop()
|
||||
if item.is_a("IfcMappedItem"):
|
||||
queue.extend(item.MappingSource.MappedRepresentation.Items)
|
||||
else:
|
||||
result += 1
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def active_surface_style(cls):
|
||||
props = bpy.context.active_object.BIMGeometryProperties
|
||||
if props.active_item_index < len(props.items):
|
||||
return props.items[props.active_item_index].surface_style
|
||||
|
||||
@classmethod
|
||||
def active_layer(cls):
|
||||
props = bpy.context.active_object.BIMGeometryProperties
|
||||
if props.active_item_index < len(props.items):
|
||||
return props.items[props.active_item_index].layer
|
||||
|
||||
|
||||
class ConnectionsData:
|
||||
data = {}
|
||||
is_loaded = False
|
||||
@@ -184,7 +230,7 @@ class ConnectionsData:
|
||||
return results
|
||||
|
||||
|
||||
class DerivedPlacementsData:
|
||||
class DerivedCoordinatesData:
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@@ -263,6 +309,25 @@ class DerivedPlacementsData:
|
||||
for i, storey in enumerate(storeys):
|
||||
if storey[0] != element:
|
||||
continue
|
||||
if i >= len(storeys):
|
||||
if i >= len(storeys) - 1:
|
||||
return "N/A"
|
||||
return "{0:.3f}".format(round(storeys[i + 1][1] - storey[1], 3))
|
||||
|
||||
|
||||
class PlacementData:
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.data = {
|
||||
"has_placement": cls.has_placement(),
|
||||
}
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def has_placement(cls):
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
if element and hasattr(element, "ObjectPlacement"):
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -34,6 +34,7 @@ import blenderbim.core.drawing
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.bim.handler
|
||||
from mathutils import Vector, Matrix
|
||||
from time import time
|
||||
from blenderbim.bim import import_ifc
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
|
||||
@@ -57,6 +58,42 @@ class EditObjectPlacement(bpy.types.Operator, Operator):
|
||||
core.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
||||
|
||||
|
||||
class OverrideMeshSeparate(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.override_mesh_separate"
|
||||
bl_label = "IFC Mesh Separate"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
type: bpy.props.StringProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
obj = context.active_object
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
return
|
||||
|
||||
# You cannot separate meshes if the representation is mapped.
|
||||
relating_type = tool.Root.get_element_type(element)
|
||||
if relating_type and tool.Root.does_type_have_representations(relating_type):
|
||||
# We toggle edit mode to ensure that once representations are
|
||||
# unmapped, our Blender mesh only has a single user.
|
||||
tool.Blender.toggle_edit_mode(context)
|
||||
bpy.ops.bim.unassign_type(related_object=obj.name)
|
||||
tool.Blender.toggle_edit_mode(context)
|
||||
|
||||
selected_objects = context.selected_objects
|
||||
bpy.ops.mesh.separate(type=self.type)
|
||||
bpy.ops.object.mode_set(mode="OBJECT", toggle=False)
|
||||
new_objs = [obj]
|
||||
for new_obj in context.selected_objects:
|
||||
if new_obj == obj:
|
||||
continue
|
||||
# This is not very efficient, it needlessly copies the representations first.
|
||||
blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj)
|
||||
new_objs.append(new_obj)
|
||||
for new_obj in new_objs:
|
||||
bpy.ops.bim.update_representation(obj=new_obj.name)
|
||||
|
||||
|
||||
class OverrideOriginSet(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.override_origin_set"
|
||||
bl_label = "IFC Origin Set"
|
||||
@@ -88,8 +125,15 @@ class AddRepresentation(bpy.types.Operator, Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
representation_conversion_method: bpy.props.EnumProperty(
|
||||
items=[
|
||||
("OUTLINE", "Trace Outline", ""),
|
||||
("BOX", "Bounding Box", ""),
|
||||
("OUTLINE", "Trace Outline", "Traces outline by local XY axes, for Profile - by local XZ axes."),
|
||||
(
|
||||
"BOX",
|
||||
"Bounding Box",
|
||||
"Creates a bounding box representation.\n"
|
||||
"For Plan context - 2D bounding box by local XY axes,\n"
|
||||
"for Profile - 2D bounding box by local XZ axes.\n"
|
||||
"For other contexts - bounding box is 3d.",
|
||||
),
|
||||
("PROJECT", "Full Representation", ""),
|
||||
],
|
||||
name="Representation Conversion Method",
|
||||
@@ -170,18 +214,22 @@ class SwitchRepresentation(bpy.types.Operator, Operator):
|
||||
should_switch_all_meshes: bpy.props.BoolProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
target = tool.Ifc.get().by_id(self.ifc_definition_id).ContextOfItems
|
||||
target_representation = tool.Ifc.get().by_id(self.ifc_definition_id)
|
||||
target = target_representation.ContextOfItems
|
||||
is_subcontext = target.is_a("IfcGeometricRepresentationSubContext")
|
||||
for obj in set(context.selected_objects + [context.active_object]):
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
if is_subcontext:
|
||||
representation = ifcopenshell.util.representation.get_representation(
|
||||
element, target.ContextType, target.ContextIdentifier, target.TargetView
|
||||
)
|
||||
if obj == context.active_object:
|
||||
representation = target_representation
|
||||
else:
|
||||
representation = ifcopenshell.util.representation.get_representation(element, target.ContextType)
|
||||
if is_subcontext:
|
||||
representation = ifcopenshell.util.representation.get_representation(
|
||||
element, target.ContextType, target.ContextIdentifier, target.TargetView
|
||||
)
|
||||
else:
|
||||
representation = ifcopenshell.util.representation.get_representation(element, target.ContextType)
|
||||
if not representation:
|
||||
continue
|
||||
core.switch_representation(
|
||||
@@ -216,7 +264,8 @@ class PurgeUnusedRepresentations(bpy.types.Operator, Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
core.purge_unused_representations(tool.Ifc, tool.Geometry)
|
||||
purged_representations = core.purge_unused_representations(tool.Ifc, tool.Geometry)
|
||||
self.report({"INFO"}, f"{purged_representations} representations were purged.")
|
||||
|
||||
|
||||
class UpdateRepresentation(bpy.types.Operator, Operator):
|
||||
@@ -424,7 +473,7 @@ class CopyRepresentation(bpy.types.Operator, Operator):
|
||||
if not element:
|
||||
continue
|
||||
bm.to_mesh(obj.data)
|
||||
old_rep = self.get_representation_by_context(element, geometric_context)
|
||||
old_rep = tool.Geometry.get_representation_by_context(element, geometric_context)
|
||||
if old_rep:
|
||||
ifcopenshell.api.run(
|
||||
"geometry.unassign_representation", tool.Ifc.get(), product=element, representation=old_rep
|
||||
@@ -441,16 +490,6 @@ class CopyRepresentation(bpy.types.Operator, Operator):
|
||||
profile_set_usage=None,
|
||||
)
|
||||
|
||||
def get_representation_by_context(self, element, context):
|
||||
if element.is_a("IfcProduct") and element.Representation:
|
||||
for r in element.Representation.Representations:
|
||||
if r.ContextOfItems == context:
|
||||
return r
|
||||
elif element.is_a("IfcTypeProduct") and element.RepresentationMaps:
|
||||
for r in element.RepresentationMaps:
|
||||
if r.MappedRepresentation.ContextOfItems == context:
|
||||
return r.MappedRepresentation
|
||||
|
||||
|
||||
class OverrideDelete(bpy.types.Operator):
|
||||
bl_idname = "bim.override_object_delete"
|
||||
@@ -499,6 +538,8 @@ class OverrideDelete(bpy.types.Operator):
|
||||
def _execute(self, context):
|
||||
if self.is_batch:
|
||||
ifcopenshell.util.element.batch_remove_deep2(tool.Ifc.get())
|
||||
|
||||
self.process_arrays(context)
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element:
|
||||
@@ -508,6 +549,7 @@ class OverrideDelete(bpy.types.Operator):
|
||||
tool.Geometry.delete_ifc_object(obj)
|
||||
else:
|
||||
bpy.data.objects.remove(obj)
|
||||
|
||||
if self.is_batch:
|
||||
old_file = tool.Ifc.get()
|
||||
old_file.end_transaction()
|
||||
@@ -528,6 +570,30 @@ class OverrideDelete(bpy.types.Operator):
|
||||
data["old_file"].redo()
|
||||
tool.Ifc.set(data["new_file"])
|
||||
|
||||
def process_arrays(self, context):
|
||||
selected_objects = set(context.selected_objects)
|
||||
array_parents = set()
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array")
|
||||
if not pset:
|
||||
continue
|
||||
array_parents.add(tool.Ifc.get().by_guid(pset["Parent"]))
|
||||
|
||||
for array_parent in array_parents:
|
||||
array_parent_obj = tool.Ifc.get_object(array_parent)
|
||||
data = [(i, data) for i, data in enumerate(tool.Blender.Modifier.Array.get_modifiers_data(array_parent))]
|
||||
# NOTE: there is a way to remove arrays more precisely but it's more complex
|
||||
for i, modifier_data in reversed(data):
|
||||
children = set(tool.Blender.Modifier.Array.get_children_objects(modifier_data))
|
||||
if children.issubset(selected_objects):
|
||||
with context.temp_override(active_object=array_parent_obj):
|
||||
bpy.ops.bim.remove_array(item=i)
|
||||
else:
|
||||
break # allows to remove only n last layers of an array
|
||||
|
||||
|
||||
class OverrideOutlinerDelete(bpy.types.Operator):
|
||||
bl_idname = "bim.override_outliner_delete"
|
||||
@@ -538,7 +604,7 @@ class OverrideOutlinerDelete(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return len(context.selected_ids) > 0
|
||||
return len(getattr(context, "selected_ids", [])) > 0
|
||||
|
||||
def execute(self, context):
|
||||
# In this override, we don't check self.hierarchy. This effectively
|
||||
@@ -653,6 +719,13 @@ class OverrideDuplicateMove(bpy.types.Operator):
|
||||
return len(context.selected_objects) > 0
|
||||
|
||||
def execute(self, context):
|
||||
return OverrideDuplicateMove.execute_duplicate_operator(self, context, linked=False)
|
||||
|
||||
def _execute(self, context):
|
||||
return OverrideDuplicateMove.execute_ifc_duplicate_operator(self, context)
|
||||
|
||||
@staticmethod
|
||||
def execute_duplicate_operator(self, context, linked=False):
|
||||
# Deep magick from the dawn of time
|
||||
if IfcStore.get_file():
|
||||
IfcStore.execute_ifc_operator(self, context)
|
||||
@@ -663,7 +736,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
|
||||
new_active_obj = None
|
||||
for obj in context.selected_objects:
|
||||
new_obj = obj.copy()
|
||||
if obj.data:
|
||||
if linked and obj.data:
|
||||
new_obj.data = obj.data.copy()
|
||||
if obj == context.active_object:
|
||||
new_active_obj = new_obj
|
||||
@@ -675,52 +748,112 @@ class OverrideDuplicateMove(bpy.types.Operator):
|
||||
context.view_layer.objects.active = new_active_obj
|
||||
return {"FINISHED"}
|
||||
|
||||
def _execute(self, context):
|
||||
@staticmethod
|
||||
def execute_ifc_duplicate_operator(self, context, linked=False):
|
||||
objects_to_duplicate = set(context.selected_objects)
|
||||
|
||||
# handle arrays
|
||||
arrays_to_duplicate, array_children = OverrideDuplicateMove.process_arrays(self, context)
|
||||
objects_to_duplicate -= array_children
|
||||
for child in array_children:
|
||||
child.select_set(False)
|
||||
|
||||
self.new_active_obj = None
|
||||
# Track decompositions so they can be recreated after the operation
|
||||
relationships = tool.Root.get_decomposition_relationships(context.selected_objects)
|
||||
relationships = tool.Root.get_decomposition_relationships(objects_to_duplicate)
|
||||
old_to_new = {}
|
||||
for obj in context.selected_objects:
|
||||
|
||||
for obj in objects_to_duplicate:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element and element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
|
||||
continue # For now, don't copy drawings until we stabilise a bit more. It's tricky.
|
||||
|
||||
linked_non_ifc_object = linked and not element
|
||||
|
||||
# Prior to duplicating, sync the object placement to make decomposition recreation more stable.
|
||||
if tool.Ifc.is_moved(obj):
|
||||
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
||||
|
||||
new_obj = obj.copy()
|
||||
temp_data = None
|
||||
if obj.data:
|
||||
|
||||
if obj.data and not linked_non_ifc_object:
|
||||
# assure root.copy_class won't replace the previous mesh globally
|
||||
temp_data = obj.data.copy()
|
||||
new_obj.data = temp_data
|
||||
|
||||
if obj == context.active_object:
|
||||
self.new_active_obj = new_obj
|
||||
for collection in obj.users_collection:
|
||||
collection.objects.link(new_obj)
|
||||
obj.select_set(False)
|
||||
new_obj.select_set(True)
|
||||
|
||||
if linked_non_ifc_object:
|
||||
continue
|
||||
|
||||
# clear object's collection so it will be able to have it's own
|
||||
new_obj.BIMObjectProperties.collection = None
|
||||
# copy the actual class
|
||||
new = blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj)
|
||||
|
||||
# clean up the orphaned mesh with ifc id of the original object to avoid confusion
|
||||
if new and temp_data:
|
||||
# IfcGridAxis keeps the same mesh data (it's pointing to ifc id 0, so it's not a problem)
|
||||
if new and temp_data and not new.is_a("IfcGridAxis"):
|
||||
tool.Blender.remove_data_block(temp_data)
|
||||
|
||||
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]
|
||||
# TODO: handle array data for other cases of duplication
|
||||
array_data = arrays_to_duplicate.get(obj, None)
|
||||
tool.Model.handle_array_on_copied_element(new, array_data)
|
||||
if array_data:
|
||||
for child in tool.Blender.Modifier.Array.get_all_children_objects(new):
|
||||
child.select_set(True)
|
||||
|
||||
# TODO: add new array children to recreate their decomposition too
|
||||
old_to_new[element] = [new]
|
||||
if new.is_a("IfcRelSpaceBoundary"):
|
||||
tool.Boundary.decorate_boundary(new_obj)
|
||||
|
||||
# Recreate decompositions
|
||||
tool.Root.recreate_decompositions(relationships, old_to_new)
|
||||
blenderbim.bim.handler.purge_module_data()
|
||||
blenderbim.bim.handler.refresh_ui_data()
|
||||
|
||||
@staticmethod
|
||||
def process_arrays(self, context):
|
||||
selected_objects = set(context.selected_objects)
|
||||
array_parents = set()
|
||||
arrays_to_create = dict()
|
||||
array_children = set() # will be ignored during the duplication
|
||||
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array")
|
||||
if not pset:
|
||||
continue
|
||||
array_parents.add(tool.Ifc.get().by_guid(pset["Parent"]))
|
||||
|
||||
for array_parent in array_parents:
|
||||
array_parent_obj = tool.Ifc.get_object(array_parent)
|
||||
if array_parent_obj not in selected_objects:
|
||||
continue
|
||||
|
||||
array_data = []
|
||||
for modifier_data in tool.Blender.Modifier.Array.get_modifiers_data(array_parent):
|
||||
children = set(tool.Blender.Modifier.Array.get_children_objects(modifier_data))
|
||||
if children.issubset(selected_objects):
|
||||
modifier_data["children"] = []
|
||||
array_data.append(modifier_data)
|
||||
array_children.update(children)
|
||||
else:
|
||||
break # allows to duplicate only n first layers of an array
|
||||
|
||||
if array_data:
|
||||
arrays_to_create[array_parent_obj] = array_data
|
||||
|
||||
return arrays_to_create, array_children
|
||||
|
||||
|
||||
class OverrideDuplicateMoveLinkedMacro(bpy.types.Macro):
|
||||
@@ -739,57 +872,10 @@ class OverrideDuplicateMoveLinked(bpy.types.Operator):
|
||||
return len(context.selected_objects) > 0
|
||||
|
||||
def execute(self, context):
|
||||
# Deep magick from the dawn of time
|
||||
if IfcStore.get_file():
|
||||
IfcStore.execute_ifc_operator(self, context)
|
||||
if self.new_active_obj:
|
||||
context.view_layer.objects.active = self.new_active_obj
|
||||
return {"FINISHED"}
|
||||
|
||||
new_active_obj = None
|
||||
for obj in context.selected_objects:
|
||||
new_obj = obj.copy()
|
||||
if obj == context.active_object:
|
||||
new_active_obj = new_obj
|
||||
for collection in obj.users_collection:
|
||||
collection.objects.link(new_obj)
|
||||
obj.select_set(False)
|
||||
new_obj.select_set(True)
|
||||
if new_active_obj:
|
||||
context.view_layer.objects.active = new_active_obj
|
||||
return {"FINISHED"}
|
||||
return OverrideDuplicateMove.execute_duplicate_operator(self, context, linked=True)
|
||||
|
||||
def _execute(self, context):
|
||||
self.new_active_obj = None
|
||||
# Track decompositions so they can be recreated after the operation
|
||||
relationships = tool.Root.get_decomposition_relationships(context.selected_objects)
|
||||
old_to_new = {}
|
||||
for obj in context.selected_objects:
|
||||
# Prior to duplicating, sync the object placement to make decomposition recreation more stable.
|
||||
if tool.Ifc.is_moved(obj):
|
||||
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
||||
|
||||
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:
|
||||
collection.objects.link(new_obj)
|
||||
obj.select_set(False)
|
||||
new_obj.select_set(True)
|
||||
# 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)
|
||||
blenderbim.bim.handler.purge_module_data()
|
||||
return {"FINISHED"}
|
||||
return OverrideDuplicateMove.execute_ifc_duplicate_operator(self, context, linked=True)
|
||||
|
||||
|
||||
class OverrideDuplicateMoveAggregateMacro(bpy.types.Macro):
|
||||
@@ -809,28 +895,7 @@ class OverrideDuplicateMoveAggregate(bpy.types.Operator):
|
||||
return len(context.selected_objects) > 0
|
||||
|
||||
def execute(self, context):
|
||||
# Deep magick from the dawn of time
|
||||
if IfcStore.get_file():
|
||||
IfcStore.execute_ifc_operator(self, context)
|
||||
if self.new_active_obj:
|
||||
context.view_layer.objects.active = self.new_active_obj
|
||||
return {"FINISHED"}
|
||||
|
||||
new_active_obj = None
|
||||
|
||||
for obj in context.selected_objects:
|
||||
new_obj = obj.copy()
|
||||
if obj.data:
|
||||
new_obj.data = obj.data.copy()
|
||||
if obj == context.active_object:
|
||||
new_active_obj = new_obj
|
||||
for collection in obj.users_collection:
|
||||
collection.objects.link(new_obj)
|
||||
obj.select_set(False)
|
||||
new_obj.select_set(True)
|
||||
if new_active_obj:
|
||||
context.view_layer.objects.active = new_active_obj
|
||||
return {"FINISHED"}
|
||||
return OverrideDuplicateMove.execute_duplicate_operator(self, context, linked=False)
|
||||
|
||||
def _execute(self, context):
|
||||
self.new_active_obj = None
|
||||
@@ -847,14 +912,14 @@ class OverrideDuplicateMoveAggregate(bpy.types.Operator):
|
||||
"pset.edit_pset",
|
||||
tool.Ifc.get(),
|
||||
pset=tool.Ifc.get().by_id(pset["id"]),
|
||||
properties={"Data": json.dumps(data)},
|
||||
properties={"Data": tool.Ifc.get().createIfcText(json.dumps(data))},
|
||||
)
|
||||
else:
|
||||
ifcopenshell.api.run(
|
||||
"pset.edit_pset",
|
||||
tool.Ifc.get(),
|
||||
pset=tool.Ifc.get().by_id(pset["id"]),
|
||||
properties={"Parent": parent.GlobalId, "Data": json.dumps(data)},
|
||||
properties={"Parent": parent.GlobalId, "Data": tool.Ifc.get().createIfcText(json.dumps(data))},
|
||||
)
|
||||
|
||||
else:
|
||||
@@ -866,7 +931,7 @@ class OverrideDuplicateMoveAggregate(bpy.types.Operator):
|
||||
"pset.edit_pset",
|
||||
tool.Ifc.get(),
|
||||
pset=pset,
|
||||
properties={"Parent": parent.GlobalId, "Data": json.dumps(data)},
|
||||
properties={"Parent": parent.GlobalId, "Data": tool.Ifc.get().createIfcText(json.dumps(data))},
|
||||
)
|
||||
|
||||
def add_child_to_assembly_data(new_entity):
|
||||
@@ -880,7 +945,7 @@ class OverrideDuplicateMoveAggregate(bpy.types.Operator):
|
||||
"pset.edit_pset",
|
||||
tool.Ifc.get(),
|
||||
pset=tool.Ifc.get().by_id(parent_pset["id"]),
|
||||
properties={"Data": json.dumps(data)},
|
||||
properties={"Data": tool.Ifc.get().createIfcText(json.dumps(data))},
|
||||
)
|
||||
|
||||
def create_data_structure(entity, level=-1):
|
||||
@@ -1028,12 +1093,7 @@ class OverrideDuplicateMoveAggregate(bpy.types.Operator):
|
||||
)
|
||||
|
||||
if new_entity:
|
||||
# Checks if the object belongs to an Ifc Array
|
||||
array_pset = ifcopenshell.util.element.get_pset(new_entity, "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_entity, pset=array_pset)
|
||||
|
||||
tool.Model.handle_array_on_copied_element(new_entity)
|
||||
blenderbim.core.aggregate.unassign_object(
|
||||
tool.Ifc,
|
||||
tool.Aggregate,
|
||||
@@ -1068,6 +1128,17 @@ class OverrideDuplicateMoveAggregate(bpy.types.Operator):
|
||||
|
||||
recreate_data_structure(new_root_entity)
|
||||
|
||||
# Remove connections with old objects
|
||||
for new in old_to_new.values():
|
||||
for connection in new[0].ConnectedTo:
|
||||
entity = connection.RelatedElement
|
||||
if entity in old_to_new.keys():
|
||||
core.remove_connection(tool.Geometry, connection=connection)
|
||||
for connection in new[0].ConnectedFrom:
|
||||
entity = connection.RelatingElement
|
||||
if entity in old_to_new.keys():
|
||||
core.remove_connection(tool.Geometry, connection=connection)
|
||||
|
||||
old_objs = []
|
||||
for old, new in old_to_new.items():
|
||||
old_objs.append(tool.Ifc.get_object(old))
|
||||
@@ -1076,7 +1147,7 @@ class OverrideDuplicateMoveAggregate(bpy.types.Operator):
|
||||
|
||||
tool.Root.recreate_decompositions(relationships, old_to_new)
|
||||
|
||||
blenderbim.bim.handler.purge_module_data()
|
||||
blenderbim.bim.handler.refresh_ui_data()
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -1099,6 +1170,7 @@ class RefreshAggregate(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
def _execute(self, context):
|
||||
refresh_start_time = time()
|
||||
self.new_active_obj = None
|
||||
old_to_new = {}
|
||||
|
||||
@@ -1131,9 +1203,70 @@ class RefreshAggregate(bpy.types.Operator):
|
||||
else:
|
||||
parts = ifcopenshell.util.element.get_parts(instance_entity)
|
||||
for part in parts:
|
||||
pset = ifcopenshell.util.element.get_pset(part, "BBIM_Aggregate_Data")
|
||||
if part.is_a("IfcElementAssembly"):
|
||||
if not pset:
|
||||
data_children = {
|
||||
"children": [],
|
||||
"instance_of": [part.GlobalId],
|
||||
}
|
||||
data = [data_children]
|
||||
|
||||
pset = ifcopenshell.api.run(
|
||||
"pset.add_pset", tool.Ifc.get(), product=part, name="BBIM_Aggregate_Data"
|
||||
)
|
||||
ifcopenshell.api.run(
|
||||
"pset.edit_pset",
|
||||
tool.Ifc.get(),
|
||||
pset=pset,
|
||||
properties={
|
||||
"Parent": instance_entity.GlobalId,
|
||||
"Data": tool.Ifc.get().createIfcText(json.dumps(data)),
|
||||
},
|
||||
)
|
||||
|
||||
part_obj = tool.Ifc.get_object(part)
|
||||
new_part = duplicate_objects(part_obj)
|
||||
|
||||
blenderbim.core.aggregate.assign_object(
|
||||
tool.Ifc,
|
||||
tool.Aggregate,
|
||||
tool.Collector,
|
||||
relating_obj=tool.Ifc.get_object(entity),
|
||||
related_obj=tool.Ifc.get_object(new_part),
|
||||
)
|
||||
duplicate_children(new_part)
|
||||
|
||||
for part in parts:
|
||||
pset = ifcopenshell.util.element.get_pset(part, "BBIM_Aggregate_Data")
|
||||
if part.is_a("IfcElementAssembly"):
|
||||
pass
|
||||
|
||||
else:
|
||||
if not pset:
|
||||
pset = ifcopenshell.api.run(
|
||||
"pset.add_pset", tool.Ifc.get(), product=part, name="BBIM_Aggregate_Data"
|
||||
)
|
||||
else:
|
||||
pset = ifcopenshell.util.element.get_pset(part, "BBIM_Aggregate_Data")
|
||||
pset = tool.Ifc.get().by_id(pset["id"])
|
||||
|
||||
data_children = {
|
||||
"children": [],
|
||||
"instance_of": [],
|
||||
}
|
||||
data = [data_children]
|
||||
|
||||
ifcopenshell.api.run(
|
||||
"pset.edit_pset",
|
||||
tool.Ifc.get(),
|
||||
pset=pset,
|
||||
properties={
|
||||
"Parent": instance_entity.GlobalId,
|
||||
"Data": tool.Ifc.get().createIfcText(json.dumps(data)),
|
||||
},
|
||||
)
|
||||
|
||||
part_obj = tool.Ifc.get_object(part)
|
||||
new_part = duplicate_objects(part_obj)
|
||||
blenderbim.core.aggregate.assign_object(
|
||||
@@ -1155,47 +1288,58 @@ class RefreshAggregate(bpy.types.Operator):
|
||||
obj.select_set(False)
|
||||
new_obj.select_set(True)
|
||||
|
||||
# This is needed to make sure the new object gets unlink from
|
||||
# the old object assembly collection
|
||||
new_obj.BIMObjectProperties.collection = None
|
||||
|
||||
# Copy the actual class
|
||||
new_entity = blenderbim.core.root.copy_class(
|
||||
tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj
|
||||
)
|
||||
|
||||
if new_entity:
|
||||
# Checks if the object belongs to an Ifc Array
|
||||
array_pset = ifcopenshell.util.element.get_pset(new_entity, "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_entity, pset=array_pset)
|
||||
tool.Model.handle_array_on_copied_element(new_entity)
|
||||
|
||||
blenderbim.core.aggregate.unassign_object(
|
||||
tool.Ifc,
|
||||
tool.Aggregate,
|
||||
tool.Collector,
|
||||
relating_obj=obj,
|
||||
related_obj=tool.Ifc.get_object(new_entity),
|
||||
)
|
||||
if not new_entity.is_a("IfcElementAssembly"):
|
||||
blenderbim.core.aggregate.unassign_object(
|
||||
tool.Ifc,
|
||||
tool.Aggregate,
|
||||
tool.Collector,
|
||||
relating_obj=obj,
|
||||
related_obj=tool.Ifc.get_object(new_entity),
|
||||
)
|
||||
|
||||
old_to_new[tool.Ifc.get_entity(obj)] = [new_entity]
|
||||
|
||||
return new_entity
|
||||
|
||||
if len(context.selected_objects) != 1:
|
||||
self.report({"INFO"}, "Only 1 object need to be selected.")
|
||||
return {"FINISHED"}
|
||||
|
||||
selected_root_obj = context.selected_objects[0]
|
||||
selected_root_entity = tool.Ifc.get_entity(selected_root_obj)
|
||||
|
||||
if not selected_root_entity.is_a("IfcElementAssembly"):
|
||||
if selected_root_entity.is_a("IfcElementAssembly"):
|
||||
pass
|
||||
elif selected_root_entity.Decomposes:
|
||||
if selected_root_entity.Decomposes[0].RelatingObject.is_a("IfcElementAssembly"):
|
||||
selected_root_entity = selected_root_entity.Decomposes[0].RelatingObject
|
||||
selected_root_obj = tool.Ifc.get_object(selected_root_entity)
|
||||
else:
|
||||
self.report({"INFO"}, "Object is not part of a IfcElementAssembly.")
|
||||
return {"FINISHED"}
|
||||
|
||||
pset = ifcopenshell.util.element.get_pset(selected_root_entity, "BBIM_Aggregate_Data")
|
||||
if not pset:
|
||||
self.report({"INFO"}, "Object is not part of an assembly aggregate.")
|
||||
return {"FINISHED"}
|
||||
|
||||
pset_data = json.loads(pset["Data"])[0]
|
||||
instance_of = pset_data["instance_of"][0]
|
||||
original_root_entity = tool.Ifc.get().by_guid(instance_of)
|
||||
if original_root_entity == selected_root_entity:
|
||||
self.report({"INFO"}, "Cannot refresh original assembly. Select an assembly instance.")
|
||||
return {"FINISHED"}
|
||||
|
||||
parents = remove_objects(selected_root_entity)
|
||||
@@ -1208,13 +1352,24 @@ class RefreshAggregate(bpy.types.Operator):
|
||||
for parent in parents:
|
||||
duplicate_children(parent)
|
||||
|
||||
# Remove connections with old objects
|
||||
for new in old_to_new.values():
|
||||
for connection in new[0].ConnectedTo:
|
||||
entity = connection.RelatedElement
|
||||
if entity in old_to_new.keys():
|
||||
core.remove_connection(tool.Geometry, connection=connection)
|
||||
for connection in new[0].ConnectedFrom:
|
||||
entity = connection.RelatingElement
|
||||
if entity in old_to_new.keys():
|
||||
core.remove_connection(tool.Geometry, connection=connection)
|
||||
|
||||
old_objs = []
|
||||
for old, new in old_to_new.items():
|
||||
old_objs.append(tool.Ifc.get_object(old))
|
||||
|
||||
new_obj = tool.Ifc.get_object(new[0])
|
||||
|
||||
matrix_diff = new_obj.matrix_world @ original_matrix
|
||||
matrix_diff = Matrix.inverted(original_matrix) @ new_obj.matrix_world
|
||||
new_matrix = selected_matrix @ matrix_diff
|
||||
|
||||
new_obj.matrix_world = new_matrix
|
||||
@@ -1223,8 +1378,11 @@ class RefreshAggregate(bpy.types.Operator):
|
||||
|
||||
tool.Root.recreate_decompositions(relationships, old_to_new)
|
||||
|
||||
blenderbim.bim.handler.purge_module_data()
|
||||
blenderbim.bim.handler.refresh_ui_data()
|
||||
|
||||
operator_time = time() - refresh_start_time
|
||||
if operator_time > 10:
|
||||
self.report({"INFO"}, "Refresh Aggregate was finished in {:.2f} seconds".format(operator_time))
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -1549,6 +1707,9 @@ class OverrideModeSetObject(bpy.types.Operator):
|
||||
bpy.ops.bim.finish_editing_roof_path()
|
||||
elif tool.Model.get_usage_type(element) == "PROFILE":
|
||||
bpy.ops.bim.edit_extrusion_axis()
|
||||
# if in the process of editing arbitrary profile
|
||||
elif context.scene.BIMProfileProperties.active_arbitrary_profile_id:
|
||||
bpy.ops.bim.edit_arbitrary_profile()
|
||||
else:
|
||||
bpy.ops.bim.edit_extrusion_profile()
|
||||
return self.execute(context)
|
||||
@@ -1569,3 +1730,70 @@ class OverrideModeSetObject(bpy.types.Operator):
|
||||
if self.edited_objs:
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
return self.execute(context)
|
||||
|
||||
|
||||
class FlipObject(bpy.types.Operator):
|
||||
bl_idname = "bim.flip_object"
|
||||
bl_label = "Flip Object"
|
||||
bl_description = "Flip object's local axes, keep the position"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
flip_local_axes: bpy.props.EnumProperty(
|
||||
name="Flip Local Axes", items=(("XY", "XY", ""), ("YZ", "YZ", ""), ("XZ", "XZ", "")), default="XY"
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
for obj in context.selected_objects:
|
||||
tool.Geometry.flip_object(obj, self.flip_local_axes)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class EnableEditingRepresentationItems(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.enable_editing_representation_items"
|
||||
bl_label = "Enable Editing Representation Items"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
obj = context.active_object
|
||||
|
||||
props = obj.BIMGeometryProperties
|
||||
props.is_editing = True
|
||||
|
||||
props.items.clear()
|
||||
|
||||
if bpy.context.active_object.data and hasattr(bpy.context.active_object.data, "BIMMeshProperties"):
|
||||
active_representation_id = bpy.context.active_object.data.BIMMeshProperties.ifc_definition_id
|
||||
element = tool.Ifc.get().by_id(active_representation_id)
|
||||
if not element.is_a("IfcShapeRepresentation"):
|
||||
return
|
||||
queue = list(element.Items)
|
||||
while queue:
|
||||
item = queue.pop()
|
||||
if item.is_a("IfcMappedItem"):
|
||||
queue.extend(item.MappingSource.MappedRepresentation.Items)
|
||||
else:
|
||||
new = props.items.add()
|
||||
new.name = item.is_a()
|
||||
new.ifc_definition_id = item.id()
|
||||
|
||||
styles = []
|
||||
for inverse in tool.Ifc.get().get_inverse(item):
|
||||
if inverse.is_a("IfcStyledItem"):
|
||||
styles = inverse.Styles
|
||||
if styles and styles[0].is_a("IfcPresentationStyleAssignment"):
|
||||
styles = styles[0].Styles
|
||||
for style in styles:
|
||||
if style.is_a("IfcSurfaceStyle"):
|
||||
new.surface_style = style.Name or "Unnamed"
|
||||
elif inverse.is_a("IfcPresentationLayerAssignment"):
|
||||
new.layer = inverse.Name or "Unnamed"
|
||||
|
||||
|
||||
class DisableEditingRepresentationItems(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.disable_editing_representation_items"
|
||||
bl_label = "Disable Editing Representation Items"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
obj = context.active_object
|
||||
obj.BIMGeometryProperties.is_editing = False
|
||||
|
||||
@@ -38,8 +38,18 @@ def get_contexts(self, context):
|
||||
return RepresentationsData.data["contexts"]
|
||||
|
||||
|
||||
class RepresentationItem(PropertyGroup):
|
||||
name: StringProperty(name="Name")
|
||||
surface_style: StringProperty(name="Surface Style")
|
||||
layer: StringProperty(name="Layer")
|
||||
ifc_definition_id: IntProperty(name="IFC Definition ID")
|
||||
|
||||
|
||||
class BIMObjectGeometryProperties(PropertyGroup):
|
||||
contexts: EnumProperty(items=get_contexts, name="Contexts")
|
||||
is_editing: BoolProperty(name="Is Editing", default=False)
|
||||
items: CollectionProperty(name="Representation Items", type=RepresentationItem)
|
||||
active_item_index: IntProperty(name="Active Representation Item Index")
|
||||
|
||||
|
||||
class BIMGeometryProperties(PropertyGroup):
|
||||
|
||||
@@ -17,11 +17,18 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import blenderbim.bim
|
||||
import blenderbim.tool as tool
|
||||
from bpy.types import Panel, Menu
|
||||
from bpy.types import Panel, Menu, UIList
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.helper import prop_with_search
|
||||
from blenderbim.bim.module.geometry.data import RepresentationsData, ConnectionsData, DerivedPlacementsData
|
||||
from blenderbim.bim.module.geometry.data import (
|
||||
RepresentationsData,
|
||||
RepresentationItemsData,
|
||||
ConnectionsData,
|
||||
PlacementData,
|
||||
DerivedCoordinatesData,
|
||||
)
|
||||
|
||||
|
||||
def object_menu(self, context):
|
||||
@@ -32,6 +39,37 @@ def object_menu(self, context):
|
||||
self.layout.menu("BIM_MT_object_set_origin", icon="PLUGIN")
|
||||
|
||||
|
||||
def edit_mesh_menu(self, context):
|
||||
self.layout.separator()
|
||||
self.layout.menu("BIM_MT_separate", icon="PLUGIN")
|
||||
|
||||
|
||||
class BIM_MT_separate(Menu):
|
||||
bl_idname = "BIM_MT_separate"
|
||||
bl_label = "IFC Separate"
|
||||
|
||||
def draw(self, context):
|
||||
self.layout.operator("bim.override_mesh_separate", icon="PLUGIN", text="IFC Selection").type = "SELECTED"
|
||||
self.layout.operator("bim.override_mesh_separate", icon="PLUGIN", text="IFC By Material").type = "MATERIAL"
|
||||
self.layout.operator("bim.override_mesh_separate", icon="PLUGIN", text="IFC By Loose Parts").type = "LOOSE"
|
||||
|
||||
|
||||
class BIM_MT_hotkey_separate(Menu):
|
||||
bl_idname = "BIM_MT_hotkey_separate"
|
||||
bl_label = "Separate"
|
||||
|
||||
def draw(self, context):
|
||||
self.layout.label(text="IFC Separate", icon_value=blenderbim.bim.icons["IFC"].icon_id)
|
||||
self.layout.operator("bim.override_mesh_separate", text="Selection").type = "SELECTED"
|
||||
self.layout.operator("bim.override_mesh_separate", text="By Material").type = "MATERIAL"
|
||||
self.layout.operator("bim.override_mesh_separate", text="By Loose Parts").type = "LOOSE"
|
||||
self.layout.separator()
|
||||
self.layout.label(text="Blender Separate", icon="BLENDER")
|
||||
self.layout.operator("mesh.separate", text="Selection").type = "SELECTED"
|
||||
self.layout.operator("mesh.separate", text="By Material").type = "MATERIAL"
|
||||
self.layout.operator("mesh.separate", text="By Loose Parts").type = "LOOSE"
|
||||
|
||||
|
||||
class BIM_MT_object_set_origin(Menu):
|
||||
bl_idname = "BIM_MT_object_set_origin"
|
||||
bl_label = "IFC Set Origin"
|
||||
@@ -109,6 +147,53 @@ class BIM_PT_representations(Panel):
|
||||
row.operator("bim.remove_representation", icon="X", text="").representation_id = representation["id"]
|
||||
|
||||
|
||||
class BIM_PT_representation_items(Panel):
|
||||
bl_label = "Representation Items"
|
||||
bl_idname = "BIM_PT_representation_items"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_order = 1
|
||||
bl_parent_id = "BIM_PT_tab_representations"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not context.active_object:
|
||||
return False
|
||||
if not IfcStore.get_element(context.active_object.BIMObjectProperties.ifc_definition_id):
|
||||
return False
|
||||
return IfcStore.get_file()
|
||||
|
||||
def draw(self, context):
|
||||
if not RepresentationItemsData.is_loaded:
|
||||
RepresentationItemsData.load()
|
||||
|
||||
props = context.active_object.BIMGeometryProperties
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=f"{RepresentationItemsData.data['total_items']} Items Found")
|
||||
|
||||
if props.is_editing:
|
||||
row.operator("bim.disable_editing_representation_items", icon="CANCEL", text="")
|
||||
else:
|
||||
row.operator("bim.enable_editing_representation_items", icon="IMPORT", text="")
|
||||
return
|
||||
|
||||
self.layout.template_list("BIM_UL_representation_items", "", props, "items", props, "active_item_index")
|
||||
|
||||
row = self.layout.row()
|
||||
if RepresentationItemsData.data["active_surface_style"]:
|
||||
row.label(text=RepresentationItemsData.data["active_surface_style"], icon="MATERIAL")
|
||||
else:
|
||||
row.label(text="No Surface Style", icon="MESH_UVSPHERE")
|
||||
|
||||
row = self.layout.row()
|
||||
if RepresentationItemsData.data["active_layer"]:
|
||||
row.label(text=RepresentationItemsData.data["active_layer"], icon="STICKY_UVS_LOC")
|
||||
else:
|
||||
row.label(text="No Presentation Layer", icon="STICKY_UVS_LOC")
|
||||
|
||||
|
||||
class BIM_PT_connections(Panel):
|
||||
bl_label = "Connections"
|
||||
bl_idname = "BIM_PT_connections"
|
||||
@@ -212,6 +297,9 @@ class BIM_PT_mesh(Panel):
|
||||
|
||||
layout = self.layout
|
||||
|
||||
row = layout.row()
|
||||
row.operator("bim.update_representation", text="Manually Save Representation")
|
||||
|
||||
row = layout.row()
|
||||
row.operator("bim.copy_representation", text="Copy Mesh From Active To Selected")
|
||||
|
||||
@@ -235,50 +323,99 @@ class BIM_PT_mesh(Panel):
|
||||
op = row.operator("bim.update_representation", text="Convert To Arbitrary Extrusion With Voids")
|
||||
op.ifc_representation_class = "IfcExtrudedAreaSolid/IfcArbitraryProfileDefWithVoids"
|
||||
|
||||
|
||||
def BIM_PT_transform(self, context):
|
||||
if context.active_object and context.active_object.BIMObjectProperties.ifc_definition_id:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Blender Offset")
|
||||
row.label(text=context.active_object.BIMObjectProperties.blender_offset_type)
|
||||
|
||||
row = self.layout.row()
|
||||
row.operator("bim.edit_object_placement")
|
||||
if context.active_object and context.active_object.data:
|
||||
mprops = context.active_object.data.BIMMeshProperties
|
||||
row = layout.row()
|
||||
row.operator("bim.get_representation_ifc_parameters")
|
||||
for index, ifc_parameter in enumerate(mprops.ifc_parameters):
|
||||
row = layout.row(align=True)
|
||||
row.prop(ifc_parameter, "name", text="")
|
||||
row.prop(ifc_parameter, "value", text="")
|
||||
row.operator("bim.update_parametric_representation", icon="FILE_REFRESH", text="").index = index
|
||||
|
||||
|
||||
class BIM_PT_derived_placements(Panel):
|
||||
bl_label = "Derived Placements"
|
||||
bl_idname = "BIM_PT_derived_placements"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
class BIM_PT_placement(Panel):
|
||||
bl_label = "Placement"
|
||||
bl_idname = "BIM_PT_placement"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "object"
|
||||
bl_context = "scene"
|
||||
bl_order = 1
|
||||
bl_parent_id = "OBJECT_PT_transform"
|
||||
bl_parent_id = "BIM_PT_tab_placement"
|
||||
bl_options = {"HIDE_HEADER"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.active_object and context.active_object.BIMObjectProperties.ifc_definition_id
|
||||
|
||||
def draw(self, context):
|
||||
if not DerivedPlacementsData.is_loaded:
|
||||
DerivedPlacementsData.load()
|
||||
if not PlacementData.is_loaded:
|
||||
PlacementData.load()
|
||||
|
||||
if PlacementData.data["has_placement"]:
|
||||
row = self.layout.row()
|
||||
row.prop(context.active_object, "location", text="Location")
|
||||
row = self.layout.row()
|
||||
row.prop(context.active_object, "rotation_euler", text="Rotation")
|
||||
else:
|
||||
row = self.layout.row()
|
||||
row.label(text="No Object Placement Found")
|
||||
|
||||
if context.active_object.BIMObjectProperties.blender_offset_type != "NONE":
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Blender Offset", icon="TRACKING_REFINE_FORWARDS")
|
||||
row.label(text=context.active_object.BIMObjectProperties.blender_offset_type)
|
||||
|
||||
|
||||
class BIM_PT_derived_coordinates(Panel):
|
||||
bl_label = "Derived Coordinates"
|
||||
bl_idname = "BIM_PT_derived_coordinates"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_order = 3
|
||||
bl_parent_id = "BIM_PT_tab_placement"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.active_object is not None
|
||||
|
||||
def draw(self, context):
|
||||
if not DerivedCoordinatesData.is_loaded:
|
||||
DerivedCoordinatesData.load()
|
||||
|
||||
row = self.layout.row()
|
||||
row.operator("bim.edit_object_placement", icon="EXPORT")
|
||||
|
||||
row = self.layout.row()
|
||||
row.label(text="XYZ Dimensions")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.enabled = False
|
||||
row.prop(context.active_object, "dimensions", text="X", index=0, slider=True)
|
||||
row.prop(context.active_object, "dimensions", text="Y", index=1, slider=True)
|
||||
row.prop(context.active_object, "dimensions", text="Z", index=2, slider=True)
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Min Global Z")
|
||||
row.label(text=DerivedPlacementsData.data["min_global_z"])
|
||||
row.label(text=DerivedCoordinatesData.data["min_global_z"])
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Max Global Z")
|
||||
row.label(text=DerivedPlacementsData.data["max_global_z"])
|
||||
row.label(text=DerivedCoordinatesData.data["max_global_z"])
|
||||
|
||||
if DerivedPlacementsData.data["has_collection"]:
|
||||
if DerivedCoordinatesData.data["has_collection"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Min Decomposed Z")
|
||||
row.label(text=DerivedPlacementsData.data["min_decomposed_z"])
|
||||
row.label(text=DerivedCoordinatesData.data["min_decomposed_z"])
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Max Decomposed Z")
|
||||
row.label(text=DerivedPlacementsData.data["max_decomposed_z"])
|
||||
row.label(text=DerivedCoordinatesData.data["max_decomposed_z"])
|
||||
|
||||
if DerivedPlacementsData.data["is_storey"]:
|
||||
if DerivedCoordinatesData.data["is_storey"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Storey Height")
|
||||
row.label(text=DerivedPlacementsData.data["storey_height"])
|
||||
row.label(text=DerivedCoordinatesData.data["storey_height"])
|
||||
|
||||
|
||||
class BIM_PT_workarounds(Panel):
|
||||
@@ -307,3 +444,13 @@ class BIM_PT_workarounds(Panel):
|
||||
row.prop(props, "should_force_triangulation")
|
||||
row = self.layout.row()
|
||||
row.prop(props, "should_use_presentation_style_assignment")
|
||||
|
||||
|
||||
class BIM_UL_representation_items(UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
if item:
|
||||
icon = "MATERIAL" if item.surface_style else "MESH_UVSPHERE"
|
||||
row = layout.row(align=True)
|
||||
row.label(text=item.name, icon=icon)
|
||||
if item.layer:
|
||||
row.label(text="", icon="STICKY_UVS_LOC")
|
||||
|
||||
@@ -56,7 +56,12 @@ class GeoreferenceData:
|
||||
@classmethod
|
||||
def map_conversion(cls):
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
return {}
|
||||
project = tool.Ifc.get().by_type("IfcProject")[0]
|
||||
map_conversion = ifcopenshell.util.element.get_pset(project, "ePSet_MapConversion")
|
||||
if not map_conversion:
|
||||
return {}
|
||||
del map_conversion["id"]
|
||||
return map_conversion
|
||||
|
||||
for context in tool.Ifc.get().by_type("IfcGeometricRepresentationContext", include_subtypes=False):
|
||||
if context.HasCoordinateOperation:
|
||||
@@ -72,8 +77,8 @@ class GeoreferenceData:
|
||||
def map_derived_angle(cls):
|
||||
if (
|
||||
cls.data["map_conversion"]
|
||||
and cls.data["map_conversion"]["XAxisAbscissa"] is not None
|
||||
and cls.data["map_conversion"]["XAxisOrdinate"] is not None
|
||||
and cls.data["map_conversion"].get("XAxisAbscissa", None) is not None
|
||||
and cls.data["map_conversion"].get("XAxisOrdinate", None) is not None
|
||||
):
|
||||
return str(
|
||||
round(
|
||||
@@ -88,7 +93,12 @@ class GeoreferenceData:
|
||||
@classmethod
|
||||
def projected_crs(cls):
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
return {}
|
||||
project = tool.Ifc.get().by_type("IfcProject")[0]
|
||||
projected_crs = ifcopenshell.util.element.get_pset(project, "ePSet_ProjectedCRS")
|
||||
if not projected_crs:
|
||||
return {}
|
||||
del projected_crs["id"]
|
||||
return projected_crs
|
||||
|
||||
for context in tool.Ifc.get().by_type("IfcGeometricRepresentationContext", include_subtypes=False):
|
||||
if context.HasCoordinateOperation:
|
||||
|
||||
@@ -29,7 +29,7 @@ class BIM_PT_gis(Panel):
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_geometry"
|
||||
bl_parent_id = "BIM_PT_tab_geometry"
|
||||
|
||||
def draw(self, context):
|
||||
self.layout.use_property_split = True
|
||||
@@ -78,10 +78,14 @@ class BIM_PT_gis(Panel):
|
||||
def draw_ui(self, context):
|
||||
props = context.scene.BIMGeoreferenceProperties
|
||||
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
row = self.layout.row()
|
||||
row.label(text="IFC2X3 Fallback In Use", icon="ERROR")
|
||||
|
||||
if not GeoreferenceData.data["projected_crs"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Not Georeferenced")
|
||||
if tool.Ifc.get_schema != "IFC2X3":
|
||||
if tool.Ifc.get_schema() != "IFC2X3":
|
||||
row.operator("bim.add_georeferencing", icon="ADD", text="")
|
||||
|
||||
if props.has_blender_offset:
|
||||
@@ -110,8 +114,9 @@ class BIM_PT_gis(Panel):
|
||||
if GeoreferenceData.data["projected_crs"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Projected CRS", icon="WORLD")
|
||||
row.operator("bim.enable_editing_georeferencing", icon="GREASEPENCIL", text="")
|
||||
row.operator("bim.remove_georeferencing", icon="X", text="")
|
||||
if tool.Ifc.get_schema() != "IFC2X3":
|
||||
row.operator("bim.enable_editing_georeferencing", icon="GREASEPENCIL", text="")
|
||||
row.operator("bim.remove_georeferencing", icon="X", text="")
|
||||
|
||||
for key, value in GeoreferenceData.data["projected_crs"].items():
|
||||
if not value:
|
||||
@@ -184,4 +189,4 @@ class BIM_PT_gis_utilities(Panel):
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "y_axis_abscissa_output", text="North Abscissa")
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "y_axis_ordinate_output", text="North Ordinate")
|
||||
row.prop(props, "y_axis_ordinate_output", text="North Ordinate")
|
||||
|
||||
@@ -29,7 +29,6 @@ classes = (
|
||||
operator.LoadGroups,
|
||||
operator.RemoveGroup,
|
||||
operator.SelectGroupProducts,
|
||||
operator.ToggleAssigningGroup,
|
||||
operator.ToggleGroup,
|
||||
operator.UnassignGroup,
|
||||
operator.UpdateGroup,
|
||||
|
||||
@@ -173,16 +173,6 @@ class DisableEditingGroup(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ToggleAssigningGroup(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.toggle_assigning_group"
|
||||
bl_label = "Toggle Assigning Group"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
context.scene.BIMGroupProperties.is_adding = not context.scene.BIMGroupProperties.is_adding
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AssignGroup(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.assign_group"
|
||||
bl_label = "Assign Group"
|
||||
|
||||
@@ -49,7 +49,6 @@ class Group(PropertyGroup):
|
||||
class BIMGroupProperties(PropertyGroup):
|
||||
group_attributes: CollectionProperty(name="Group Attributes", type=Attribute)
|
||||
is_editing: BoolProperty(name="Is Editing", default=False)
|
||||
is_adding: BoolProperty(name="Is Adding", default=False)
|
||||
groups: CollectionProperty(name="Groups", type=Group)
|
||||
active_group_index: IntProperty(name="Active Group Index")
|
||||
active_group_id: IntProperty(name="Active Group Id")
|
||||
|
||||
@@ -89,9 +89,9 @@ class BIM_PT_object_groups(Panel):
|
||||
ObjectGroupsData.load()
|
||||
self.props = context.scene.BIMGroupProperties
|
||||
row = self.layout.row(align=True)
|
||||
if self.props.is_adding:
|
||||
if self.props.is_editing:
|
||||
row.label(text="Adding Groups", icon="OUTLINER")
|
||||
row.operator("bim.toggle_assigning_group", text="", icon="CANCEL")
|
||||
row.operator("bim.disable_group_editing_ui", text="", icon="CANCEL")
|
||||
self.layout.template_list(
|
||||
"BIM_UL_object_groups",
|
||||
"",
|
||||
@@ -102,7 +102,7 @@ class BIM_PT_object_groups(Panel):
|
||||
)
|
||||
else:
|
||||
row.label(text=f"{ObjectGroupsData.data['total_groups']} Groups in IFC Project", icon="OUTLINER")
|
||||
row.operator("bim.toggle_assigning_group", text="", icon="ADD")
|
||||
row.operator("bim.load_groups", text="", icon="GREASEPENCIL")
|
||||
|
||||
for group in ObjectGroupsData.data["groups"]:
|
||||
row = self.layout.row(align=True)
|
||||
|
||||
@@ -11,7 +11,7 @@ class IFCGIT_PT_panel(bpy.types.Panel):
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
bl_parent_id = "BIM_PT_project_info"
|
||||
bl_parent_id = "BIM_PT_tab_project_info"
|
||||
|
||||
def draw(self, context):
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ class EnableEditingLayer(bpy.types.Operator):
|
||||
def execute(self, context):
|
||||
props = context.scene.BIMLayerProperties
|
||||
props.layer_attributes.clear()
|
||||
blenderbim.bim.helper.import_attributes(tool.Ifc.get().by_id(self.layer), props.layer_attributes)
|
||||
blenderbim.bim.helper.import_attributes2(tool.Ifc.get().by_id(self.layer), props.layer_attributes)
|
||||
props.active_layer_id = self.layer
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ class BIM_PT_libraries(Panel):
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_project_setup"
|
||||
bl_parent_id = "BIM_PT_tab_project_setup"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
|
||||
@@ -40,10 +40,12 @@ classes = (
|
||||
operator.EditMaterial,
|
||||
operator.EditMaterialSetItem,
|
||||
operator.EditMaterialSetItemProfile,
|
||||
operator.EditMaterialStyle,
|
||||
operator.EnableEditingAssignedMaterial,
|
||||
operator.EnableEditingMaterial,
|
||||
operator.EnableEditingMaterialSetItem,
|
||||
operator.EnableEditingMaterialSetItemProfile,
|
||||
operator.EnableEditingMaterialStyle,
|
||||
operator.ExpandMaterialCategory,
|
||||
operator.LoadMaterials,
|
||||
operator.RemoveConstituent,
|
||||
@@ -55,6 +57,7 @@ classes = (
|
||||
operator.ReorderMaterialSetItem,
|
||||
operator.SelectByMaterial,
|
||||
operator.UnassignMaterial,
|
||||
operator.UnassignMaterialStyle,
|
||||
operator.UnlinkMaterial,
|
||||
prop.Material,
|
||||
prop.BIMMaterialProperties,
|
||||
|
||||
@@ -38,6 +38,9 @@ class MaterialsData:
|
||||
"total_materials": cls.total_materials(),
|
||||
"material_types": cls.material_types(),
|
||||
"profiles": cls.profiles(),
|
||||
"styles": cls.styles(),
|
||||
"contexts": cls.contexts(),
|
||||
"active_styles": cls.active_styles(),
|
||||
}
|
||||
cls.is_loaded = True
|
||||
|
||||
@@ -79,6 +82,58 @@ class MaterialsData:
|
||||
if p.ProfileName
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def contexts(cls):
|
||||
results = []
|
||||
for element in tool.Ifc.get().by_type("IfcGeometricRepresentationContext", include_subtypes=False):
|
||||
results.append((str(element.id()), element.ContextType or "Unnamed", ""))
|
||||
for element in tool.Ifc.get().by_type("IfcGeometricRepresentationSubContext", include_subtypes=False):
|
||||
results.append(
|
||||
(
|
||||
str(element.id()),
|
||||
"{}/{}/{}".format(
|
||||
element.ContextType or "Unnamed",
|
||||
element.ContextIdentifier or "Unnamed",
|
||||
element.TargetView or "Unnamed",
|
||||
),
|
||||
"",
|
||||
)
|
||||
)
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
def styles(cls):
|
||||
return [(str(s.id()), s.Name or "Unnamed", "") for s in tool.Ifc.get().by_type("IfcSurfaceStyle") if s.Name]
|
||||
|
||||
@classmethod
|
||||
def active_styles(cls):
|
||||
props = bpy.context.scene.BIMMaterialProperties
|
||||
results = []
|
||||
if props.materials and props.active_material_index < len(props.materials):
|
||||
material = props.materials[props.active_material_index].ifc_definition_id
|
||||
if not material:
|
||||
return results
|
||||
material = tool.Ifc.get().by_id(material)
|
||||
for definition in material.HasRepresentation:
|
||||
for representation in definition.Representations:
|
||||
if not representation.is_a("IfcStyledRepresentation"):
|
||||
continue
|
||||
context = representation.ContextOfItems
|
||||
for item in representation.Items:
|
||||
if not item.is_a("IfcStyledItem"):
|
||||
continue
|
||||
for style in item.Styles:
|
||||
if style.is_a("IfcSurfaceStyle"):
|
||||
results.append({
|
||||
"context_type": context.ContextType,
|
||||
"context_identifier": getattr(context, "ContextIdentifier", ""),
|
||||
"target_view": getattr(context, "TargetView", ""),
|
||||
"name": style.Name or "Unnamed",
|
||||
"id": style.id(),
|
||||
"context_id": context.id(),
|
||||
})
|
||||
return results
|
||||
|
||||
|
||||
class ObjectMaterialData:
|
||||
data = {}
|
||||
@@ -173,6 +228,8 @@ class ObjectMaterialData:
|
||||
items = cls.material.MaterialProfiles
|
||||
elif cls.material.is_a("IfcMaterialConstituentSet"):
|
||||
items = cls.material.MaterialConstituents
|
||||
elif cls.material.is_a("IfcMaterialList"):
|
||||
items = cls.material.Materials
|
||||
|
||||
icon = "LAYER_ACTIVE"
|
||||
if "Layer" in cls.material.is_a():
|
||||
@@ -191,7 +248,9 @@ class ObjectMaterialData:
|
||||
data["name"] = "No Profile"
|
||||
if item.is_a("IfcMaterialLayer"):
|
||||
data["name"] += f" ({item.LayerThickness})"
|
||||
if not item.is_a("IfcMaterialList"):
|
||||
if item.is_a("IfcMaterial"):
|
||||
data["material"] = item.Name or "Unnamed"
|
||||
else:
|
||||
data["material"] = item.Material.Name or "Unnamed"
|
||||
results.append(data)
|
||||
return results
|
||||
|
||||
@@ -118,10 +118,18 @@ class AddMaterial(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_label = "Add Material"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
name: bpy.props.StringProperty(default="Default")
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def draw(self, context):
|
||||
row = self.layout
|
||||
row.prop(self, "name", text="Name")
|
||||
|
||||
def _execute(self, context):
|
||||
obj = bpy.data.materials.get(self.obj) if self.obj else None
|
||||
core.add_material(tool.Ifc, tool.Material, tool.Style, obj=obj)
|
||||
core.add_material(tool.Ifc, tool.Material, tool.Style, obj=obj, name=self.name)
|
||||
material_prop_purge()
|
||||
|
||||
|
||||
@@ -174,7 +182,7 @@ class AssignMaterial(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
def _execute(self, context):
|
||||
objects = [bpy.data.objects.get(self.obj)] if self.obj else tool.Blender.get_selected_objects()
|
||||
core.assign_material(tool.Ifc, tool.Material, material_type= self.material_type , objects=objects )
|
||||
core.assign_material(tool.Ifc, tool.Material, material_type=self.material_type, objects=objects)
|
||||
|
||||
|
||||
class UnassignMaterial(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -185,7 +193,7 @@ class UnassignMaterial(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
def _execute(self, context):
|
||||
objects = [bpy.data.objects.get(self.obj)] if self.obj else tool.Blender.get_selected_objects()
|
||||
core.unassign_material(tool.Ifc, tool.Material, objects=objects )
|
||||
core.unassign_material(tool.Ifc, tool.Material, objects=objects)
|
||||
|
||||
|
||||
class AddConstituent(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -350,10 +358,8 @@ class AddListItem(bpy.types.Operator, tool.Ifc.Operator):
|
||||
ifcopenshell.api.run(
|
||||
"material.add_list_item",
|
||||
self.file,
|
||||
**{
|
||||
"material_list": self.file.by_id(self.list_item_set),
|
||||
"material": self.file.by_id(int(obj.BIMObjectMaterialProperties.material)),
|
||||
},
|
||||
material_list=self.file.by_id(self.list_item_set),
|
||||
material=self.file.by_id(int(obj.BIMObjectMaterialProperties.material)),
|
||||
)
|
||||
|
||||
|
||||
@@ -709,3 +715,50 @@ class ContractMaterialCategory(bpy.types.Operator):
|
||||
category.is_expanded = False
|
||||
core.load_materials(tool.Material, props.material_type)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class EnableEditingMaterialStyle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.enable_editing_material_style"
|
||||
bl_label = "Enable Editing Material Style"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
material: bpy.props.IntProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
props = bpy.context.scene.BIMMaterialProperties
|
||||
props.active_material_id = self.material
|
||||
props.editing_material_type = "STYLE"
|
||||
|
||||
|
||||
class EditMaterialStyle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.edit_material_style"
|
||||
bl_label = "Edit Material Style"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
props = bpy.context.scene.BIMMaterialProperties
|
||||
material = tool.Ifc.get().by_id(props.active_material_id)
|
||||
style = tool.Ifc.get().by_id(int(props.styles))
|
||||
context = tool.Ifc.get().by_id(int(props.contexts))
|
||||
ifcopenshell.api.run(
|
||||
"style.assign_material_style", tool.Ifc.get(), material=material, style=style, context=context
|
||||
)
|
||||
tool.Material.disable_editing_material()
|
||||
core.load_materials(tool.Material, props.material_type)
|
||||
|
||||
|
||||
class UnassignMaterialStyle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.unassign_material_style"
|
||||
bl_label = "Unassign Material Style"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
style: bpy.props.IntProperty()
|
||||
context: bpy.props.IntProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
props = bpy.context.scene.BIMMaterialProperties
|
||||
material = tool.Ifc.get().by_id(props.materials[props.active_material_index].ifc_definition_id)
|
||||
style = tool.Ifc.get().by_id(self.style)
|
||||
context = tool.Ifc.get().by_id(self.context)
|
||||
ifcopenshell.api.run(
|
||||
"style.unassign_material_style", tool.Ifc.get(), material=material, style=style, context=context
|
||||
)
|
||||
core.load_materials(tool.Material, props.material_type)
|
||||
|
||||
@@ -103,11 +103,24 @@ def get_profiles(self, context):
|
||||
return MaterialsData.data["profiles"]
|
||||
|
||||
|
||||
def get_styles(self, context):
|
||||
if not MaterialsData.is_loaded:
|
||||
MaterialsData.load()
|
||||
return MaterialsData.data["styles"]
|
||||
|
||||
|
||||
def get_contexts(self, context):
|
||||
if not MaterialsData.is_loaded:
|
||||
MaterialsData.load()
|
||||
return MaterialsData.data["contexts"]
|
||||
|
||||
|
||||
class Material(PropertyGroup):
|
||||
name: StringProperty(name="Name")
|
||||
ifc_definition_id: IntProperty(name="IFC Definition ID")
|
||||
is_category: BoolProperty(name="Is Category", default=False)
|
||||
is_expanded: BoolProperty(name="Is Expanded", default=True)
|
||||
has_style: BoolProperty(name="Has Style", default=True)
|
||||
total_elements: IntProperty(name="Total Elements")
|
||||
|
||||
|
||||
@@ -119,7 +132,9 @@ class BIMMaterialProperties(PropertyGroup):
|
||||
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")
|
||||
editing_material_type: StringProperty(name="Editing Material Type")
|
||||
styles: EnumProperty(items=get_styles, name="Styles")
|
||||
contexts: EnumProperty(items=get_contexts, name="Contexts")
|
||||
|
||||
|
||||
class BIMObjectMaterialProperties(PropertyGroup):
|
||||
|
||||
@@ -28,11 +28,11 @@ from blenderbim.bim.module.material.data import MaterialsData, ObjectMaterialDat
|
||||
class BIM_PT_materials(Panel):
|
||||
bl_label = "Materials"
|
||||
bl_idname = "BIM_PT_materials"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_geometry"
|
||||
bl_parent_id = "BIM_PT_tab_materials"
|
||||
bl_options = {"HIDE_HEADER"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@@ -45,11 +45,10 @@ class BIM_PT_materials(Panel):
|
||||
self.props = context.scene.BIMMaterialProperties
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="{} Materials Found".format(MaterialsData.data["total_materials"]), icon="MATERIAL")
|
||||
row.label(text="{} Materials".format(MaterialsData.data["total_materials"]), icon="NODE_MATERIAL")
|
||||
if self.props.is_editing:
|
||||
row.operator("bim.disable_editing_materials", text="", icon="CANCEL")
|
||||
else:
|
||||
row = self.layout.row(align=True)
|
||||
prop_with_search(row, self.props, "material_type", text="")
|
||||
row.operator("bim.load_materials", text="", icon="IMPORT")
|
||||
return
|
||||
@@ -58,20 +57,19 @@ class BIM_PT_materials(Panel):
|
||||
row.alignment = "RIGHT"
|
||||
|
||||
if self.props.material_type == "IfcMaterial":
|
||||
if not self.props.active_material_id:
|
||||
row.operator("bim.add_material", text="", icon="ADD")
|
||||
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:
|
||||
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:
|
||||
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
|
||||
op = row.operator("bim.select_by_material", text="", icon="RESTRICT_SELECT_OFF")
|
||||
op.material = material.ifc_definition_id
|
||||
op = row.operator("bim.enable_editing_material", text="", icon="GREASEPENCIL")
|
||||
op.material = material.ifc_definition_id
|
||||
op = row.operator("bim.enable_editing_material_style", text="", icon="SHADING_RENDERED")
|
||||
op.material = material.ifc_definition_id
|
||||
row.operator("bim.remove_material", text="", icon="X").material = material.ifc_definition_id
|
||||
|
||||
self.draw_editing_ui()
|
||||
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):
|
||||
@@ -83,9 +81,34 @@ class BIM_PT_materials(Panel):
|
||||
|
||||
self.layout.template_list("BIM_UL_materials", "", self.props, "materials", self.props, "active_material_index")
|
||||
|
||||
for style in MaterialsData.data["active_styles"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="", icon="SHADING_RENDERED")
|
||||
row.label(text=style["context_type"])
|
||||
row.label(text=style["context_identifier"])
|
||||
row.label(text=style["target_view"])
|
||||
row.label(text=style["name"])
|
||||
op = row.operator("bim.unassign_material_style", text="", icon="X")
|
||||
op.style = style["id"]
|
||||
op.context = style["context_id"]
|
||||
|
||||
def draw_editing_ui(self):
|
||||
if not self.props.active_material_id:
|
||||
return
|
||||
ifc_definition_id = self.props.active_material_id
|
||||
if self.props.editing_material_type == "ATTRIBUTES":
|
||||
blenderbim.bim.helper.draw_attributes(self.props.material_attributes, self.layout)
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.edit_material", text="Save Material", icon="CHECKMARK").material = ifc_definition_id
|
||||
row.operator("bim.disable_editing_material", text="", icon="CANCEL")
|
||||
elif self.props.editing_material_type == "STYLE":
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.props, "contexts", text="")
|
||||
row.prop(self.props, "styles", text="")
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.edit_material_style", text="Assign Style", icon="CHECKMARK")
|
||||
row.operator("bim.disable_editing_material", text="", icon="CANCEL")
|
||||
|
||||
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 = "Material"
|
||||
@@ -220,9 +243,7 @@ class BIM_PT_object_material(Panel):
|
||||
elif self.props.active_material_set_item_id == set_item["id"]:
|
||||
self.draw_editable_set_item_ui(set_item)
|
||||
else:
|
||||
self.draw_read_only_set_item_ui(
|
||||
set_item, index, is_first=index == 0, is_last=index == total_items - 1
|
||||
)
|
||||
self.draw_read_only_set_item_ui(set_item, index, is_first=index == 0, is_last=index == total_items - 1)
|
||||
|
||||
def draw_editable_set_item_profile_ui(self, set_item):
|
||||
box = self.layout.box()
|
||||
@@ -268,7 +289,10 @@ class BIM_PT_object_material(Panel):
|
||||
op.old_index = index
|
||||
op.new_index = index + 1
|
||||
setattr(op, "material_set", ObjectMaterialData.data["set"]["id"])
|
||||
if not self.props.active_material_set_item_id and ObjectMaterialData.data["material_class"] != "IfcMaterialList":
|
||||
if (
|
||||
not self.props.active_material_set_item_id
|
||||
and ObjectMaterialData.data["material_class"] != "IfcMaterialList"
|
||||
):
|
||||
if "Profile" in ObjectMaterialData.data["material_class"]:
|
||||
op = row.operator("bim.enable_editing_material_set_item_profile", icon="ITALIC", text="")
|
||||
op.material_set_item = set_item["id"]
|
||||
@@ -284,9 +308,11 @@ class BIM_PT_object_material(Panel):
|
||||
setattr(op, f"{ObjectMaterialData.data['set_item_name']}_index", index)
|
||||
|
||||
def draw_read_only_set_ui(self):
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Name")
|
||||
row.label(text=ObjectMaterialData.data["set"]["name"])
|
||||
if ObjectMaterialData.data["material_class"] != "IfcMaterialList":
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Name")
|
||||
row.label(text=ObjectMaterialData.data["set"]["name"])
|
||||
|
||||
if ObjectMaterialData.data["set"]["description"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Description")
|
||||
@@ -335,3 +361,6 @@ class BIM_UL_materials(UIList):
|
||||
row2 = row.row()
|
||||
row2.alignment = "RIGHT"
|
||||
row2.label(text=str(item.total_elements))
|
||||
|
||||
if item.has_style:
|
||||
row2.label(text="", icon="SHADING_RENDERED")
|
||||
|
||||
@@ -22,6 +22,7 @@ import ifcopenshell
|
||||
import blenderbim.bim.handler
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.core.misc as core
|
||||
import blenderbim.core.geometry as core_geometry
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from mathutils import Vector, Matrix, Euler
|
||||
|
||||
@@ -140,7 +141,51 @@ class SplitAlongEdge(bpy.types.Operator, Operator):
|
||||
return context.selected_objects and tool.Ifc.get()
|
||||
|
||||
def _execute(self, context):
|
||||
core.split_along_edge(tool.Misc, cutter=context.active_object, objs=context.selected_objects)
|
||||
cutter = context.active_object
|
||||
objs = [o for o in context.selected_objects if o != cutter]
|
||||
|
||||
# Splitting only works on meshes
|
||||
for obj in objs:
|
||||
# You cannot split meshes if the representation is mapped.
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element:
|
||||
relating_type = tool.Root.get_element_type(element)
|
||||
if relating_type and tool.Root.does_type_have_representations(relating_type):
|
||||
bpy.ops.bim.unassign_type(related_object=obj.name)
|
||||
|
||||
representation = tool.Geometry.get_active_representation(obj)
|
||||
core_geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
apply_openings=False,
|
||||
)
|
||||
|
||||
if not tool.Geometry.is_meshlike(representation):
|
||||
bpy.ops.bim.update_representation(obj=obj.name, ifc_representation_class="IfcTessellatedFaceSet")
|
||||
|
||||
new_objs = tool.Misc.split_objects_with_cutter(objs, cutter)
|
||||
for obj in new_objs:
|
||||
blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=obj)
|
||||
bpy.ops.bim.update_representation(obj=obj.name)
|
||||
for obj in objs:
|
||||
bpy.ops.bim.update_representation(obj=obj.name)
|
||||
|
||||
representation = tool.Geometry.get_active_representation(obj)
|
||||
core_geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
apply_openings=True,
|
||||
)
|
||||
|
||||
|
||||
class GetConnectedSystemElements(bpy.types.Operator, Operator):
|
||||
@@ -205,46 +250,50 @@ class DrawSystemArrows(bpy.types.Operator, Operator):
|
||||
return context.selected_objects and tool.Ifc.get()
|
||||
|
||||
def _execute(self, context):
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
curve = bpy.data.objects.new("System Arrows", bpy.data.curves.new("System Arrows", "CURVE"))
|
||||
curve.data.dimensions = "3D"
|
||||
context.scene.collection.objects.link(curve)
|
||||
sinks = []
|
||||
sources = []
|
||||
|
||||
for obj in bpy.context.selected_objects:
|
||||
if not obj.BIMObjectProperties.ifc_definition_id:
|
||||
continue
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
sources = []
|
||||
sinks = []
|
||||
for rel in getattr(element, "HasPorts", []) or []:
|
||||
if rel.RelatingPort.FlowDirection == "SOURCE":
|
||||
sources.append(
|
||||
self.get_absolute_matrix(
|
||||
ifcopenshell.util.placement.get_local_placement(rel.RelatingPort.ObjectPlacement)
|
||||
)
|
||||
)
|
||||
elif rel.RelatingPort.FlowDirection == "SINK":
|
||||
sinks.append(
|
||||
self.get_absolute_matrix(
|
||||
ifcopenshell.util.placement.get_local_placement(rel.RelatingPort.ObjectPlacement)
|
||||
)
|
||||
)
|
||||
sources_current = []
|
||||
sinks_current = []
|
||||
|
||||
for port in tool.System.get_ports(element):
|
||||
local_placement = ifcopenshell.util.placement.get_local_placement(port.ObjectPlacement)
|
||||
m = self.get_absolute_matrix(local_placement)
|
||||
if port.FlowDirection == "SOURCE":
|
||||
sources_current.append(m)
|
||||
elif port.FlowDirection == "SINK":
|
||||
sinks_current.append(m)
|
||||
else:
|
||||
sources.append(
|
||||
self.get_absolute_matrix(
|
||||
ifcopenshell.util.placement.get_local_placement(rel.RelatingPort.ObjectPlacement)
|
||||
)
|
||||
)
|
||||
sinks.append(
|
||||
self.get_absolute_matrix(
|
||||
ifcopenshell.util.placement.get_local_placement(rel.RelatingPort.ObjectPlacement)
|
||||
)
|
||||
)
|
||||
for sink in sinks:
|
||||
for source in sources:
|
||||
sources_current.append(m)
|
||||
sinks_current.append(m)
|
||||
|
||||
if sinks_current or sources_current:
|
||||
sinks.append(sinks_current)
|
||||
sources.append(sources_current)
|
||||
|
||||
if not sinks:
|
||||
self.report({"INFO"}, "No sinks/sources found for selected objects.")
|
||||
return {"FINISHED"}
|
||||
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
curve = bpy.data.objects.new("System Arrows", bpy.data.curves.new("System Arrows", "CURVE"))
|
||||
curve.data.dimensions = "3D"
|
||||
curve.show_in_front = True
|
||||
context.scene.collection.objects.link(curve)
|
||||
|
||||
for i in range(len(sinks)):
|
||||
for sink in sinks[i]:
|
||||
for source in sources[i]:
|
||||
polyline = curve.data.splines.new("POLY")
|
||||
polyline.points.add(1)
|
||||
polyline.points[0].co = (Matrix(sink).translation * unit_scale).to_4d()
|
||||
polyline.points[1].co = (Matrix(source).translation * unit_scale).to_4d()
|
||||
tool.Blender.select_and_activate_single_object(context, curve)
|
||||
|
||||
def get_absolute_matrix(self, matrix):
|
||||
props = bpy.context.scene.BIMGeoreferenceProperties
|
||||
|
||||
@@ -28,6 +28,7 @@ from . import (
|
||||
roof,
|
||||
slab,
|
||||
space,
|
||||
covering,
|
||||
stair,
|
||||
window,
|
||||
opening,
|
||||
@@ -106,6 +107,11 @@ classes = (
|
||||
slab.SetArcIndex,
|
||||
space.GenerateSpace,
|
||||
space.GenerateSpacesFromWalls,
|
||||
covering.AddInstanceFlooringCoveringsFromWalls,
|
||||
covering.AddInstanceCeilingCoveringsFromWalls,
|
||||
covering.AddInstanceFlooringCoveringFromCursor,
|
||||
covering.AddInstanceCeilingCoveringFromCursor,
|
||||
covering.RegenSelectedCoveringObject,
|
||||
space.ToggleSpaceVisibility,
|
||||
mep.FitFlowSegments,
|
||||
mep.RegenerateDistributionElement,
|
||||
|
||||
@@ -59,7 +59,7 @@ class AddArray(bpy.types.Operator, tool.Ifc.Operator):
|
||||
"pset.edit_pset",
|
||||
tool.Ifc.get(),
|
||||
pset=pset,
|
||||
properties={"Parent": element.GlobalId, "Data": json.dumps(data)},
|
||||
properties={"Parent": element.GlobalId, "Data": tool.Ifc.get().createIfcText(json.dumps(data))},
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -132,7 +132,7 @@ class EditArray(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tool.Model.regenerate_array(parent, data)
|
||||
|
||||
pset = tool.Ifc.get().by_id(pset["id"])
|
||||
data = json.dumps(data)
|
||||
data = tool.Ifc.get().createIfcText(json.dumps(data))
|
||||
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Data": data})
|
||||
|
||||
tool.Blender.Modifier.Array.set_children_lock_state(element, self.item, True)
|
||||
@@ -169,7 +169,6 @@ class RemoveArray(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array")
|
||||
data = json.loads(pset["Data"])
|
||||
data[self.item]["count"] = 1
|
||||
|
||||
if (self.keep_objs) & (self.item < (len(data) - 1)):
|
||||
self.report(
|
||||
@@ -188,6 +187,8 @@ class RemoveArray(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tool.Blender.Modifier.Array.bake_children_transform(element, self.item)
|
||||
tool.Blender.Modifier.Array.set_children_lock_state(element, self.item, False)
|
||||
|
||||
if not self.keep_objs:
|
||||
data[self.item]["count"] = 1
|
||||
tool.Model.regenerate_array(parent, data, self.keep_objs)
|
||||
|
||||
pset = tool.Ifc.get().by_id(pset["id"])
|
||||
@@ -195,7 +196,7 @@ class RemoveArray(bpy.types.Operator, tool.Ifc.Operator):
|
||||
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
|
||||
else:
|
||||
del data[self.item]
|
||||
data = json.dumps(data)
|
||||
data = tool.Ifc.get().createIfcText(json.dumps(data))
|
||||
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Data": data})
|
||||
tool.Blender.Modifier.Array.constrain_children_to_parent(element)
|
||||
|
||||
@@ -206,15 +207,29 @@ class SelectArrayParent(bpy.types.Operator):
|
||||
bl_idname = "bim.select_array_parent"
|
||||
bl_label = "Select Array Parent"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
parent: bpy.props.StringProperty(description="Parent Element GUID")
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not context.active_object:
|
||||
cls.poll_message_set("No active object selected")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
try:
|
||||
element = tool.Ifc.get().by_guid(self.parent)
|
||||
except:
|
||||
self.report({"ERROR"}, f"Couldn't find array parent by guid '{self.parent}'")
|
||||
object = context.active_object
|
||||
element = tool.Ifc.get_entity(object)
|
||||
array_pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array")
|
||||
if not array_pset:
|
||||
self.report({"ERROR"}, f"Object is not part of an array.")
|
||||
return {"CANCELLED"}
|
||||
obj = tool.Ifc.get_object(element)
|
||||
|
||||
try:
|
||||
parent_element = tool.Ifc.get().by_guid(array_pset["Parent"])
|
||||
except:
|
||||
self.report({"ERROR"}, f"Couldn't find array parent by guid '{array_pset['Parent']}'")
|
||||
return {"CANCELLED"}
|
||||
|
||||
obj = tool.Ifc.get_object(parent_element)
|
||||
if obj:
|
||||
tool.Blender.select_and_activate_single_object(context, active_object=obj)
|
||||
return {"FINISHED"}
|
||||
@@ -224,17 +239,39 @@ class SelectAllArrayObjects(bpy.types.Operator):
|
||||
bl_idname = "bim.select_all_array_objects"
|
||||
bl_label = "Select All Array Objects"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
parent: bpy.props.StringProperty(description="Parent Element GUID")
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not context.active_object:
|
||||
cls.poll_message_set("No active object selected")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
try:
|
||||
parent_element = tool.Ifc.get().by_guid(self.parent)
|
||||
except RuntimeError:
|
||||
self.report({"ERROR"}, f"Couldn't find array parent by guid '{self.parent}'")
|
||||
return {"CANCELLED"}
|
||||
objects = context.selected_objects
|
||||
for object in objects:
|
||||
element = tool.Ifc.get_entity(object)
|
||||
if not element:
|
||||
self.report({"ERROR"}, f"Non IFC objects, were deselected.")
|
||||
object.select_set(False)
|
||||
|
||||
array_objects = tool.Blender.Modifier.Array.get_all_objects(parent_element)
|
||||
tool.Blender.set_objects_selection(context, active_object=array_objects[0], selected_objects=array_objects)
|
||||
if element:
|
||||
array_pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array")
|
||||
if not array_pset:
|
||||
self.report({"ERROR"}, f"Objects not part of an array, were deselected.")
|
||||
object.select_set(False)
|
||||
|
||||
if array_pset:
|
||||
try:
|
||||
parent_element = tool.Ifc.get().by_guid(array_pset["Parent"])
|
||||
except RuntimeError:
|
||||
self.report({"ERROR"}, f"Objects that don't have an array parent, were deselected.")
|
||||
object.select_set(False)
|
||||
|
||||
array_objects = tool.Blender.Modifier.Array.get_all_objects(parent_element)
|
||||
tool.Blender.set_objects_selection(
|
||||
context, active_object=array_objects[0], selected_objects=array_objects, clear_previous_selection=False
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
# 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 bpy
|
||||
import ifcopenshell
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.core.covering as core
|
||||
|
||||
class AddInstanceFlooringCoveringFromCursor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_instance_flooring_covering_from_cursor"
|
||||
bl_label = "Add Flooring From Cursor"
|
||||
bl_options = {"REGISTER"}
|
||||
bl_description = "Add a typed instance flooring covering from 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 = collection.BIMCollectionProperties.obj
|
||||
return tool.Ifc.get_entity(collection_obj)
|
||||
|
||||
def _execute(self, context):
|
||||
|
||||
def msg(self, context):
|
||||
self.layout.label(text="NO ACTIVE STOREY")
|
||||
|
||||
collection = context.view_layer.active_layer_collection.collection
|
||||
collection_obj = collection.BIMCollectionProperties.obj
|
||||
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
|
||||
|
||||
core.add_instance_flooring_covering_from_cursor(tool.Ifc, tool.Spatial, tool.Model, tool.Type, tool.Geometry)
|
||||
|
||||
class AddInstanceCeilingCoveringFromCursor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_instance_ceiling_covering_from_cursor"
|
||||
bl_label = "Add Ceiling From Cursor"
|
||||
bl_options = {"REGISTER"}
|
||||
bl_description = "Add a typed instance ceiling covering from 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 = collection.BIMCollectionProperties.obj
|
||||
return tool.Ifc.get_entity(collection_obj)
|
||||
|
||||
def _execute(self, context):
|
||||
|
||||
def msg(self, context):
|
||||
self.layout.label(text="NO ACTIVE STOREY")
|
||||
|
||||
collection = context.view_layer.active_layer_collection.collection
|
||||
collection_obj = collection.BIMCollectionProperties.obj
|
||||
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
|
||||
|
||||
core.add_instance_ceiling_covering_from_cursor(tool.Ifc, tool.Spatial, tool.Model, tool.Type, tool.Geometry, tool.Covering)
|
||||
|
||||
class RegenSelectedCoveringObject(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.regen_selected_covering_object"
|
||||
bl_label = "Regen"
|
||||
bl_options = {"REGISTER"}
|
||||
bl_description = "Regen selected covering object"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
active_obj = bpy.context.active_object
|
||||
element = tool.Ifc.get_entity(active_obj)
|
||||
return element and element.is_a("IfcCovering")
|
||||
|
||||
def _execute(self, context):
|
||||
|
||||
def msg(self, context):
|
||||
self.layout.label(text="NO ACTIVE STOREY")
|
||||
|
||||
active_obj = bpy.context.active_object
|
||||
element = tool.Ifc.get_entity(active_obj)
|
||||
if not element.is_a("IfcCovering"):
|
||||
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
|
||||
return
|
||||
|
||||
core.regen_selected_covering_object(tool.Ifc, tool.Spatial, tool.Model, tool.Type, tool.Geometry)
|
||||
|
||||
|
||||
class AddInstanceFlooringCoveringsFromWalls(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_instance_flooring_coverings_from_walls"
|
||||
bl_label = "Add Flooring From Walls"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Add instance flooring coverings from selected walls. The active object must be a wall and layered vertically"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
active_obj = bpy.context.active_object
|
||||
element = tool.Ifc.get_entity(active_obj)
|
||||
if element:
|
||||
if element.is_a("IfcWall") and tool.Model.get_usage_type(element) == "LAYER2":
|
||||
return context.selected_objects
|
||||
|
||||
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 object must be a wall and
|
||||
# there must be selected walls
|
||||
|
||||
active_obj = bpy.context.active_object
|
||||
if not active_obj:
|
||||
self.report({"ERROR"}, "No active object. Please select a wall")
|
||||
return
|
||||
|
||||
element = tool.Ifc.get_entity(active_obj)
|
||||
if element and not element.is_a("IfcWall"):
|
||||
return self.report({"ERROR"}, "The active object is not a wall. Please select a wall.")
|
||||
|
||||
container = ifcopenshell.util.element.get_container(element)
|
||||
if not container:
|
||||
self.report({"ERROR"}, "The wall is not contained.")
|
||||
|
||||
if not bpy.context.selected_objects:
|
||||
self.report({"ERROR"}, "No selected objects found. Please select walls.")
|
||||
return
|
||||
|
||||
core.add_instance_flooring_coverings_from_walls(tool.Ifc, tool.Spatial, tool.Collector, tool.Geometry)
|
||||
|
||||
class AddInstanceCeilingCoveringsFromWalls(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_instance_ceiling_coverings_from_walls"
|
||||
bl_label = "Add Ceilings From Walls"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Add instance ceiling coverings from selected walls. The active object must be a wall and layered vertically"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
active_obj = bpy.context.active_object
|
||||
element = tool.Ifc.get_entity(active_obj)
|
||||
if element:
|
||||
if element.is_a("IfcWall") and tool.Model.get_usage_type(element) == "LAYER2":
|
||||
return context.selected_objects
|
||||
|
||||
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 object must be a wall and
|
||||
# there must be selected walls
|
||||
|
||||
active_obj = bpy.context.active_object
|
||||
if not active_obj:
|
||||
self.report({"ERROR"}, "No active object. Please select a wall")
|
||||
return
|
||||
|
||||
element = tool.Ifc.get_entity(active_obj)
|
||||
if element and not element.is_a("IfcWall"):
|
||||
return self.report({"ERROR"}, "The active object is not a wall. Please select a wall.")
|
||||
|
||||
container = ifcopenshell.util.element.get_container(element)
|
||||
if not container:
|
||||
self.report({"ERROR"}, "The wall is not contained.")
|
||||
|
||||
if not bpy.context.selected_objects:
|
||||
self.report({"ERROR"}, "No selected objects found. Please select walls.")
|
||||
return
|
||||
|
||||
core.add_instance_ceiling_coverings_from_walls(tool.Ifc, tool.Spatial, tool.Collector, tool.Geometry, tool.Covering)
|
||||
|
||||
@@ -53,6 +53,7 @@ class AuthoringData:
|
||||
cls.data["ifc_element_type"] = cls.ifc_element_type
|
||||
cls.data["ifc_classes"] = cls.ifc_classes()
|
||||
cls.data["relating_type_id"] = cls.relating_type_id() # only after .ifc_classes()
|
||||
cls.data["predefined_type"] = cls.predefined_type()
|
||||
cls.data["type_class"] = cls.type_class()
|
||||
|
||||
# only after .type_class()
|
||||
@@ -212,6 +213,7 @@ class AuthoringData:
|
||||
if bpy.context.active_object:
|
||||
representation = tool.Geometry.get_active_representation(bpy.context.active_object)
|
||||
if representation and representation.is_a("IfcShapeRepresentation"):
|
||||
representation = tool.Geometry.resolve_mapped_representation(representation)
|
||||
return representation.RepresentationType
|
||||
|
||||
@classmethod
|
||||
@@ -247,6 +249,15 @@ class AuthoringData:
|
||||
return [(str(e.id()), e.Name or "Unnamed", e.Description or "") for e in results]
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def predefined_type(cls):
|
||||
relating_type_id = cls.props.relating_type_id
|
||||
if not relating_type_id:
|
||||
return
|
||||
relating_type = tool.Ifc.get().by_id(int(relating_type_id))
|
||||
predefined_type = relating_type.PredefinedType
|
||||
return predefined_type
|
||||
|
||||
@classmethod
|
||||
def selected_material_usages(cls):
|
||||
selected_usages = {}
|
||||
@@ -303,6 +314,7 @@ class StairData:
|
||||
if not cls.data["pset_data"]:
|
||||
return
|
||||
cls.data["general_params"] = cls.general_params()
|
||||
cls.data["calculated_params"] = cls.calculated_params()
|
||||
|
||||
@classmethod
|
||||
def pset_data(cls):
|
||||
@@ -319,6 +331,10 @@ class StairData:
|
||||
general_params[prop_readable_name] = prop_value
|
||||
return general_params
|
||||
|
||||
@classmethod
|
||||
def calculated_params(cls):
|
||||
return tool.Model.get_active_stair_calculated_params(cls.data["pset_data"]["data_dict"])
|
||||
|
||||
|
||||
class SverchokData:
|
||||
data = {}
|
||||
@@ -345,7 +361,7 @@ class SverchokData:
|
||||
|
||||
|
||||
def get_prop_from_data(props, data, prop_name):
|
||||
prop_value = data[prop_name]
|
||||
prop_value = data.get(prop_name, tool.Blender.get_blender_prop_default_value(props, prop_name))
|
||||
prop_value = round(prop_value, 5) if type(prop_value) is float else prop_value
|
||||
prop_readable_name = props.bl_rna.properties[prop_name].name
|
||||
return prop_readable_name, prop_value
|
||||
|
||||
@@ -191,15 +191,15 @@ 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")
|
||||
# POLYLINE_UNIFORM_COLOR is good for smoothed lines since `bgl.enable(GL_LINE_SMOOTH)` is deprecated
|
||||
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
|
||||
self.line_shader.bind()
|
||||
# POLYLINE_UNIFORM_COLOR specific uniforms
|
||||
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
|
||||
self.line_shader.uniform_float("lineWidth", 2.0)
|
||||
|
||||
# general shader
|
||||
self.shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
|
||||
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
self.shader.bind()
|
||||
|
||||
# Draw faces
|
||||
|
||||
@@ -68,6 +68,8 @@ def update_door_modifier_representation(context, obj):
|
||||
},
|
||||
}
|
||||
|
||||
previously_active_context = tool.Geometry.get_active_representation_context(obj)
|
||||
|
||||
# ELEVATION_VIEW representation
|
||||
profile = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Profile", "ELEVATION_VIEW")
|
||||
if profile:
|
||||
@@ -78,6 +80,7 @@ def update_door_modifier_representation(context, obj):
|
||||
tool.Model.replace_object_ifc_representation(profile, obj, elevation_representation)
|
||||
|
||||
# MODEL_VIEW representation
|
||||
# (Model/Body defined only BEFORE Plan/Body to prevent #2744)
|
||||
body = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
|
||||
representation_data["context"] = body
|
||||
model_representation = ifcopenshell.api.run("geometry.add_door_representation", ifc_file, **representation_data)
|
||||
@@ -113,14 +116,34 @@ def update_door_modifier_representation(context, obj):
|
||||
)
|
||||
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(
|
||||
# adding switch representation at the end instead of changing order of representations
|
||||
# to prevent #2744
|
||||
if tool.Geometry.get_active_representation_context(obj) != previously_active_context:
|
||||
previously_active_representation = ifcopenshell.util.representation.get_representation(
|
||||
element,
|
||||
previously_active_context.ContextType,
|
||||
previously_active_context.ContextIdentifier,
|
||||
previously_active_context.TargetView,
|
||||
)
|
||||
|
||||
if not previously_active_representation:
|
||||
# we assume there is no representation because it was
|
||||
# Plan/Annotation/PLAN_VIEW
|
||||
previously_active_context = ifcopenshell.util.representation.get_context(
|
||||
ifc_file, "Plan", "Body", "PLAN_VIEW"
|
||||
)
|
||||
previously_active_representation = ifcopenshell.util.representation.get_representation(
|
||||
element,
|
||||
previously_active_context.ContextType,
|
||||
previously_active_context.ContextIdentifier,
|
||||
previously_active_context.TargetView,
|
||||
)
|
||||
|
||||
blenderbim.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=model_representation,
|
||||
representation=previously_active_representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=True,
|
||||
@@ -526,7 +549,7 @@ class AddDoor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
"pset.edit_pset",
|
||||
tool.Ifc.get(),
|
||||
pset=pset,
|
||||
properties={"Data": json.dumps(door_data, default=list)},
|
||||
properties={"Data": tool.Ifc.get().createIfcText(json.dumps(door_data, default=list))},
|
||||
)
|
||||
update_door_modifier_representation(context, obj)
|
||||
return {"FINISHED"}
|
||||
@@ -585,7 +608,7 @@ class FinishEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
update_door_modifier_representation(context, obj)
|
||||
|
||||
pset = tool.Pset.get_element_pset(element, "BBIM_Door")
|
||||
door_data = json.dumps(door_data, default=list)
|
||||
door_data = tool.Ifc.get().createIfcText(json.dumps(door_data, default=list))
|
||||
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Data": door_data})
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -18,12 +18,10 @@
|
||||
|
||||
import bpy
|
||||
import ifcopenshell.api
|
||||
import blenderbim.core.spatial
|
||||
import blenderbim.tool as tool
|
||||
from bpy.types import Operator
|
||||
from bpy.props import FloatProperty, IntProperty
|
||||
from mathutils import Vector
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
|
||||
|
||||
def add_object(self, context):
|
||||
@@ -66,7 +64,7 @@ def add_object(self, context):
|
||||
tool.Ifc.get(),
|
||||
**{"axis_tag": tag, "uvw_axes": "UAxes", "grid": grid},
|
||||
)
|
||||
IfcStore.link_element(result, obj)
|
||||
tool.Ifc.link(result, obj)
|
||||
ifcopenshell.api.run("grid.create_axis_curve", tool.Ifc.get(), **{"axis_curve": obj, "grid_axis": result})
|
||||
obj.BIMObjectProperties.ifc_definition_id = result.id()
|
||||
|
||||
@@ -91,7 +89,7 @@ def add_object(self, context):
|
||||
tool.Ifc.get(),
|
||||
**{"axis_tag": tag, "uvw_axes": "VAxes", "grid": grid},
|
||||
)
|
||||
IfcStore.link_element(result, obj)
|
||||
tool.Ifc.link(result, obj)
|
||||
ifcopenshell.api.run("grid.create_axis_curve", tool.Ifc.get(), **{"axis_curve": obj, "grid_axis": result})
|
||||
obj.BIMObjectProperties.ifc_definition_id = result.id()
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ import blenderbim.core.type
|
||||
import blenderbim.core.root
|
||||
import blenderbim.core.geometry
|
||||
import blenderbim.tool as tool
|
||||
from math import pi, degrees, radians, sin, cos, asin
|
||||
from math import pi, degrees, radians, sin, cos, asin, tan
|
||||
from copy import copy
|
||||
from mathutils import Vector, Matrix
|
||||
from ifcopenshell.util.shape_builder import ShapeBuilder
|
||||
@@ -366,6 +366,8 @@ class MEPGenerator:
|
||||
ifc_file = tool.Ifc.get()
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
|
||||
precision = VTX_PRECISION / si_conversion
|
||||
angle_precision = degrees(precision)
|
||||
start_port_match = True
|
||||
|
||||
segments_data = []
|
||||
for segment, port in zip(segments, ports, strict=True):
|
||||
@@ -375,8 +377,9 @@ class MEPGenerator:
|
||||
return
|
||||
segments_data.append((segment_type, port.PredefinedType, port.SystemType))
|
||||
|
||||
# TODO: test it with flipped transition where start length != end length
|
||||
def compatible_with_bbim_data(fitting_type):
|
||||
nonlocal start_port_match
|
||||
start_port_match = True
|
||||
if not bbim_data:
|
||||
return True
|
||||
fitting_type_obj = tool.Ifc.get_object(fitting_type)
|
||||
@@ -385,15 +388,39 @@ class MEPGenerator:
|
||||
return False
|
||||
|
||||
fitting_bbim_data = fitting_bbim_data["data_dict"]
|
||||
for key in bbim_data:
|
||||
|
||||
def compare_value(key, second_key=None):
|
||||
second_key = second_key or key
|
||||
requested_value = bbim_data[key]
|
||||
fitting_value = fitting_bbim_data[key]
|
||||
fitting_value = fitting_bbim_data[second_key]
|
||||
|
||||
if isinstance(requested_value, float):
|
||||
compare_precision = None if key == "angle" else precision
|
||||
compare_precision = angle_precision if key == "angle" else precision
|
||||
compare = tool.Cad.is_x(requested_value, fitting_value, compare_precision)
|
||||
elif isinstance(fitting_value, list):
|
||||
compare = tool.Cad.are_vectors_equal(requested_value, Vector(fitting_value), precision)
|
||||
if not compare:
|
||||
return compare
|
||||
|
||||
ignore_keys = []
|
||||
if predefined_type == "BEND":
|
||||
ignore_keys.extend(("start_length", "end_length"))
|
||||
# for bends there is a special case when lengths might not match
|
||||
# but fitting is still compatible if we flip it
|
||||
# since bend connects segments of the same type
|
||||
default_lengths_match = compare_value("start_length") and compare_value("end_length")
|
||||
if not default_lengths_match:
|
||||
switched_lengths_match = compare_value("start_length", "end_length") and compare_value(
|
||||
"end_length", "start_length"
|
||||
)
|
||||
if switched_lengths_match:
|
||||
start_port_match = False
|
||||
else:
|
||||
return False
|
||||
|
||||
for key in bbim_data:
|
||||
if key in ignore_keys:
|
||||
continue
|
||||
if not compare_value(key):
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -424,11 +451,12 @@ class MEPGenerator:
|
||||
# NOTE: I have a feeling that there are cases where order
|
||||
# in which we're checking the segments is important
|
||||
# but I couldn't pin it down to exact cases
|
||||
for test_segment_data in fitting_data[:]:
|
||||
for test_segment_data in fitting_data:
|
||||
for base_segment_data in segments_data:
|
||||
if not are_segments_compatible(test_segment_data, base_segment_data):
|
||||
continue
|
||||
segments_data.remove(test_segment_data)
|
||||
segments_data.remove(base_segment_data)
|
||||
break
|
||||
|
||||
# all segments were sorted
|
||||
return len(segments_data) == 0
|
||||
@@ -448,7 +476,7 @@ class MEPGenerator:
|
||||
connected_port = tool.System.get_connected_port(start_port)
|
||||
connected_element = tool.System.get_port_relating_element(connected_port)
|
||||
element_type = ifcopenshell.util.element.get_type(connected_element)
|
||||
packed_data["start_port_match"] = element_type == segments_data[0][0]
|
||||
packed_data["start_port_match"] = element_type == segments_data[0][0] and start_port_match
|
||||
|
||||
return packed_data
|
||||
|
||||
@@ -559,6 +587,8 @@ class MEPGenerator:
|
||||
obstruction_obj.matrix_world = segment_matrix
|
||||
|
||||
profile_joiner.set_depth(obstruction_obj, length)
|
||||
# NOTE: we add ports to the obstruction occurence and not to the type
|
||||
# since it's material profile based like segments
|
||||
obstruction_port = tool.System.add_ports(
|
||||
obstruction_obj,
|
||||
add_start_port=not at_segment_start,
|
||||
@@ -624,13 +654,13 @@ class MEPAddTransition(bpy.types.Operator, tool.Ifc.Operator):
|
||||
)
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
start_length: bpy.props.FloatProperty(
|
||||
name="Start Length", description="Transition start length in SI units", default=0.1, subtype="DISTANCE"
|
||||
name="Start Length", description="Transition start length in SI units", default=0.1, subtype="DISTANCE", min=0
|
||||
)
|
||||
end_length: bpy.props.FloatProperty(
|
||||
name="End Length", description="Transition end length in SI units", default=0.1, subtype="DISTANCE"
|
||||
name="End Length", description="Transition end length in SI units", default=0.1, subtype="DISTANCE", min=0
|
||||
)
|
||||
angle: bpy.props.FloatProperty(
|
||||
name="Transition Angle", description="Transition angle in degrees", default=pi / 6, subtype="ANGLE"
|
||||
name="Transition Angle", description="Transition angle in degrees", default=pi / 6, subtype="ANGLE", min=0
|
||||
)
|
||||
start_segment_id: bpy.props.IntProperty(name="Start Segment Element ID", default=0)
|
||||
end_segment_id: bpy.props.IntProperty(name="End Segment Element ID", default=0)
|
||||
@@ -715,6 +745,12 @@ class MEPAddTransition(bpy.types.Operator, tool.Ifc.Operator):
|
||||
)
|
||||
start_port = points_ports_map[start_point]
|
||||
end_port = points_ports_map[end_point]
|
||||
start_point_on_origin = start_point == start_segment_data["start_point"]
|
||||
start_connection = "ATSTART" if start_point_on_origin else "ATEND"
|
||||
start_segment_sign = -1 if start_point_on_origin else 1
|
||||
|
||||
end_point_on_origin = end_point == end_segment_data["start_point"]
|
||||
end_connection = "ATSTART" if end_point_on_origin else "ATEND"
|
||||
|
||||
# figure profile offset
|
||||
base_transition_dir = keep_only_z_axis(end_point - start_point).normalized()
|
||||
@@ -732,7 +768,8 @@ class MEPAddTransition(bpy.types.Operator, tool.Ifc.Operator):
|
||||
profile_offset *= V(1, -1)
|
||||
|
||||
# world space profile offset
|
||||
profile_offset_ws = start_object_rotation @ (profile_offset * si_conversion).to_3d()
|
||||
profile_offset_si = (profile_offset * si_conversion).to_3d()
|
||||
profile_offset_ws = start_object_rotation @ profile_offset_si
|
||||
|
||||
def get_segments_length():
|
||||
start_dir = (start_point - first_segment_start).normalized()
|
||||
@@ -777,24 +814,16 @@ class MEPAddTransition(bpy.types.Operator, tool.Ifc.Operator):
|
||||
transition_dir = keep_only_z_axis(end_segment_extend_point - start_segment_extend_point).normalized()
|
||||
|
||||
# adjust the segments
|
||||
end_object_rotation = end_object.matrix_world.to_quaternion()
|
||||
end_object_z_basis = end_object_rotation.to_matrix().col[2] # z basis vector
|
||||
|
||||
# TODO: do it beforehand, as with bends
|
||||
if tool.Cad.is_x(start_object_z_basis.dot(transition_dir), 1):
|
||||
start_connection = "ATEND"
|
||||
else:
|
||||
start_connection = "ATSTART"
|
||||
if tool.Cad.is_x(end_object_z_basis.dot(transition_dir), 1):
|
||||
end_connection = "ATSTART"
|
||||
else:
|
||||
end_connection = "ATEND"
|
||||
DumbProfileJoiner().join_E(start_object, start_segment_extend_point, start_connection)
|
||||
DumbProfileJoiner().join_E(end_object, end_segment_extend_point, end_connection)
|
||||
|
||||
# For bbim transitions, there is small convention that:
|
||||
# - start_length segment positioned at the start of the transition's Z-axis.
|
||||
# - end_length segment positioned at the of it.
|
||||
# this is why we sort the lengths in parametric data too
|
||||
parametric_data = {
|
||||
"start_length": self.start_length / si_conversion,
|
||||
"end_length": self.end_length / si_conversion,
|
||||
"start_length": (self.start_length if start_segment_sign == 1 else self.end_length) / si_conversion,
|
||||
"end_length": (self.end_length if start_segment_sign == 1 else self.start_length) / si_conversion,
|
||||
"profile_offset": profile_offset,
|
||||
"angle": degrees(self.angle),
|
||||
}
|
||||
@@ -822,13 +851,15 @@ class MEPAddTransition(bpy.types.Operator, tool.Ifc.Operator):
|
||||
)
|
||||
body = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
|
||||
tool.Model.replace_object_ifc_representation(body, obj, rep)
|
||||
tool.Blender.remove_data_block(mesh)
|
||||
pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=transition_type, name="BBIM_Fitting")
|
||||
ifcopenshell.api.run(
|
||||
"pset.edit_pset",
|
||||
tool.Ifc.get(),
|
||||
pset=pset,
|
||||
properties={"Data": json.dumps(transition_data, default=list)},
|
||||
properties={"Data": tool.Ifc.get().createIfcText(json.dumps(transition_data, default=list))},
|
||||
)
|
||||
tool.System.add_ports(obj, offset_end_port=profile_offset_si)
|
||||
|
||||
# NOTE: at this point we loose current blender objects selection
|
||||
# create transition element
|
||||
@@ -852,7 +883,7 @@ class MEPAddTransition(bpy.types.Operator, tool.Ifc.Operator):
|
||||
transition_obj.location = start_segment_extend_point if start_port_match else end_segment_extend_point
|
||||
|
||||
# add ports and connect them
|
||||
ports = tool.System.add_ports(transition_obj, offset_end_port=profile_offset_ws)
|
||||
ports = tool.System.get_ports(tool.Ifc.get_entity(transition_obj))
|
||||
if not start_port_match:
|
||||
start_port, end_port = end_port, start_port
|
||||
tool.Ifc.run("system.connect_port", port1=ports[0], port2=start_port, direction="NOTDEFINED")
|
||||
@@ -866,15 +897,15 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_description = "Adds a bend between two MEP elements. Elements are either provided by ID or selected in Blender"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
start_length: bpy.props.FloatProperty(
|
||||
name="Start Length", description="Bend start length in SI units", default=0.1, subtype="DISTANCE"
|
||||
name="Start Length", description="Bend start length in SI units", default=0.1, subtype="DISTANCE", min=0
|
||||
)
|
||||
end_length: bpy.props.FloatProperty(
|
||||
name="End Length", description="Bend end length in SI units", default=0.1, subtype="DISTANCE"
|
||||
name="End Length", description="Bend end length in SI units", default=0.1, subtype="DISTANCE", min=0
|
||||
)
|
||||
start_segment_id: bpy.props.IntProperty(name="Start Segment Element ID", default=0)
|
||||
end_segment_id: bpy.props.IntProperty(name="End Segment Element ID", default=0)
|
||||
radius: bpy.props.FloatProperty(
|
||||
"Bend Inner Radius", description="Bend inner radius in SI units", default=0.2, subtype="DISTANCE"
|
||||
"Bend Inner Radius", description="Bend inner radius in SI units", default=0.2, subtype="DISTANCE", min=0
|
||||
)
|
||||
|
||||
def _execute(self, context):
|
||||
@@ -882,12 +913,6 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator):
|
||||
ifc_file = tool.Ifc.get()
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
|
||||
|
||||
self.start_length, self.end_length = 0, 0
|
||||
|
||||
if not (self.start_length == 0 and self.end_length == 0):
|
||||
self.report({"ERROR"}, f"Only zero lengths are now supported.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
if self.start_segment_id and self.end_segment_id:
|
||||
start_element = ifc_file.by_id(self.start_segment_id)
|
||||
end_element = ifc_file.by_id(self.end_segment_id)
|
||||
@@ -940,19 +965,16 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if not types_check():
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
"Segments types do not match " "or one of the segments doesn't have type which is required for a bend.",
|
||||
"Segments types do not match or one of the segments doesn't have type which is required for a bend.",
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
|
||||
# TODO: support circular profiles
|
||||
profile = tool.Model.get_flow_segment_profile(start_element)
|
||||
if not profile.is_a("IfcRectangleProfileDef"):
|
||||
if not profile.is_a("IfcRectangleProfileDef") and not profile.is_a("IfcCircleProfileDef"):
|
||||
self.report(
|
||||
{
|
||||
"ERROR",
|
||||
"For now Only IfcRectangleProfileDef profiles supported for a bend, "
|
||||
f"the segments are {profile.is_a()}",
|
||||
}
|
||||
{"ERROR"},
|
||||
"For now Only IfcRectangleProfileDef/IfcCircleProfileDef profiles supported for a bend, "
|
||||
f"the segments are {profile.is_a()}",
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
|
||||
@@ -973,10 +995,22 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator):
|
||||
end_segment_data["start_point"]: end_segment_data["start_port"],
|
||||
end_segment_data["end_point"]: end_segment_data["end_port"],
|
||||
}
|
||||
(start_point, end_point), (first_segment_start, second_segment_end) = tool.Cad.closest_points(
|
||||
(start_segment_data["start_point"], start_segment_data["end_point"]),
|
||||
(end_segment_data["start_point"], end_segment_data["end_point"]),
|
||||
|
||||
get_z_basis = lambda o: tool.Cad.get_basis_vector(o, 2)
|
||||
segments_intersection_ws = tool.Cad.intersect_edges(
|
||||
(start_object.location, start_object.location + get_z_basis(start_object)),
|
||||
(end_object.location, end_object.location + get_z_basis(end_object)),
|
||||
)[0]
|
||||
|
||||
start_point, first_segment_start = tool.Cad.closest_and_furthest_vectors(
|
||||
segments_intersection_ws, (start_segment_data["start_point"], start_segment_data["end_point"])
|
||||
)
|
||||
end_point, second_segment_end = tool.Cad.closest_and_furthest_vectors(
|
||||
segments_intersection_ws, (end_segment_data["start_point"], end_segment_data["end_point"])
|
||||
)
|
||||
|
||||
# start_/end_segment_sign indicate
|
||||
# whether segments' z axes are directed towards the bend
|
||||
start_port = points_ports_map[start_point]
|
||||
end_port = points_ports_map[end_point]
|
||||
start_point_on_origin = start_point == start_segment_data["start_point"]
|
||||
@@ -992,6 +1026,7 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator):
|
||||
# TODO: profile offset may need to be flipped (check transition code)
|
||||
to_start_object_space = start_object_rotation.inverted()
|
||||
profile_offset = (to_start_object_space @ end_point) - (to_start_object_space @ start_point)
|
||||
z_axis_end_object_local = to_start_object_space @ tool.Cad.get_basis_vector(end_object, 2)
|
||||
|
||||
def check_for_double_bends():
|
||||
# The theory is To avoid double bends, the profile offset should occur along only two axes:
|
||||
@@ -1005,8 +1040,6 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator):
|
||||
# NOTE: some double bends are only possible for square profiles:
|
||||
# https://i.imgur.com/ZhdGbEp.png
|
||||
|
||||
z_axis_end_object = end_object.matrix_world.col[2].normalized().to_3d()
|
||||
z_axis_end_object_local = to_start_object_space @ z_axis_end_object
|
||||
lateral_axes = [i for i in range(2) if not tool.Cad.is_x(z_axis_end_object_local[i], 0)]
|
||||
|
||||
if len(lateral_axes) != 1:
|
||||
@@ -1030,30 +1063,39 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if error_msg:
|
||||
self.report({"ERROR"}, error_msg)
|
||||
return {"CANCELLED"}
|
||||
non_lateral_axis = 0 if lateral_axis == 1 else 1
|
||||
|
||||
O = V(0, 0, 0)
|
||||
get_z_basis = lambda o: o.matrix_world.col[2].normalized().to_3d()
|
||||
angle = tool.Cad.angle_edges((get_z_basis(start_object), O), (get_z_basis(end_object), O))
|
||||
def get_bend_rotation():
|
||||
O = V(0, 0, 0)
|
||||
edge1 = (get_z_basis(start_object) * start_segment_sign, O)
|
||||
edge2 = (get_z_basis(end_object) * end_segment_sign, O)
|
||||
angle = pi - tool.Cad.angle_edges(edge1, edge2)
|
||||
axis = (edge2[1] - edge2[0]).cross(edge1[1] - edge1[0])
|
||||
return angle, axis
|
||||
|
||||
angle, rotation_axis = get_bend_rotation()
|
||||
|
||||
lateral_sign = tool.Cad.sign(profile_offset[lateral_axis])
|
||||
radial_offset = V(0, 0, 0)
|
||||
ref_point_radius = self.radius + profile_dim[lateral_axis]
|
||||
radial_offset[lateral_axis] = ref_point_radius * (1 - cos(angle)) * lateral_sign
|
||||
radial_offset.z = ref_point_radius * sin(angle)
|
||||
radial_offset.z = ref_point_radius * sin(angle) * start_segment_sign
|
||||
end_port_offset = radial_offset + V(0, 0, self.start_length * start_segment_sign)
|
||||
end_port_offset += z_axis_end_object_local * (self.end_length * -end_segment_sign)
|
||||
|
||||
def get_segments_extend():
|
||||
end_segment_z_local = to_start_object_space @ get_z_basis(end_object)
|
||||
segments_intersection = tool.Cad.intersect_edges(
|
||||
(V(0, 0, 1), V(0, 0, 0)), (profile_offset + end_segment_z_local, profile_offset)
|
||||
)[0]
|
||||
segments_intersection = segments_intersection_ws - start_point
|
||||
segments_intersection = to_start_object_space @ segments_intersection
|
||||
|
||||
curent_start_offset = segments_intersection.length
|
||||
required_start_offset = abs(radial_offset.z)
|
||||
# since tangent segments are equal
|
||||
# if drawn for the circle from the same point
|
||||
required_offset = ref_point_radius * tan(angle / 2)
|
||||
|
||||
current_start_offset = segments_intersection.length
|
||||
current_end_offset = (segments_intersection - profile_offset).length
|
||||
required_end_offset = abs(radial_offset[lateral_axis])
|
||||
|
||||
start_extend = curent_start_offset - required_start_offset
|
||||
end_extend = current_end_offset - required_end_offset
|
||||
start_extend = current_start_offset - (required_offset + self.start_length)
|
||||
end_extend = current_end_offset - (required_offset + self.end_length)
|
||||
|
||||
return start_extend, end_extend
|
||||
|
||||
@@ -1076,7 +1118,6 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
# adjust segments to fit the radius and angle
|
||||
start_segment_extend, end_segment_extend = get_segments_extend()
|
||||
|
||||
start_segment_extend_point = start_point + start_segment_sign * start_segment_extend * get_z_basis(start_object)
|
||||
projection = check_new_segment_length(first_segment_start, start_point, start_segment_extend_point)
|
||||
if projection is not None:
|
||||
@@ -1108,17 +1149,38 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator):
|
||||
angle,
|
||||
self.radius / si_conversion,
|
||||
profile_offset / si_conversion,
|
||||
flip_z_axis=start_segment_sign == -1,
|
||||
)
|
||||
|
||||
bpy.ops.bim.create_shape_from_step_id(step_id=rep.id(), should_include_curves=True)
|
||||
|
||||
parametric_data = {
|
||||
"start_length": self.start_length / si_conversion,
|
||||
"end_length": self.end_length / si_conversion,
|
||||
"radius": self.radius / si_conversion,
|
||||
"angle": degrees(angle),
|
||||
"main_profile_dimension": profile_dim[lateral_axis] / si_conversion,
|
||||
}
|
||||
# find the compatible fitting type
|
||||
fitting_data = MEPGenerator().get_compatible_fitting_type(
|
||||
[start_element, end_element], [start_port, end_port], "BEND"
|
||||
[start_element, end_element], [start_port, end_port], "BEND", bbim_data=parametric_data
|
||||
)
|
||||
bend_type = fitting_data["fitting_type"] if fitting_data else None
|
||||
start_port_match = fitting_data["start_port_match"] if fitting_data else True
|
||||
|
||||
# use current segments axes if no fitting type found
|
||||
lateral_axis_type = lateral_axis
|
||||
lateral_sign_type = lateral_sign
|
||||
z_sign_type = start_segment_sign
|
||||
non_lateral_axis_type = non_lateral_axis
|
||||
if bend_type:
|
||||
bend_obj = tool.Ifc.get_object(bend_type)
|
||||
bbim_data = tool.Model.get_modeling_bbim_pset_data(bend_obj, "BBIM_Fitting")["data_dict"]
|
||||
lateral_axis_type, lateral_sign_type = bbim_data["lateral_axis"], bbim_data["lateral_sign"]
|
||||
non_lateral_axis_type = 0 if lateral_axis_type == 1 else 1
|
||||
z_sign_type = bbim_data.get("z_axis_sign", None)
|
||||
# TODO: drop flip_z_axis a bit later
|
||||
if z_sign_type is None:
|
||||
z_sign_type = -1 if bbim_data["flip_z_axis"] else 1
|
||||
|
||||
# TODO: handle the case without creating a representation in the first place?
|
||||
ifcopenshell.api.run("geometry.remove_representation", ifc_file, representation=rep)
|
||||
else: # create new fitting type if nothing is compatible
|
||||
@@ -1135,13 +1197,15 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator):
|
||||
)
|
||||
body = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
|
||||
tool.Model.replace_object_ifc_representation(body, obj, rep)
|
||||
tool.Blender.remove_data_block(mesh)
|
||||
pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=bend_type, name="BBIM_Fitting")
|
||||
ifcopenshell.api.run(
|
||||
"pset.edit_pset",
|
||||
tool.Ifc.get(),
|
||||
pset=pset,
|
||||
properties={"Data": json.dumps(bend_data, default=list)},
|
||||
properties={"Data": tool.Ifc.get().createIfcText(json.dumps(bend_data, default=list))},
|
||||
)
|
||||
tool.System.add_ports(obj, end_port_pos=end_port_offset)
|
||||
|
||||
# NOTE: at this point we loose current blender objects selection
|
||||
# create transition element
|
||||
@@ -1153,18 +1217,39 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator):
|
||||
fitting_obj.matrix_world = start_object.matrix_world
|
||||
context.view_layer.update()
|
||||
|
||||
# depending on fitting direction we may need to flip it or attach it's origin to end segment
|
||||
# direction can be different depending on:
|
||||
# - order of the current segments
|
||||
# - order of the segments that were used with the same fitting type before
|
||||
direction_match = tool.Cad.are_vectors_equal(get_z_basis(start_object), get_z_basis(fitting_obj))
|
||||
# if there are no mismatches or everything matches up we don't need to flip the transition
|
||||
if start_port_match != direction_match:
|
||||
fitting_obj.matrix_world = start_object.matrix_world @ Matrix.Rotation(radians(180), 4, "X")
|
||||
fitting_obj.location = start_segment_extend_point if start_port_match else end_segment_extend_point
|
||||
# depending on bend direction we may need to rotate it to match
|
||||
# we just calculate the matrix basises - it's simpler than describing all possible conditions
|
||||
def get_fitting_matrix():
|
||||
matrix = Matrix.Identity(3)
|
||||
start_object_z_basis = tool.Cad.get_basis_vector(start_object, 2)
|
||||
start_object_lateral_basis = tool.Cad.get_basis_vector(start_object, lateral_axis)
|
||||
|
||||
def axis_direction(current_axis_sign, type_axis_sign):
|
||||
return -1 if current_axis_sign != type_axis_sign else 1
|
||||
|
||||
matrix.col[2] = start_object_z_basis * axis_direction(start_segment_sign, z_sign_type)
|
||||
matrix.col[lateral_axis_type] = start_object_lateral_basis * axis_direction(lateral_sign, lateral_sign_type)
|
||||
if not start_port_match:
|
||||
matrix.col[2] *= -1
|
||||
|
||||
if non_lateral_axis_type == 0:
|
||||
non_lateral_axis = matrix.col[lateral_axis_type].cross(matrix.col[2])
|
||||
else:
|
||||
non_lateral_axis = matrix.col[2].cross(matrix.col[lateral_axis_type])
|
||||
matrix.col[non_lateral_axis_type] = non_lateral_axis
|
||||
|
||||
if not start_port_match:
|
||||
angle_sign = np.sign(rotation_axis.dot(non_lateral_axis))
|
||||
matrix = matrix @ Matrix.Rotation(angle * angle_sign, 3, "XY"[non_lateral_axis_type])
|
||||
|
||||
matrix = matrix.to_4x4()
|
||||
matrix.translation = start_segment_extend_point if start_port_match else end_segment_extend_point
|
||||
return matrix
|
||||
|
||||
fitting_obj.matrix_world = get_fitting_matrix()
|
||||
|
||||
# add ports and connect them
|
||||
ports = tool.System.add_ports(fitting_obj, offset_end_port=start_object_rotation @ (radial_offset * V(1, 1, 0)))
|
||||
ports = tool.System.get_ports(tool.Ifc.get_entity(fitting_obj))
|
||||
if not start_port_match:
|
||||
start_port, end_port = end_port, start_port
|
||||
tool.Ifc.run("system.connect_port", port1=ports[0], port2=start_port, direction="NOTDEFINED")
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
import bpy
|
||||
import gpu
|
||||
import json
|
||||
import bmesh
|
||||
import shapely
|
||||
import logging
|
||||
@@ -160,7 +161,10 @@ class FilledOpeningGenerator:
|
||||
|
||||
for voided_obj in voided_objs:
|
||||
if voided_obj.data:
|
||||
representation = tool.Ifc.get().by_id(voided_obj.data.BIMMeshProperties.ifc_definition_id)
|
||||
voided_element = tool.Ifc.get_entity(voided_obj)
|
||||
context = tool.Geometry.get_active_representation_context(voided_obj)
|
||||
representation = tool.Geometry.get_representation_by_context(voided_element, context)
|
||||
|
||||
blenderbim.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
@@ -274,9 +278,7 @@ class FilledOpeningGenerator:
|
||||
get_curve_2d_from_3d(profile),
|
||||
magnitude=thickness / unit_scale,
|
||||
position=Vector([0.0, -thickness * 0.5 / unit_scale, 0.0]),
|
||||
position_x_axis=Vector((1, 0, 0)),
|
||||
position_z_axis=Vector((0, -1, 0)),
|
||||
extrusion_vector=Vector((0, 0, -1)),
|
||||
**shape_builder.extrude_kwargs("Y")
|
||||
)
|
||||
return shape_builder.get_representation(context, [extrusion])
|
||||
|
||||
@@ -309,9 +311,7 @@ class FilledOpeningGenerator:
|
||||
shape_builder.rectangle(size=opening_size),
|
||||
magnitude=thickness / unit_scale,
|
||||
position=opening_position,
|
||||
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)),
|
||||
**shape_builder.extrude_kwargs("Y")
|
||||
)
|
||||
|
||||
return shape_builder.get_representation(context, [extrusion])
|
||||
@@ -397,18 +397,7 @@ class FlipFill(bpy.types.Operator, tool.Ifc.Operator):
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not element.FillsVoids:
|
||||
continue
|
||||
|
||||
flip_matrix = Matrix.Rotation(pi, 4, "Z")
|
||||
|
||||
bottom_left = obj.matrix_world @ Vector(obj.bound_box[0])
|
||||
top_right = obj.matrix_world @ Vector(obj.bound_box[6])
|
||||
center = obj.matrix_world.translation.copy()
|
||||
center_offset = center - bottom_left
|
||||
flipped_center = top_right - center_offset
|
||||
|
||||
obj.matrix_world = obj.matrix_world @ flip_matrix
|
||||
obj.matrix_world.translation.xy = flipped_center.xy
|
||||
bpy.context.view_layer.update()
|
||||
tool.Geometry.flip_object(obj, "XY")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -535,10 +524,11 @@ class AddBoolean(Operator, tool.Ifc.Operator):
|
||||
elif obj2.data:
|
||||
mesh_data = {"type": "Mesh", "blender_obj": obj1, "blender_void": obj2}
|
||||
|
||||
ifcopenshell.api.run(
|
||||
booleans = ifcopenshell.api.run(
|
||||
"geometry.add_boolean", tool.Ifc.get(), representation=representation, operator="DIFFERENCE", **mesh_data
|
||||
)
|
||||
|
||||
tool.Model.mark_manual_booleans(element1, booleans)
|
||||
tool.Model.clear_scene_openings()
|
||||
|
||||
blenderbim.core.geometry.switch_representation(
|
||||
@@ -659,6 +649,7 @@ class RemoveBooleans(Operator, tool.Ifc.Operator, AddObjectHelper):
|
||||
|
||||
def _execute(self, context):
|
||||
upstream_obj = None
|
||||
bbim_boolean_updates = {}
|
||||
for obj in context.selected_objects:
|
||||
if (
|
||||
not obj.data
|
||||
@@ -667,13 +658,21 @@ class RemoveBooleans(Operator, tool.Ifc.Operator, AddObjectHelper):
|
||||
):
|
||||
continue
|
||||
try:
|
||||
boolean = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_boolean_id)
|
||||
item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_boolean_id)
|
||||
except:
|
||||
continue
|
||||
ifcopenshell.api.run("geometry.remove_boolean", tool.Ifc.get(), item=boolean)
|
||||
|
||||
boolean = None
|
||||
for inverse in tool.Ifc.get().get_inverse(item):
|
||||
if inverse.is_a("IfcBooleanResult"):
|
||||
boolean = inverse
|
||||
break
|
||||
ifcopenshell.api.run("geometry.remove_boolean", tool.Ifc.get(), item=item)
|
||||
|
||||
if obj.data.BIMMeshProperties.obj:
|
||||
upstream_obj = obj.data.BIMMeshProperties.obj
|
||||
element = tool.Ifc.get_entity(upstream_obj)
|
||||
bbim_boolean_updates.setdefault(element, []).append(boolean)
|
||||
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
||||
if body:
|
||||
blenderbim.core.geometry.switch_representation(
|
||||
@@ -686,7 +685,10 @@ class RemoveBooleans(Operator, tool.Ifc.Operator, AddObjectHelper):
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
bpy.data.objects.remove(obj)
|
||||
|
||||
|
||||
for element, booleans in bbim_boolean_updates.items():
|
||||
tool.Model.unmark_manual_booleans(element, booleans)
|
||||
|
||||
tool.Blender.set_active_object(upstream_obj)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -730,6 +732,9 @@ class HideOpenings(Operator, tool.Ifc.Operator):
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
if element.is_a("IfcOpeningElement"):
|
||||
element = element.VoidsElements[0].RelatingBuildingElement
|
||||
obj = tool.Ifc.get_object(element)
|
||||
openings = [r.RelatedOpeningElement for r in element.HasOpenings]
|
||||
for opening in openings:
|
||||
opening_obj = tool.Ifc.get_object(opening)
|
||||
@@ -752,18 +757,19 @@ class EditOpenings(Operator, tool.Ifc.Operator):
|
||||
building_objs = set()
|
||||
model = tool.Ifc.get()
|
||||
all_openings = model.by_type("IfcOpeningElement")
|
||||
similar_openings = []
|
||||
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
if element.is_a("IfcOpeningElement"):
|
||||
element = element.VoidsElements[0].RelatingBuildingElement
|
||||
obj = tool.Ifc.get_object(element)
|
||||
openings = [r.RelatedOpeningElement for r in element.HasOpenings]
|
||||
for opening in openings:
|
||||
for all_opening in all_openings:
|
||||
if all_opening.ObjectPlacement == opening.ObjectPlacement:
|
||||
similar_openings.append(all_opening)
|
||||
similar_openings = [o for o in all_openings if o.ObjectPlacement == opening.ObjectPlacement]
|
||||
opening_obj = tool.Ifc.get_object(opening)
|
||||
building_objs.add(obj)
|
||||
if opening_obj:
|
||||
if tool.Ifc.is_edited(opening_obj):
|
||||
tool.Geometry.run_geometry_update_representation(obj=opening_obj)
|
||||
@@ -771,9 +777,12 @@ class EditOpenings(Operator, tool.Ifc.Operator):
|
||||
blenderbim.core.geometry.edit_object_placement(
|
||||
tool.Ifc, tool.Geometry, tool.Surveyor, obj=opening_obj
|
||||
)
|
||||
for similar_opening in similar_openings:
|
||||
similar_opening.ObjectPlacement = opening.ObjectPlacement
|
||||
building_objs.add(obj)
|
||||
for similar_opening in similar_openings:
|
||||
similar_opening.ObjectPlacement = opening.ObjectPlacement
|
||||
element = similar_opening.VoidsElements[0].RelatingBuildingElement
|
||||
obj = tool.Ifc.get_object(element)
|
||||
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)
|
||||
@@ -782,7 +791,7 @@ class EditOpenings(Operator, tool.Ifc.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
def get_all_building_objects_of_similar_openings(self, opening):
|
||||
if not opening.HasFillings:
|
||||
if not opening.is_a("IfcOpeningElement") or not opening.HasFillings:
|
||||
return []
|
||||
results = set()
|
||||
for rel in opening.HasFillings:
|
||||
@@ -797,6 +806,7 @@ class EditOpenings(Operator, tool.Ifc.Operator):
|
||||
results.add(obj)
|
||||
return results
|
||||
|
||||
|
||||
class CloneOpening(Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.clone_opening"
|
||||
bl_label = "Clone Opening"
|
||||
@@ -820,12 +830,15 @@ class CloneOpening(Operator, tool.Ifc.Operator):
|
||||
|
||||
new_opening = ifcopenshell.api.run("root.create_entity", tool.Ifc.get(), ifc_class="IfcOpeningElement")
|
||||
for representation in opening_representations:
|
||||
ifcopenshell.api.run("geometry.assign_representation", tool.Ifc.get(), product = new_opening, representation = representation)
|
||||
ifcopenshell.api.run(
|
||||
"geometry.assign_representation", tool.Ifc.get(), product=new_opening, representation=representation
|
||||
)
|
||||
|
||||
ifcopenshell.api.run("void.add_opening", tool.Ifc.get(), opening = new_opening, element = wall)
|
||||
ifcopenshell.api.run("void.add_opening", tool.Ifc.get(), opening=new_opening, element=wall)
|
||||
new_opening.ObjectPlacement = opening_placement
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
# TODO: merge with ProfileDecorator?
|
||||
class DecorationsHandler:
|
||||
installed = None
|
||||
@@ -870,14 +883,14 @@ class DecorationsHandler:
|
||||
if not obj:
|
||||
continue
|
||||
|
||||
self.line_shader = gpu.shader.from_builtin("3D_POLYLINE_UNIFORM_COLOR")
|
||||
self.line_shader = gpu.shader.from_builtin("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")
|
||||
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
|
||||
verts = []
|
||||
selected_edges = []
|
||||
|
||||
@@ -80,9 +80,13 @@ class AddDefaultType(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMModelProperties
|
||||
ifc_file = tool.Ifc.get()
|
||||
props.type_class = self.ifc_element_type
|
||||
if self.ifc_element_type == "IfcWallType":
|
||||
props.type_predefined_type = "SOLIDWALL"
|
||||
if ifc_file.schema == "IFC2X3":
|
||||
props.type_predefined_type = "STANDARD"
|
||||
else:
|
||||
props.type_predefined_type = "SOLIDWALL"
|
||||
props.type_template = "LAYERSET_AXIS2"
|
||||
elif self.ifc_element_type == "IfcSlabType":
|
||||
props.type_predefined_type = "FLOOR"
|
||||
@@ -185,8 +189,13 @@ class AddConstrTypeInstance(bpy.types.Operator):
|
||||
collection_obj = collection.BIMCollectionProperties.obj
|
||||
|
||||
bpy.ops.bim.assign_class(obj=obj.name, ifc_class=instance_class)
|
||||
tool.Blender.remove_data_block(mesh) # Remove "Instance" mesh
|
||||
|
||||
mesh_data = obj.data
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
blenderbim.core.type.assign_type(tool.Ifc, tool.Type, element=element, type=relating_type)
|
||||
if obj.data != mesh_data: # remove orphaned mesh from "bim.assign_class"
|
||||
tool.Blender.remove_data_block(mesh_data)
|
||||
|
||||
# Update required as core.type.assign_type may change obj.data
|
||||
# TODO: This is inefficient. It literally creates a mesh, then potentially removes it.
|
||||
@@ -240,6 +249,8 @@ class AddConstrTypeInstance(bpy.types.Operator):
|
||||
mat.translation *= unit_scale
|
||||
mat = obj.matrix_world @ mat
|
||||
new_port = tool.Ifc.run("root.create_entity", ifc_class="IfcDistributionPort")
|
||||
new_port.PredefinedType = port.PredefinedType
|
||||
new_port.SystemType = port.SystemType
|
||||
tool.Ifc.run("system.assign_port", element=element, port=new_port)
|
||||
tool.Ifc.run("geometry.edit_object_placement", product=new_port, matrix=mat, is_si=True)
|
||||
|
||||
@@ -550,5 +561,6 @@ def ensure_material_unassigned(usecase_path, ifc_file, settings):
|
||||
total_removed = 0
|
||||
for i in to_remove:
|
||||
obj.active_material_index = i - total_removed
|
||||
bpy.ops.object.material_slot_remove({"object": obj})
|
||||
with bpy.context.temp_override(object=obj):
|
||||
bpy.ops.object.material_slot_remove()
|
||||
total_removed += 1
|
||||
|
||||
@@ -132,6 +132,7 @@ class DumbProfileGenerator:
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
tool.Blender.remove_data_block(mesh)
|
||||
|
||||
pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="EPset_Parametric")
|
||||
ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Engine": "BlenderBIM.DumbProfile"})
|
||||
@@ -448,6 +449,16 @@ class DumbProfileJoiner:
|
||||
"geometry.assign_representation", tool.Ifc.get(), product=element, representation=new_axis
|
||||
)
|
||||
|
||||
def get_placement_axes(body_representation):
|
||||
if not body_representation:
|
||||
return None, None
|
||||
extrusion = tool.Model.get_extrusion(body_representation)
|
||||
if not extrusion:
|
||||
return None, None
|
||||
position = extrusion.Position
|
||||
return (position.Axis.DirectionRatios, position.RefDirection.DirectionRatios)
|
||||
|
||||
old_body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
||||
new_body = ifcopenshell.api.run(
|
||||
"geometry.add_profile_representation",
|
||||
tool.Ifc.get(),
|
||||
@@ -456,9 +467,9 @@ class DumbProfileJoiner:
|
||||
depth=depth,
|
||||
cardinal_point=usage.CardinalPoint if usage else None,
|
||||
clippings=self.clippings,
|
||||
placement_zx_axes=get_placement_axes(old_body),
|
||||
)
|
||||
|
||||
old_body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
||||
if old_body:
|
||||
for inverse in tool.Ifc.get().get_inverse(old_body):
|
||||
ifcopenshell.util.element.replace_attribute(inverse, old_body, new_body)
|
||||
@@ -572,15 +583,11 @@ class DumbProfileJoiner:
|
||||
if connection1 == "ATEND":
|
||||
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
|
||||
plane = self.get_profile_plane(profile2, furthest_plane)
|
||||
intersect = mathutils.geometry.intersect_line_plane(
|
||||
*axis1, plane.translation, plane.col[2].to_3d()
|
||||
)
|
||||
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
|
||||
self.body[1] = intersect
|
||||
else:
|
||||
plane = self.get_profile_plane(profile2, furthest_plane, z_inwards=False)
|
||||
intersect = mathutils.geometry.intersect_line_plane(
|
||||
*axis1, plane.translation, plane.col[2].to_3d()
|
||||
)
|
||||
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
|
||||
max_dim = self.get_max_bound_box_dimension(profile1)
|
||||
self.body[1] = intersect + profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
|
||||
|
||||
@@ -623,15 +630,11 @@ class DumbProfileJoiner:
|
||||
elif connection1 == "ATSTART":
|
||||
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
|
||||
plane = self.get_profile_plane(profile2, furthest_plane)
|
||||
intersect = mathutils.geometry.intersect_line_plane(
|
||||
*axis1, plane.translation, plane.col[2].to_3d()
|
||||
)
|
||||
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
|
||||
self.body[0] = intersect
|
||||
else:
|
||||
plane = self.get_profile_plane(profile2, furthest_plane, z_inwards=False)
|
||||
intersect = mathutils.geometry.intersect_line_plane(
|
||||
*axis1, plane.translation, plane.col[2].to_3d()
|
||||
)
|
||||
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
|
||||
max_dim = self.get_max_bound_box_dimension(profile1)
|
||||
self.body[0] = intersect - profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
|
||||
|
||||
@@ -675,9 +678,7 @@ class DumbProfileJoiner:
|
||||
if connection1 == "ATEND":
|
||||
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
|
||||
plane = self.get_profile_plane(profile2, furthest_plane if is_relating else closest_plane)
|
||||
intersect = mathutils.geometry.intersect_line_plane(
|
||||
*axis1, plane.translation, plane.col[2].to_3d()
|
||||
)
|
||||
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
|
||||
self.body[1] = intersect
|
||||
else:
|
||||
plane = self.get_profile_plane(
|
||||
@@ -685,9 +686,7 @@ class DumbProfileJoiner:
|
||||
furthest_plane if is_relating else closest_plane,
|
||||
z_inwards=False if is_relating else True,
|
||||
)
|
||||
intersect = mathutils.geometry.intersect_line_plane(
|
||||
*axis1, plane.translation, plane.col[2].to_3d()
|
||||
)
|
||||
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
|
||||
max_dim = self.get_max_bound_box_dimension(profile1)
|
||||
self.body[1] = intersect + profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
|
||||
self.clippings.append(
|
||||
@@ -700,9 +699,7 @@ class DumbProfileJoiner:
|
||||
elif connection1 == "ATSTART":
|
||||
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
|
||||
plane = self.get_profile_plane(profile2, furthest_plane if is_relating else closest_plane)
|
||||
intersect = mathutils.geometry.intersect_line_plane(
|
||||
*axis1, plane.translation, plane.col[2].to_3d()
|
||||
)
|
||||
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
|
||||
self.body[0] = intersect
|
||||
else:
|
||||
plane = self.get_profile_plane(
|
||||
@@ -710,9 +707,7 @@ class DumbProfileJoiner:
|
||||
furthest_plane if is_relating else closest_plane,
|
||||
z_inwards=False if is_relating else True,
|
||||
)
|
||||
intersect = mathutils.geometry.intersect_line_plane(
|
||||
*axis1, plane.translation, plane.col[2].to_3d()
|
||||
)
|
||||
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
|
||||
max_dim = self.get_max_bound_box_dimension(profile1)
|
||||
self.body[0] = intersect - profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
|
||||
self.clippings.append(
|
||||
|
||||
@@ -79,6 +79,7 @@ def update_type_class(self, context):
|
||||
def update_relating_type_id(self, context):
|
||||
AuthoringData.data["relating_type_id"] = AuthoringData.relating_type_id()
|
||||
AuthoringData.data["type_thumbnail"] = AuthoringData.type_thumbnail()
|
||||
AuthoringData.data["predefined_type"] = AuthoringData.predefined_type()
|
||||
|
||||
|
||||
def update_type_page(self, context):
|
||||
@@ -132,13 +133,14 @@ class BIMModelProperties(PropertyGroup):
|
||||
y: bpy.props.FloatProperty(name="Y", default=0.5, subtype="DISTANCE", description="Size by Y axis for the opening")
|
||||
z: bpy.props.FloatProperty(name="Z", default=0.5, subtype="DISTANCE", description="Size by Z axis for the opening")
|
||||
# Used for things like walls, doors, flooring, skirting, etc
|
||||
rl1: bpy.props.FloatProperty(name="RL", default=1, subtype="DISTANCE", description="Z offset for walls")
|
||||
rl1: bpy.props.FloatProperty(name="RL", default=1, subtype="DISTANCE", description="Z offset for walls")
|
||||
# Used for things like windows, other hosted furniture, and MEP
|
||||
rl2: bpy.props.FloatProperty(name="RL", default=1, subtype="DISTANCE", description="Z offset for windows")
|
||||
# Used for plan calculation points such as in room generation
|
||||
rl3: bpy.props.FloatProperty(name="RL", default=1, subtype="DISTANCE", description="Z offset for space calculation")
|
||||
x_angle: bpy.props.FloatProperty(name="X Angle", default=0, subtype="ANGLE", min=-pi / 180 * 89, max=pi / 180 * 89)
|
||||
type_page: bpy.props.IntProperty(name="Type Page", default=1, update=update_type_page)
|
||||
# fmt: off
|
||||
type_template: bpy.props.EnumProperty(
|
||||
items=(
|
||||
("MESH", "Custom Mesh", "Use as a representation currently active object mesh or default cube if no object selected"),
|
||||
@@ -158,6 +160,7 @@ class BIMModelProperties(PropertyGroup):
|
||||
name="Type Template",
|
||||
default="MESH",
|
||||
)
|
||||
# fmt: on
|
||||
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")
|
||||
@@ -190,6 +193,10 @@ class BIMArrayProperties(PropertyGroup):
|
||||
|
||||
|
||||
class BIMStairProperties(PropertyGroup):
|
||||
def validate_nosing_value(self, context):
|
||||
if self.stair_type != "WOOD/STEEL" and self.nosing_length < 0:
|
||||
self["nosing_length"] = 0
|
||||
|
||||
non_si_units_props = ("is_editing", "number_of_treads", "has_top_nib", "stair_type")
|
||||
stair_types = (
|
||||
("CONCRETE", "Concrete", ""),
|
||||
@@ -200,13 +207,37 @@ class BIMStairProperties(PropertyGroup):
|
||||
is_editing: bpy.props.BoolProperty(default=False)
|
||||
width: bpy.props.FloatProperty(name="Width", default=1.2, soft_min=0.01, subtype="DISTANCE")
|
||||
height: bpy.props.FloatProperty(name="Height", default=1.0, soft_min=0.01, subtype="DISTANCE")
|
||||
number_of_treads: bpy.props.IntProperty(name="Number of treads", default=6, soft_min=1)
|
||||
number_of_treads: bpy.props.IntProperty(name="Number of Treads", default=6, soft_min=1)
|
||||
tread_depth: bpy.props.FloatProperty(name="Tread Depth", default=0.25, soft_min=0.01, subtype="DISTANCE")
|
||||
tread_run: bpy.props.FloatProperty(name="Tread Run", default=0.3, soft_min=0.01, subtype="DISTANCE")
|
||||
base_slab_depth: bpy.props.FloatProperty(name="Base slab depth", default=0.25, soft_min=0, subtype="DISTANCE")
|
||||
top_slab_depth: bpy.props.FloatProperty(name="Top slab depth", default=0.25, soft_min=0, subtype="DISTANCE")
|
||||
has_top_nib: bpy.props.BoolProperty(name="Has top nib", default=True)
|
||||
stair_type: bpy.props.EnumProperty(name="Stair type", items=stair_types, default="CONCRETE")
|
||||
base_slab_depth: bpy.props.FloatProperty(name="Base Slab Depth", default=0.25, soft_min=0, subtype="DISTANCE")
|
||||
top_slab_depth: bpy.props.FloatProperty(name="Top Slab Depth", default=0.25, soft_min=0, subtype="DISTANCE")
|
||||
has_top_nib: bpy.props.BoolProperty(name="Has Top Nib", default=True)
|
||||
stair_type: bpy.props.EnumProperty(
|
||||
name="Stair Type", items=stair_types, default="CONCRETE", update=validate_nosing_value
|
||||
)
|
||||
custom_first_last_tread_run: bpy.props.FloatVectorProperty(
|
||||
name="Custom First / Last Treads Widths",
|
||||
description='Specify custom first / last treads widths, different from the general "Tread Run". Leave 0 to disable.',
|
||||
default=(0, 0),
|
||||
min=0,
|
||||
unit="LENGTH",
|
||||
size=2,
|
||||
)
|
||||
# TODO: need to clamp at zero for non WOOD/STEEL
|
||||
nosing_length: bpy.props.FloatProperty(
|
||||
name="Nosing Length",
|
||||
description=(
|
||||
"Overhang of the tread, not counted as a part of the tread run.\n"
|
||||
"Can be negative for WOOD/STEEL stair (then it becomes a tread gap)"
|
||||
),
|
||||
default=0,
|
||||
unit="LENGTH",
|
||||
update=validate_nosing_value,
|
||||
)
|
||||
nosing_depth: bpy.props.FloatProperty(
|
||||
name="Nosing Depth", description="Depth of the tread's nosing", min=0, default=0, unit="LENGTH"
|
||||
)
|
||||
|
||||
def get_props_kwargs(self, convert_to_project_units=False, stair_type=None):
|
||||
if not stair_type:
|
||||
@@ -217,10 +248,12 @@ class BIMStairProperties(PropertyGroup):
|
||||
"height": self.height,
|
||||
"number_of_treads": self.number_of_treads,
|
||||
"tread_run": self.tread_run,
|
||||
"nosing_length": self.nosing_length,
|
||||
}
|
||||
|
||||
if stair_type == "CONCRETE":
|
||||
concrete_props = {
|
||||
"nosing_depth": self.nosing_depth,
|
||||
"base_slab_depth": self.base_slab_depth,
|
||||
"top_slab_depth": self.top_slab_depth,
|
||||
"has_top_nib": self.has_top_nib,
|
||||
@@ -235,7 +268,13 @@ class BIMStairProperties(PropertyGroup):
|
||||
stair_kwargs.update(wood_steel_props)
|
||||
|
||||
elif stair_type == "GENERIC":
|
||||
pass
|
||||
generic_props = {
|
||||
"nosing_depth": self.nosing_depth,
|
||||
}
|
||||
stair_kwargs.update(generic_props)
|
||||
|
||||
# defined here to appear last in UI
|
||||
stair_kwargs["custom_first_last_tread_run"] = self.custom_first_last_tread_run
|
||||
|
||||
if not convert_to_project_units:
|
||||
return stair_kwargs
|
||||
|
||||
@@ -114,7 +114,7 @@ def update_bbim_railing_pset(element, railing_data):
|
||||
pset = tool.Pset.get_element_pset(element, "BBIM_Railing")
|
||||
if not pset:
|
||||
pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=element, name="BBIM_Railing")
|
||||
railing_data = json.dumps(railing_data, default=list)
|
||||
railing_data = tool.Ifc.get().createIfcText(json.dumps(railing_data, default=list))
|
||||
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Data": railing_data})
|
||||
|
||||
|
||||
|
||||
@@ -412,7 +412,7 @@ def update_bbim_roof_pset(element, roof_data):
|
||||
pset = tool.Pset.get_element_pset(element, "BBIM_Roof")
|
||||
if not pset:
|
||||
pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=element, name="BBIM_Roof")
|
||||
roof_data = json.dumps(roof_data, default=list)
|
||||
roof_data = tool.Ifc.get().createIfcText(json.dumps(roof_data, default=list))
|
||||
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Data": roof_data})
|
||||
|
||||
|
||||
|
||||
@@ -196,6 +196,7 @@ class DumbSlabGenerator:
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
tool.Blender.remove_data_block(mesh)
|
||||
|
||||
if self.footprint_context:
|
||||
extrusion = tool.Model.get_extrusion(representation)
|
||||
|
||||
@@ -18,15 +18,11 @@
|
||||
|
||||
|
||||
import bpy
|
||||
import bmesh
|
||||
import shapely
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.element
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.core.spatial as core
|
||||
import blenderbim.core.type
|
||||
from math import pi
|
||||
from mathutils import Vector, Matrix
|
||||
from shapely import Polygon, MultiPolygon
|
||||
|
||||
|
||||
class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -49,13 +45,7 @@ class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator):
|
||||
def msg(self, context):
|
||||
self.layout.label(text="NO ACTIVE STOREY")
|
||||
|
||||
props = context.scene.BIMModelProperties
|
||||
relating_type_id = props.relating_type_id
|
||||
relating_type = None
|
||||
if relating_type_id:
|
||||
relating_type = tool.Ifc.get().by_id(int(relating_type_id))
|
||||
if not relating_type.is_a("IfcSpaceType"):
|
||||
relating_type = None
|
||||
# props = context.scene.BIMModelProperties
|
||||
|
||||
collection = context.view_layer.active_layer_collection.collection
|
||||
collection_obj = collection.BIMCollectionProperties.obj
|
||||
@@ -67,151 +57,7 @@ class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
|
||||
return
|
||||
|
||||
active_obj = bpy.context.active_object
|
||||
element = None
|
||||
if bpy.context.selected_objects and active_obj:
|
||||
element = tool.Ifc.get_entity(active_obj)
|
||||
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
|
||||
z = collection_obj.matrix_world.translation.z
|
||||
mat = Matrix()
|
||||
mat.translation = (x, y, z)
|
||||
h = 3
|
||||
|
||||
calculation_rl = context.scene.BIMModelProperties.rl3
|
||||
self.cut_point = collection_obj.matrix_world.translation.copy() + Vector((0, 0, calculation_rl))
|
||||
self.cut_normal = Vector((0, 0, 1))
|
||||
boundary_lines = []
|
||||
|
||||
gross_settings = ifcopenshell.geom.settings()
|
||||
gross_settings.set(gross_settings.DISABLE_OPENING_SUBTRACTIONS, True)
|
||||
|
||||
for obj in bpy.context.visible_objects:
|
||||
visible_element = tool.Ifc.get_entity(obj)
|
||||
|
||||
if (
|
||||
not visible_element
|
||||
or obj.type != "MESH"
|
||||
or not self.is_bounding_class(visible_element)
|
||||
or not tool.Drawing.is_intersecting_plane(obj, self.cut_point, self.cut_normal)
|
||||
):
|
||||
continue
|
||||
|
||||
old_mesh = None
|
||||
if visible_element.HasOpenings:
|
||||
new_mesh = self.create_mesh(ifcopenshell.geom.create_shape(gross_settings, visible_element))
|
||||
old_mesh = obj.data
|
||||
obj.data = new_mesh
|
||||
|
||||
local_cut_point = obj.matrix_world.inverted() @ self.cut_point
|
||||
local_cut_normal = obj.matrix_world.inverted().to_quaternion() @ self.cut_normal
|
||||
verts, edges = tool.Drawing.bisect_mesh_with_plane(obj, local_cut_point, local_cut_normal)
|
||||
|
||||
if old_mesh:
|
||||
obj.data = old_mesh
|
||||
bpy.data.meshes.remove(new_mesh)
|
||||
|
||||
for edge in edges or []:
|
||||
boundary_lines.append(
|
||||
shapely.LineString(
|
||||
[Vector((round(x, 3) for x in verts[edge[0]])), Vector((round(x, 3) for x in verts[edge[1]]))]
|
||||
)
|
||||
)
|
||||
|
||||
unioned_boundaries = shapely.union_all(shapely.GeometryCollection(boundary_lines))
|
||||
closed_polygons = shapely.polygonize(unioned_boundaries.geoms)
|
||||
|
||||
space_polygon = None
|
||||
for polygon in closed_polygons.geoms:
|
||||
if shapely.contains_xy(polygon, x, y):
|
||||
space_polygon = shapely.force_3d(polygon)
|
||||
|
||||
if not space_polygon:
|
||||
return
|
||||
|
||||
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], z])) for v in space_polygon.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]))
|
||||
|
||||
for interior in space_polygon.interiors:
|
||||
new_verts = [bm.verts.new(mat_invert @ Vector([v[0], v[1], z])) for v in interior.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)
|
||||
|
||||
mesh = bpy.data.meshes.new(name="Space")
|
||||
bm.to_mesh(mesh)
|
||||
bm.free()
|
||||
|
||||
if element and element.is_a("IfcSpace"):
|
||||
mesh.name = active_obj.data.name
|
||||
mesh.BIMMeshProperties.ifc_definition_id = active_obj.data.BIMMeshProperties.ifc_definition_id
|
||||
tool.Geometry.change_object_data(active_obj, mesh, is_global=True)
|
||||
tool.Ifc.edit(active_obj)
|
||||
else:
|
||||
if relating_type:
|
||||
name = tool.Model.generate_occurrence_name(relating_type, "IfcSpace")
|
||||
else:
|
||||
name = "Space"
|
||||
obj = bpy.data.objects.new("Space", mesh)
|
||||
obj.matrix_world = mat
|
||||
collection.objects.link(obj)
|
||||
bpy.ops.bim.assign_class(obj=obj.name, ifc_class="IfcSpace")
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if relating_type:
|
||||
blenderbim.core.type.assign_type(tool.Ifc, tool.Type, element=element, type=relating_type)
|
||||
|
||||
def is_bounding_class(self, element):
|
||||
for ifc_class in ["IfcWall", "IfcColumn", "IfcMember", "IfcVirtualElement", "IfcPlate"]:
|
||||
if element.is_a(ifc_class):
|
||||
return True
|
||||
return False
|
||||
|
||||
def create_mesh(self, shape):
|
||||
geometry = shape.geometry
|
||||
mesh = bpy.data.meshes.new("tmp")
|
||||
verts = geometry.verts
|
||||
if geometry.faces:
|
||||
num_vertices = len(verts) // 3
|
||||
total_faces = len(geometry.faces)
|
||||
loop_start = range(0, total_faces, 3)
|
||||
num_loops = total_faces // 3
|
||||
loop_total = [3] * num_loops
|
||||
num_vertex_indices = len(geometry.faces)
|
||||
|
||||
mesh.vertices.add(num_vertices)
|
||||
mesh.vertices.foreach_set("co", verts)
|
||||
mesh.loops.add(num_vertex_indices)
|
||||
mesh.loops.foreach_set("vertex_index", geometry.faces)
|
||||
mesh.polygons.add(num_loops)
|
||||
mesh.polygons.foreach_set("loop_start", loop_start)
|
||||
mesh.polygons.foreach_set("loop_total", loop_total)
|
||||
mesh.polygons.foreach_set("use_smooth", [0] * total_faces)
|
||||
mesh.update()
|
||||
return mesh
|
||||
core.generate_space(tool.Ifc, tool.Spatial, tool.Model, tool.Type)
|
||||
|
||||
|
||||
class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -230,10 +76,12 @@ class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator):
|
||||
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
|
||||
# In order to run, the active object must be a wall and
|
||||
# there must be selected walls
|
||||
|
||||
active_obj = bpy.context.active_object
|
||||
element = tool.Ifc.get_entity(active_obj)
|
||||
container = tool.Spatial.get_container(element)
|
||||
|
||||
if not active_obj:
|
||||
self.report({"ERROR"}, "No active object. Please select a wall")
|
||||
@@ -243,7 +91,6 @@ class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if element and not element.is_a("IfcWall"):
|
||||
return self.report({"ERROR"}, "The active object is not a wall. Please select a wall.")
|
||||
|
||||
container = ifcopenshell.util.element.get_container(element)
|
||||
if not container:
|
||||
self.report({"ERROR"}, "The wall is not contained.")
|
||||
|
||||
@@ -251,161 +98,7 @@ class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator):
|
||||
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)
|
||||
|
||||
union = self.get_purged_inner_holes_poly(union_geom=union, min_area=self.get_converted_tolerance(tolerance=3))
|
||||
|
||||
for i, linear_ring in enumerate(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)
|
||||
|
||||
if z != 0:
|
||||
obj.location = obj.location + Vector((0, 0, z))
|
||||
|
||||
context.view_layer.active_layer_collection.collection.objects.link(obj)
|
||||
bpy.ops.bim.assign_class(obj=obj.name, ifc_class="IfcSpace")
|
||||
container_obj = tool.Ifc.get_object(container)
|
||||
blenderbim.core.spatial.assign_container(
|
||||
tool.Ifc, tool.Collector, tool.Spatial, structure_obj=container_obj, element_obj=obj
|
||||
)
|
||||
|
||||
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") or subelement.is_a("IfcColumn"):
|
||||
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_purged_inner_holes_poly(self, union_geom, min_area):
|
||||
interiors_list = []
|
||||
|
||||
if union_geom.geom_type == "MultiPolygon":
|
||||
for poly in union_geom.geoms:
|
||||
interiors_list = self.get_poly_valid_interior_list(
|
||||
poly=poly, min_area=min_area, interiors_list=interiors_list
|
||||
)
|
||||
|
||||
new_poly = Polygon(poly.exterior.coords, holes=interiors_list)
|
||||
|
||||
if union_geom.geom_type == "Polygon":
|
||||
interiors_list = self.get_poly_valid_interior_list(
|
||||
poly=union_geom, min_area=min_area, interiors_list=interiors_list
|
||||
)
|
||||
new_poly = Polygon(union_geom.exterior.coords, holes=interiors_list)
|
||||
|
||||
return new_poly
|
||||
|
||||
def get_poly_valid_interior_list(self, poly, min_area, interiors_list):
|
||||
for interior in poly.interiors:
|
||||
p = Polygon(interior)
|
||||
if p.area >= min_area:
|
||||
interiors_list.append(interior)
|
||||
return interiors_list
|
||||
|
||||
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
|
||||
|
||||
core.generate_spaces_from_walls(tool.Ifc, tool.Spatial, tool.Collector)
|
||||
|
||||
class ToggleSpaceVisibility(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.toggle_space_visibility"
|
||||
@@ -414,26 +107,6 @@ class ToggleSpaceVisibility(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_description = "Change the space visibility"
|
||||
|
||||
def execute(cls, context):
|
||||
model = tool.Ifc.get()
|
||||
core.toggle_space_visibility(tool.Ifc, tool.Spatial)
|
||||
return {"FINISHED"}
|
||||
|
||||
spaces = model.by_type("IfcSpace")
|
||||
|
||||
if not spaces:
|
||||
print(spaces)
|
||||
return {"FINISHED"}
|
||||
|
||||
first_obj = tool.Ifc.get_object(spaces[0])
|
||||
|
||||
if bpy.data.objects[first_obj.name].display_type == "TEXTURED":
|
||||
for space in spaces:
|
||||
obj = tool.Ifc.get_object(space)
|
||||
bpy.data.objects[obj.name].show_wire = True
|
||||
bpy.data.objects[obj.name].display_type = "WIRE"
|
||||
return {"FINISHED"}
|
||||
|
||||
elif bpy.data.objects[first_obj.name].display_type == "WIRE":
|
||||
for space in spaces:
|
||||
obj = tool.Ifc.get_object(space)
|
||||
bpy.data.objects[obj.name].show_wire = False
|
||||
bpy.data.objects[obj.name].display_type = "TEXTURED"
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -17,22 +17,17 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import json
|
||||
import bmesh
|
||||
import ifcopenshell
|
||||
import blenderbim
|
||||
import blenderbim.tool as tool
|
||||
from mathutils import Vector
|
||||
from bmesh.types import BMVert
|
||||
from bpy.types import Operator
|
||||
from bpy.props import FloatProperty, IntProperty
|
||||
from bpy_extras.object_utils import AddObjectHelper, object_data_add
|
||||
|
||||
import bmesh
|
||||
from bmesh.types import BMVert
|
||||
|
||||
import ifcopenshell
|
||||
from ifcopenshell.util.shape_builder import V, ShapeBuilder
|
||||
import blenderbim
|
||||
import blenderbim.tool as tool
|
||||
|
||||
from mathutils import Vector
|
||||
from pprint import pprint
|
||||
import json
|
||||
|
||||
|
||||
def add_dumb_stair_object(self, context):
|
||||
verts = [
|
||||
@@ -82,138 +77,10 @@ class BIM_OT_add_object(Operator, AddObjectHelper):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def generate_stair_2d_profile(
|
||||
number_of_treads,
|
||||
height,
|
||||
width,
|
||||
tread_run,
|
||||
stair_type,
|
||||
# WOOD/STEEL CONCRETE STAIR ARGUMENTS
|
||||
tread_depth=None,
|
||||
# CONCRETE STAIR ARGUMENTS
|
||||
has_top_nib=None,
|
||||
top_slab_depth=None,
|
||||
base_slab_depth=None,
|
||||
):
|
||||
vertices = []
|
||||
edges = []
|
||||
faces = []
|
||||
|
||||
number_of_risers = number_of_treads + 1
|
||||
tread_rise = height / number_of_risers
|
||||
length = tread_run * number_of_risers
|
||||
|
||||
if stair_type == "WOOD/STEEL":
|
||||
builder = ShapeBuilder(None)
|
||||
tread_shape = builder.get_rectangle_coords(
|
||||
size=V(tread_run, 0, tread_depth),
|
||||
position=V(0, 0, -(tread_depth-tread_rise))
|
||||
)
|
||||
tread_offset = V(tread_run, 0, tread_rise)
|
||||
|
||||
for i in range(number_of_risers):
|
||||
cur_trade_shape = [v + tread_offset * i for v in tread_shape]
|
||||
vertices.extend(cur_trade_shape)
|
||||
|
||||
cur_vertex = i * 4
|
||||
edges.extend([
|
||||
(cur_vertex, cur_vertex+1),
|
||||
(cur_vertex+1, cur_vertex+2),
|
||||
(cur_vertex+2, cur_vertex+3),
|
||||
(cur_vertex+3, cur_vertex),
|
||||
])
|
||||
faces.append(list(range(cur_vertex, cur_vertex + 1)))
|
||||
|
||||
return (vertices, edges, faces)
|
||||
|
||||
elif stair_type == "GENERIC":
|
||||
vertices.append(Vector([0, 0, 0]))
|
||||
|
||||
tread_verts = [
|
||||
Vector([0, 0, tread_rise]),
|
||||
Vector([tread_run, 0, tread_rise])
|
||||
]
|
||||
tread_offset = Vector([tread_run, 0, tread_rise])
|
||||
|
||||
for i in range(number_of_risers):
|
||||
current_tread_verts = [v + tread_offset * i for v in tread_verts]
|
||||
last_vert_i = len(vertices) - 1
|
||||
edges.extend([
|
||||
(last_vert_i, last_vert_i + 1),
|
||||
(last_vert_i + 1, last_vert_i + 2)
|
||||
])
|
||||
vertices.extend(current_tread_verts)
|
||||
|
||||
last_vert_i = len(vertices)
|
||||
vertices.append(vertices[-1] * V(1,0,0))
|
||||
edges.extend([
|
||||
(last_vert_i - 1, last_vert_i),
|
||||
(last_vert_i, 0)
|
||||
])
|
||||
|
||||
return (vertices, edges, faces)
|
||||
|
||||
elif stair_type == "CONCRETE":
|
||||
for i in range(number_of_risers):
|
||||
vertices.extend([
|
||||
Vector((tread_run*i, 0, tread_rise*i)),
|
||||
Vector((tread_run*i, 0, tread_rise*(i+1)))
|
||||
])
|
||||
cur_vertex = i * 2
|
||||
if i != 0:
|
||||
edges.append((cur_vertex - 1, cur_vertex))
|
||||
edges.append((cur_vertex, cur_vertex + 1))
|
||||
|
||||
vertices.append(Vector((tread_run * number_of_risers, 0, tread_rise * number_of_risers)))
|
||||
edges.append((number_of_risers * 2, number_of_risers * 2 - 1))
|
||||
|
||||
td_vector = Vector((vertices[2][2], 0, -vertices[2][0])).normalized() * tread_depth
|
||||
|
||||
k = tread_rise / tread_run
|
||||
s0 = vertices[0] + td_vector
|
||||
b = s0.z - k * s0.x # comes from y = kx + b
|
||||
# you could use td_vector as depth_vector
|
||||
# but then stair won't be perpendicular to the X+
|
||||
# b is kind of vertical tread_depth (along Z+)
|
||||
depth_vector = Vector((0, 0, b))
|
||||
|
||||
# top nib
|
||||
if has_top_nib:
|
||||
vertices.append( vertices[number_of_risers * 2] + Vector((0, 0, -top_slab_depth)) )
|
||||
vertices.append( vertices[number_of_risers * 2] + Vector(((-top_slab_depth - b) / k, 0, -top_slab_depth)) )
|
||||
last_vertex_i = len(vertices) - 1
|
||||
edges.append( (number_of_risers * 2, last_vertex_i - 1) )
|
||||
edges.append( (last_vertex_i - 1, last_vertex_i) )
|
||||
else:
|
||||
vertices.append(vertices[number_of_risers * 2] + depth_vector)
|
||||
last_vertex_i = len(vertices) - 1
|
||||
edges.append((number_of_risers * 2, last_vertex_i))
|
||||
|
||||
top_nib_end = len(vertices) - 1
|
||||
|
||||
# bottom nib
|
||||
if abs(b) <= base_slab_depth:
|
||||
vertices.append(vertices[0] + depth_vector)
|
||||
edges.append((0, len(vertices) - 1))
|
||||
bottom_nib_end = len(vertices) - 1
|
||||
else:
|
||||
vertices.append(vertices[0] + Vector(((-base_slab_depth - b) / k, 0, -base_slab_depth)))
|
||||
vertices.append(vertices[0] + Vector((0, 0, -base_slab_depth)))
|
||||
last_vertex_i = len(vertices) - 1
|
||||
edges.append( (0, last_vertex_i) )
|
||||
edges.append( (last_vertex_i - 1, last_vertex_i) )
|
||||
bottom_nib_end = len(vertices) - 2
|
||||
|
||||
edges.append( (bottom_nib_end, top_nib_end) )
|
||||
faces = [list(range(len(vertices)))]
|
||||
|
||||
return (vertices, edges, faces)
|
||||
|
||||
|
||||
def update_stair_modifier(context):
|
||||
def regenerate_stair_mesh(context):
|
||||
obj = context.active_object
|
||||
props_kwargs = obj.BIMStairProperties.get_props_kwargs()
|
||||
vertices, edges, faces = generate_stair_2d_profile(**props_kwargs)
|
||||
vertices, edges, faces = tool.Model.generate_stair_2d_profile(**props_kwargs)
|
||||
|
||||
obj = context.active_object
|
||||
bm = bmesh.new()
|
||||
@@ -221,7 +88,7 @@ def update_stair_modifier(context):
|
||||
bm.edges.index_update()
|
||||
|
||||
new_verts = [bm.verts.new(v) for v in vertices]
|
||||
new_edges = [bm.edges.new( (new_verts[e[0]], new_verts[e[1]]) ) for e in edges]
|
||||
new_edges = [bm.edges.new((new_verts[e[0]], new_verts[e[1]])) for e in edges]
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
|
||||
@@ -242,6 +109,26 @@ def update_stair_modifier(context):
|
||||
obj.data.update()
|
||||
|
||||
|
||||
def update_stair_representation(obj):
|
||||
body = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
|
||||
representation = ifcopenshell.api.run(
|
||||
"geometry.add_representation",
|
||||
tool.Ifc.get(),
|
||||
context=body,
|
||||
blender_object=obj,
|
||||
geometry=obj.data,
|
||||
coordinate_offset=tool.Geometry.get_cartesian_point_coordinate_offset(obj),
|
||||
total_items=tool.Geometry.get_total_representation_items(obj),
|
||||
should_force_faceted_brep=tool.Geometry.should_force_faceted_brep(),
|
||||
should_force_triangulation=tool.Geometry.should_force_triangulation(),
|
||||
should_generate_uvs=tool.Geometry.should_generate_uvs(obj),
|
||||
ifc_representation_class=None,
|
||||
profile_set_usage=None,
|
||||
)
|
||||
tool.Model.replace_object_ifc_representation(body, obj, representation)
|
||||
tool.Ifc.finish_edit(obj)
|
||||
|
||||
|
||||
def update_ifc_stair_props(obj):
|
||||
"""should be called after new geometry settled
|
||||
since it's going to update ifc representation
|
||||
@@ -259,6 +146,7 @@ def update_ifc_stair_props(obj):
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
|
||||
riser_height = props.height / number_of_risers / si_conversion
|
||||
tread_length = props.tread_depth / si_conversion
|
||||
nosing_length = props.nosing_length / si_conversion
|
||||
|
||||
if element.is_a("IfcStairFlight"):
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
@@ -284,6 +172,7 @@ def update_ifc_stair_props(obj):
|
||||
"NumberOfTreads": props.number_of_treads,
|
||||
"RiserHeight": riser_height,
|
||||
"TreadLength": tread_length,
|
||||
"NosingLength": nosing_length,
|
||||
},
|
||||
)
|
||||
tool.Ifc.edit(obj)
|
||||
@@ -327,7 +216,7 @@ class BIM_OT_add_clever_stair(bpy.types.Operator, tool.Ifc.Operator):
|
||||
obj.location = spawn_location
|
||||
collection = context.view_layer.active_layer_collection.collection
|
||||
collection.objects.link(obj)
|
||||
|
||||
|
||||
body_context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
|
||||
element = blenderbim.core.root.assign_class(
|
||||
tool.Ifc,
|
||||
@@ -335,7 +224,7 @@ class BIM_OT_add_clever_stair(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tool.Root,
|
||||
obj=obj,
|
||||
ifc_class="IfcStairFlight",
|
||||
should_add_representation=True,
|
||||
should_add_representation=False,
|
||||
context=body_context,
|
||||
)
|
||||
if tool.Ifc.get_schema() != "IFC2X3":
|
||||
@@ -370,10 +259,11 @@ class AddStair(bpy.types.Operator, tool.Ifc.Operator):
|
||||
"pset.edit_pset",
|
||||
ifc_file,
|
||||
pset=pset,
|
||||
properties={"Data": json.dumps(stair_data)},
|
||||
properties={"Data": tool.Ifc.get().createIfcText(json.dumps(stair_data))},
|
||||
)
|
||||
update_stair_modifier(context)
|
||||
regenerate_stair_mesh(context)
|
||||
update_ifc_stair_props(obj)
|
||||
update_stair_representation(obj)
|
||||
|
||||
|
||||
class CancelEditingStair(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -388,7 +278,7 @@ class CancelEditingStair(bpy.types.Operator, tool.Ifc.Operator):
|
||||
props = obj.BIMStairProperties
|
||||
# restore previous settings since editing was canceled
|
||||
props.set_props_kwargs_from_ifc_data(data)
|
||||
update_stair_modifier(context)
|
||||
regenerate_stair_mesh(context)
|
||||
|
||||
props.is_editing = False
|
||||
|
||||
@@ -407,10 +297,11 @@ class FinishEditingStair(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
data = props.get_props_kwargs(convert_to_project_units=True)
|
||||
props.is_editing = False
|
||||
update_stair_modifier(context)
|
||||
regenerate_stair_mesh(context)
|
||||
update_stair_representation(obj)
|
||||
|
||||
pset = tool.Pset.get_element_pset(element, "BBIM_Stair")
|
||||
data = json.dumps(data)
|
||||
data = tool.Ifc.get().createIfcText(json.dumps(data))
|
||||
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Data": data})
|
||||
|
||||
# update IfcStairFlight properties
|
||||
|
||||
@@ -99,7 +99,7 @@ class DeleteSverchokGraph(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
def draw(self, context):
|
||||
self.layout.label(text="WARNING. The graph will be removed permamently.")
|
||||
self.layout.label(text="WARNING. The graph will be removed permanently.")
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
@@ -169,7 +169,7 @@ class UpdateDataFromSverchok(bpy.types.Operator, tool.Ifc.Operator):
|
||||
"pset.edit_pset",
|
||||
tool.Ifc.get(),
|
||||
pset=pset,
|
||||
properties={"Data": json.dumps(sverchok_data)},
|
||||
properties={"Data": tool.Ifc.get().createIfcText(json.dumps(sverchok_data))},
|
||||
)
|
||||
update_sverchok_modifier(context)
|
||||
|
||||
|
||||
@@ -30,12 +30,13 @@ from blenderbim.bim.module.model.data import (
|
||||
RoofData,
|
||||
)
|
||||
from blenderbim.bim.module.model.prop import get_ifc_class
|
||||
from blenderbim.bim.module.model.stair import update_stair_modifier
|
||||
from blenderbim.bim.module.model.stair import regenerate_stair_mesh
|
||||
from blenderbim.bim.module.model.window import update_window_modifier_bmesh
|
||||
from blenderbim.bim.module.model.door import update_door_modifier_bmesh
|
||||
from blenderbim.bim.module.model.railing import update_railing_modifier_bmesh
|
||||
from blenderbim.bim.module.model.roof import update_roof_modifier_bmesh
|
||||
from blenderbim.bim.helper import prop_with_search
|
||||
from collections.abc import Iterable
|
||||
|
||||
|
||||
class LaunchTypeManager(bpy.types.Operator):
|
||||
@@ -160,7 +161,7 @@ class BIM_PT_Grids(Panel):
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_project_setup"
|
||||
bl_parent_id = "BIM_PT_tab_project_setup"
|
||||
|
||||
def draw(self, context):
|
||||
self.layout.row().operator("mesh.add_grid", icon="ADD", text="Add Grids")
|
||||
@@ -188,10 +189,8 @@ class BIM_PT_array(bpy.types.Panel):
|
||||
if ArrayData.data["parameters"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=ArrayData.data["parameters"]["parent_name"], icon="CON_CHILDOF")
|
||||
op = row.operator("bim.select_array_parent", icon="OBJECT_DATA", text="")
|
||||
op.parent = ArrayData.data["parameters"]["Parent"]
|
||||
op = row.operator("bim.select_all_array_objects", icon="RESTRICT_SELECT_OFF", text="")
|
||||
op.parent = ArrayData.data["parameters"]["Parent"]
|
||||
row.operator("bim.select_array_parent", icon="OBJECT_DATA", text="")
|
||||
row.operator("bim.select_all_array_objects", icon="RESTRICT_SELECT_OFF", text="")
|
||||
|
||||
if ArrayData.data["parameters"]["data_dict"]:
|
||||
row.operator("bim.add_array", icon="ADD", text="")
|
||||
@@ -261,35 +260,42 @@ class BIM_PT_stair(bpy.types.Panel):
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Stair parameters", icon="IPO_CONSTANT")
|
||||
|
||||
stair_data = StairData.data["pset_data"]["data_dict"]
|
||||
if props.is_editing:
|
||||
calculated_params = tool.Model.get_active_stair_calculated_params()
|
||||
row = self.layout.row(align=True)
|
||||
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():
|
||||
self.layout.prop(props, prop_name)
|
||||
update_stair_modifier(context)
|
||||
prop_value = getattr(props, prop_name)
|
||||
if isinstance(prop_value, Iterable) and not isinstance(prop_value, str):
|
||||
prop_readable_name = props.bl_rna.properties[prop_name].name
|
||||
self.layout.label(text=f"{prop_readable_name}:")
|
||||
self.layout.prop(props, prop_name, text="")
|
||||
else:
|
||||
self.layout.prop(props, prop_name)
|
||||
regenerate_stair_mesh(context)
|
||||
else:
|
||||
calculated_params = StairData.data["calculated_params"]
|
||||
row.operator("bim.enable_editing_stair", icon="GREASEPENCIL", text="")
|
||||
row.operator("bim.remove_stair", icon="X", text="")
|
||||
row = self.layout.row(align=True)
|
||||
for prop_name, prop_value in StairData.data["general_params"].items():
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=prop_name)
|
||||
row.label(text=str(prop_value))
|
||||
if isinstance(prop_value, Iterable) and not isinstance(prop_value, str):
|
||||
row.label(text=f"{prop_name}:")
|
||||
row = self.layout.row(align=True)
|
||||
for prop_value_item in prop_value:
|
||||
row.label(text=str(prop_value_item))
|
||||
else:
|
||||
row.label(text=prop_name)
|
||||
row.label(text=str(prop_value))
|
||||
|
||||
# calculated properties
|
||||
number_of_rises = props.number_of_treads + 1
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Number of risers")
|
||||
row.label(text=str(number_of_rises))
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Tread rise")
|
||||
row.label(text=str(round(props.height / number_of_rises, 5)))
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Length")
|
||||
row.label(text=str(round(props.tread_run * number_of_rises, 5)))
|
||||
for prop_name, prop_value in calculated_params.items():
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=prop_name)
|
||||
row.label(text=str(prop_value))
|
||||
else:
|
||||
row = self.layout.row()
|
||||
row.label(text="No Stair Found")
|
||||
|
||||
@@ -637,6 +637,7 @@ class DumbWallGenerator:
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
tool.Blender.remove_data_block(mesh)
|
||||
pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="EPset_Parametric")
|
||||
ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Engine": "BlenderBIM.DumbLayer2"})
|
||||
obj.select_set(True)
|
||||
@@ -827,10 +828,10 @@ class DumbWallJoiner:
|
||||
return
|
||||
|
||||
for rel in element1.ConnectedTo:
|
||||
if rel.RelatingConnectionType in ["ATSTART", "ATEND"]:
|
||||
if rel.is_a("IfcRelConnectsPathElements") and rel.RelatingConnectionType in ["ATSTART", "ATEND"]:
|
||||
rel.RelatingConnectionType = "ATSTART" if rel.RelatingConnectionType == "ATEND" else "ATEND"
|
||||
for rel in element1.ConnectedFrom:
|
||||
if rel.RelatedConnectionType in ["ATSTART", "ATEND"]:
|
||||
if rel.is_a("IfcRelConnectsPathElements") and rel.RelatedConnectionType in ["ATSTART", "ATEND"]:
|
||||
rel.RelatedConnectionType = "ATSTART" if rel.RelatedConnectionType == "ATEND" else "ATEND"
|
||||
|
||||
layers1 = tool.Model.get_material_layer_parameters(element1)
|
||||
@@ -1261,8 +1262,13 @@ class DumbWallJoiner:
|
||||
results["is_sloped"] = True
|
||||
results["height"] = (item.Depth * self.unit_scale) / (1 / cos(results["x_angle"]))
|
||||
break
|
||||
elif item.is_a("IfcBooleanClippingResult"):
|
||||
elif item.is_a("IfcBooleanClippingResult"): # should be before IfcBooleanResult check
|
||||
item = item.FirstOperand
|
||||
elif item.is_a("IfcBooleanResult"):
|
||||
if item.FirstOperand.is_a("IfcExtrudedAreaSolid") or item.FirstOperand.is_a("IfcBooleanResult"):
|
||||
item = item.FirstOperand
|
||||
else:
|
||||
item = item.SecondOperand
|
||||
else:
|
||||
break
|
||||
return results
|
||||
@@ -1367,9 +1373,7 @@ class DumbWallJoiner:
|
||||
clamp_axis = clamp_axis[::-1]
|
||||
vectors = tuple([clamp_point_by_direction(v, clamp_axis) for v in vectors])
|
||||
|
||||
closest = tool.Cad.closest_vector(ref_point_2d.to_3d(), vectors)
|
||||
farthest = vectors[1] if closest == vectors[0] else vectors[0]
|
||||
return closest, farthest
|
||||
return tool.Cad.closest_and_furthest_vectors(ref_point_2d.to_3d(), vectors)
|
||||
|
||||
bbn, bbf = get_closest_and_furthest_vectors(axis1["base"][i], (bb1, bb2), bba1)
|
||||
bsn, bsf = get_closest_and_furthest_vectors(axis1["side"][i], (bs1, bs2))
|
||||
|
||||
@@ -68,9 +68,7 @@ def update_simple_openings(element, opening_width, 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]),
|
||||
position_x_axis=V(1, 0, 0),
|
||||
position_z_axis=V(0, -1, 0),
|
||||
extrusion_vector=V(0, 0, -1),
|
||||
**shape_builder.extrude_kwargs("Y")
|
||||
)
|
||||
|
||||
new_representation = shape_builder.get_representation(context, extrusion)
|
||||
@@ -83,6 +81,9 @@ def update_simple_openings(element, opening_width, opening_height):
|
||||
has_replaced_opening_representation = True
|
||||
|
||||
tool.Model.reload_body_representation(voided_objs)
|
||||
if fillings:
|
||||
with bpy.context.temp_override(selected_objects=[tool.Ifc.get_object(f) for f in fillings]):
|
||||
bpy.ops.bim.recalculate_fill()
|
||||
|
||||
|
||||
def update_window_modifier_representation(context, obj):
|
||||
@@ -118,6 +119,8 @@ def update_window_modifier_representation(context, obj):
|
||||
}
|
||||
representation_data["panel_properties"].append(panel_data)
|
||||
|
||||
previously_active_context = tool.Geometry.get_active_representation_context(obj)
|
||||
|
||||
# ELEVATION_VIEW representation
|
||||
profile = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Profile", "ELEVATION_VIEW")
|
||||
if profile:
|
||||
@@ -128,6 +131,7 @@ def update_window_modifier_representation(context, obj):
|
||||
tool.Model.replace_object_ifc_representation(profile, obj, elevation_representation)
|
||||
|
||||
# MODEL_VIEW representation
|
||||
# (Model/Body defined only BEFORE Plan/Body to prevent #2744)
|
||||
body = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
|
||||
representation_data["context"] = body
|
||||
model_representation = ifcopenshell.api.run("geometry.add_window_representation", ifc_file, **representation_data)
|
||||
@@ -142,13 +146,20 @@ def update_window_modifier_representation(context, obj):
|
||||
)
|
||||
tool.Model.replace_object_ifc_representation(plan, obj, plan_representation)
|
||||
|
||||
# adding switch representation at the end instead of changing order of representations
|
||||
# to prevent #2744
|
||||
# adding switch representation at the end instead of changing order of representations
|
||||
# to prevent #2744
|
||||
if tool.Geometry.get_active_representation_context(obj) != previously_active_context:
|
||||
previously_active_representation = ifcopenshell.util.representation.get_representation(
|
||||
element,
|
||||
previously_active_context.ContextType,
|
||||
previously_active_context.ContextIdentifier,
|
||||
previously_active_context.TargetView,
|
||||
)
|
||||
blenderbim.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=model_representation,
|
||||
representation=previously_active_representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=True,
|
||||
@@ -485,7 +496,7 @@ class AddWindow(bpy.types.Operator, tool.Ifc.Operator):
|
||||
"pset.edit_pset",
|
||||
tool.Ifc.get(),
|
||||
pset=pset,
|
||||
properties={"Data": json.dumps(window_data, default=list)},
|
||||
properties={"Data": tool.Ifc.get().createIfcText(json.dumps(window_data, default=list))},
|
||||
)
|
||||
update_window_modifier_representation(context, obj)
|
||||
return {"FINISHED"}
|
||||
@@ -542,7 +553,7 @@ class FinishEditingWindow(bpy.types.Operator, tool.Ifc.Operator):
|
||||
update_window_modifier_representation(context, obj)
|
||||
|
||||
pset = tool.Pset.get_element_pset(element, "BBIM_Window")
|
||||
window_data = json.dumps(window_data, default=list)
|
||||
window_data = tool.Ifc.get().createIfcText(json.dumps(window_data, default=list))
|
||||
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Data": window_data})
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -24,8 +24,9 @@ import blenderbim.tool as tool
|
||||
import blenderbim.bim.module.type.prop as type_prop
|
||||
from blenderbim.bim.helper import prop_with_search, close_operator_panel
|
||||
from bpy.types import WorkSpaceTool
|
||||
from blenderbim.bim.module.model.data import AuthoringData, RailingData, RoofData
|
||||
from blenderbim.bim.module.model.data import AuthoringData
|
||||
from blenderbim.bim.module.drawing.data import DecoratorData
|
||||
from blenderbim.bim.module.system.data import PortData
|
||||
from blenderbim.bim.module.model.prop import get_ifc_class
|
||||
|
||||
|
||||
@@ -218,6 +219,9 @@ class BimToolUI:
|
||||
row.label(text="No IFC Project", icon="ERROR")
|
||||
return
|
||||
|
||||
if not PortData.is_loaded:
|
||||
PortData.load()
|
||||
|
||||
if not AuthoringData.is_loaded:
|
||||
AuthoringData.load(ifc_element_type)
|
||||
elif ifc_element_type == "all" and AuthoringData.data["ifc_element_type"] is not None:
|
||||
@@ -347,20 +351,24 @@ class BimToolUI:
|
||||
"IfcDuctSegment",
|
||||
"IfcPipeSegment",
|
||||
):
|
||||
add_layout_hotkey_operator(cls.layout, "Extend", "S_E", "")
|
||||
add_layout_hotkey_operator(cls.layout, "Add Fitting", "S_F", "")
|
||||
|
||||
if context.region.type != "TOOL_HEADER":
|
||||
cls.layout.operator("bim.mep_add_bend")
|
||||
cls.layout.operator("bim.mep_add_transition")
|
||||
cls.layout.operator("bim.mep_add_obstruction")
|
||||
|
||||
else:
|
||||
add_layout_hotkey_operator(cls.layout, "Edit Axis", "A_E", "")
|
||||
add_layout_hotkey_operator(cls.layout, "Flip", "S_F", bpy.ops.bim.flip_object.__doc__)
|
||||
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__)
|
||||
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["pset_data"]
|
||||
and not context.active_object.BIMRailingProperties.is_editing_path
|
||||
tool.Model.is_parametric_railing_active() 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
|
||||
@@ -369,7 +377,8 @@ class BimToolUI:
|
||||
row.operator("bim.enable_editing_railing_path", text="Edit Railing Path")
|
||||
|
||||
elif AuthoringData.data["active_representation_type"] == "SweptSolid":
|
||||
add_layout_hotkey_operator(cls.layout, "Edit Profile", "S_E", "")
|
||||
if not tool.Model.is_parametric_window_active() and not tool.Model.is_parametric_door_active():
|
||||
add_layout_hotkey_operator(cls.layout, "Edit Profile", "S_E", "")
|
||||
|
||||
elif AuthoringData.data["active_class"] in (
|
||||
"IfcWindow",
|
||||
@@ -390,15 +399,17 @@ class BimToolUI:
|
||||
elif AuthoringData.data["active_class"] in ("IfcSpace",):
|
||||
add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.generate_space.__doc__)
|
||||
|
||||
elif (
|
||||
(RoofData.is_loaded or not RoofData.load())
|
||||
and RoofData.data["pset_data"]
|
||||
and not context.active_object.BIMRoofProperties.is_editing_path
|
||||
):
|
||||
elif tool.Model.is_parametric_roof_active() and not context.active_object.BIMRoofProperties.is_editing_path:
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon=f"EVENT_TAB")
|
||||
row.operator("bim.enable_editing_roof_path", text="Edit Roof Path")
|
||||
|
||||
if context.region.type != "TOOL_HEADER" and PortData.data["total_ports"] > 0:
|
||||
add_layout_hotkey_operator(
|
||||
cls.layout, "Regen MEP", "S_G", bpy.ops.bim.regenerate_distribution_element.__doc__
|
||||
)
|
||||
cls.layout.operator("bim.mep_connect_elements")
|
||||
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_O")
|
||||
@@ -416,11 +427,14 @@ class BimToolUI:
|
||||
row.operator("bim.show_openings", icon="HIDE_OFF", text="")
|
||||
|
||||
if AuthoringData.data["active_class"] in ("IfcOpeningElement",):
|
||||
row.operator("bim.edit_openings", icon="CHECKMARK", text="")
|
||||
row.operator("bim.hide_openings", icon="CANCEL", text="")
|
||||
if len(context.selected_objects) == 2:
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_L")
|
||||
row.operator("bim.clone_opening", text="Clone Opening")
|
||||
|
||||
|
||||
cls.layout.row(align=True).label(text="Align")
|
||||
add_layout_hotkey_operator(cls.layout, "Align Exterior", "S_X", "")
|
||||
@@ -432,6 +446,11 @@ class BimToolUI:
|
||||
add_layout_hotkey_operator(cls.layout, "Void", "A_O", "Toggle openings")
|
||||
add_layout_hotkey_operator(cls.layout, "Decomposition", "A_D", "Select decomposition")
|
||||
|
||||
cls.layout.separator()
|
||||
add_layout_hotkey_operator(
|
||||
cls.layout, "Calculate All Quantities", "S_Q", bpy.ops.bim.calculate_all_quantities.__doc__
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def draw_header_interface(cls):
|
||||
cls.draw_type_selection_interface()
|
||||
@@ -585,22 +604,20 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
# 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["pset_data"]
|
||||
tool.Model.is_parametric_railing_active()
|
||||
and not bpy.context.active_object.BIMRailingProperties.is_editing_path
|
||||
):
|
||||
bpy.ops.bim.enable_editing_railing_path()
|
||||
return
|
||||
|
||||
elif (
|
||||
(RoofData.is_loaded or not RoofData.load())
|
||||
and RoofData.data["pset_data"]
|
||||
and not bpy.context.active_object.BIMRoofProperties.is_editing_path
|
||||
):
|
||||
elif tool.Model.is_parametric_roof_active() and not bpy.context.active_object.BIMRoofProperties.is_editing_path:
|
||||
# undo the unselection done above because roof has no usage type
|
||||
bpy.ops.bim.enable_editing_roof_path()
|
||||
return
|
||||
|
||||
elif tool.Model.is_parametric_window_active() or tool.Model.is_parametric_door_active():
|
||||
return
|
||||
|
||||
selected_usages = {}
|
||||
for obj in bpy.context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
@@ -610,6 +627,7 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
usage = tool.Model.get_usage_type(element)
|
||||
if not usage:
|
||||
representation = tool.Geometry.get_active_representation(obj)
|
||||
representation = tool.Geometry.resolve_mapped_representation(representation)
|
||||
if representation and representation.RepresentationType == "SweptSolid":
|
||||
usage = "SWEPTSOLID"
|
||||
else:
|
||||
@@ -663,18 +681,30 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bpy.ops.bim.flip_wall()
|
||||
elif self.active_class in ("IfcWindow", "IfcWindowStandardCase", "IfcDoor", "IfcDoorStandardCase"):
|
||||
bpy.ops.bim.flip_fill()
|
||||
elif self.active_class in ("IfcBeam", "IfcColumn"):
|
||||
bpy.ops.bim.flip_object(flip_local_axes="XZ")
|
||||
elif self.active_class in ("IfcDuctSegment", "IfcPipeSegment", "IfcCableCarrierSegment", "IfcCableSegment"):
|
||||
bpy.ops.bim.fit_flow_segments()
|
||||
|
||||
def hotkey_S_G(self):
|
||||
obj = bpy.context.active_object
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not bpy.context.selected_objects:
|
||||
if self.props.ifc_class == "IfcSpaceType":
|
||||
bpy.ops.bim.generate_space()
|
||||
return
|
||||
if self.active_material_usage == "LAYER2":
|
||||
bpy.ops.bim.recalculate_wall()
|
||||
elif tool.System.get_ports(element):
|
||||
bpy.ops.bim.regenerate_distribution_element()
|
||||
elif self.active_material_usage == "PROFILE":
|
||||
bpy.ops.bim.recalculate_profile()
|
||||
if self.active_class not in (
|
||||
"IfcCableCarrierSegment",
|
||||
"IfcCableSegment",
|
||||
"IfcDuctSegment",
|
||||
"IfcPipeSegment",
|
||||
):
|
||||
bpy.ops.bim.recalculate_profile()
|
||||
elif self.active_class in ("IfcWindow", "IfcWindowStandardCase", "IfcDoor", "IfcDoorStandardCase"):
|
||||
bpy.ops.bim.recalculate_fill()
|
||||
elif self.active_class in ("IfcSpace"):
|
||||
|
||||
@@ -83,7 +83,7 @@ class BIM_PT_people(bpy.types.Panel):
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_project_setup"
|
||||
bl_parent_id = "BIM_PT_tab_stakeholders"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@@ -142,7 +142,7 @@ class BIM_PT_organisations(bpy.types.Panel):
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_project_setup"
|
||||
bl_parent_id = "BIM_PT_tab_stakeholders"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@@ -189,7 +189,7 @@ class BIM_PT_owner(bpy.types.Panel):
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_project_setup"
|
||||
bl_parent_id = "BIM_PT_tab_stakeholders"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@@ -240,7 +240,7 @@ class BIM_PT_actor(bpy.types.Panel):
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
bl_parent_id = "BIM_PT_project_setup"
|
||||
bl_parent_id = "BIM_PT_tab_stakeholders"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
|
||||
@@ -223,7 +223,9 @@ class PurgeUnusedProfiles(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMProfileProperties
|
||||
core.purge_unused_profiles(tool.Ifc, tool.Profile)
|
||||
purged_profiles = core.purge_unused_profiles(tool.Ifc, tool.Profile)
|
||||
self.report({"INFO"}, f"{purged_profiles} profiles were purged.")
|
||||
|
||||
if props.is_editing:
|
||||
refresh()
|
||||
bpy.ops.bim.load_profiles()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user