Merge branch 'IfcOpenShell:v0.7.0' into v0.7.0

This commit is contained in:
Carlos Dias
2023-09-27 10:55:52 -03:00
committed by GitHub
247 changed files with 9377 additions and 5602 deletions
-1
View File
@@ -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 |
+67 -80
View File
@@ -36,35 +36,49 @@ endif()
set(EXTRA_VERSION "-alpha.3")
option(MINIMAL_BUILD "The build is to make a minimal version of IFC converter from OCCT into IFC." OFF)
option(COLLADA_SUPPORT "Build IfcConvert with COLLADA support (requires OpenCOLLADA)." ON)
option(IFCXML_SUPPORT "Build IfcParse with ifcXML support (requires libxml2)." ON)
option(WASM_BUILD "Build a WebAssembly binary." OFF)
option(ENABLE_BUILD_OPTIMIZATIONS "Enable certain compiler and linker optimizations on RelWithDebInfo and Release builds." OFF)
option(BUILD_PACKAGE "" OFF)
option(BUILD_IFCGEOM "Build IfcGeom." ON)
option(BUILD_DOCUMENTATION "Build IfcOpenShell Documentation." OFF)
option(BUILD_IFCPYTHON "Build IfcPython." ON)
option(BUILD_EXAMPLES "Build example applications." ON)
option(BUILD_GEOMSERVER "Build IfcGeomServer executable." ON)
option(BUILD_CONVERT "Build IfcConvert executable." ON)
option(HDF5_SUPPORT "Enable HDF5 support (requires HDF5, zlib)" ON)
option(USE_VLD "Use Visual Leak Detector for debugging memory leaks, MSVC-only." OFF)
option(USE_MMAP "Adds a command line options to parse IFC files from memory mapped files using Boost.Iostreams" OFF)
option(BUILD_SHARED_LIBS "Build IfcParse and IfcGeom as shared libs (SO/DLL)." OFF)
option(MSVC_PARALLEL_BUILD "Multi-threaded compilation in Microsoft Visual Studio (/MP)" OFF)
option(USE_VLD "Use Visual Leak Detector for debugging memory leaks, MSVC-only." OFF)
option(USE_MMAP "Adds a command line options to parse IFC files from memory mapped files using Boost.Iostreams" OFF)
option(NO_WARN "Disable all warnings" OFF)
option(WASM_BUILD OFF)
if(${BUILD_CONVERT})
option(GLTF_SUPPORT "Build IfcConvert with glTF support (requires json.hpp)." OFF)
option(USD_SUPPORT "Build IfcConvert with USD support (requires pixar's USD library)." OFF)
endif()
option(BUILD_IFCGEOM "Build IfcGeom." ON)
option(BUILD_IFCPYTHON "Build IfcPython." ON)
option(BUILD_CONVERT "Build IfcConvert executable." ON)
option(BUILD_DOCUMENTATION "Build IfcOpenShell Documentation." OFF)
option(BUILD_EXAMPLES "Build example applications." ON)
option(BUILD_GEOMSERVER "Build IfcGeomServer executable." ON)
option(BUILD_IFCMAX "Build IfcMax, a 3ds Max plug-in, Windows-only." OFF)
option(BUILD_QTVIEWER "Build IfcOpenShell Qt GUI Viewer" OFF) # QtViewer requires Qt6
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(IFCXML_SUPPORT "Build IfcParse with ifcXML support (requires libxml2)." ON)
option(USD_SUPPORT "Build IfcConvert with USD support (requires pixar's USD library)." OFF)
option(USERSPACE_PYTHON_PREFIX "Installs IfcPython for the current user only instead of system-wide." OFF)
option(ADD_COMMIT_SHA "Add commit sha and branch in version number, warning results in many rebuilds, requires git" OFF)
option(BUILD_IFCMAX "Build IfcMax, a 3ds Max plug-in, Windows-only." OFF)
# QtViewer requires Qt6
option(BUILD_QTVIEWER "Build IfcOpenShell Qt GUI Viewer" OFF)
if(MINIMAL_BUILD)
message(STATUS "Setting options for minimal build")
set(BUILD_GEOMSERVER OFF)
set(BUILD_IFCPYTHON OFF)
set(COLLADA_SUPPORT OFF)
set(GLTF_SUPPORT OFF)
set(HDF5_SUPPORT OFF)
set(IFCXML_SUPPORT OFF)
set(USD_SUPPORT OFF)
endif()
if((BUILD_CONVERT OR BUILD_GEOMSERVER OR BUILD_IFCPYTHON) AND(NOT BUILD_IFCGEOM))
message(STATUS "'IfcGeom' is required with current outputs")
set(BUILD_IFCGEOM ON)
endif()
if(MSVC AND MSVC_PARALLEL_BUILD)
add_definitions("/MP")
@@ -80,50 +94,37 @@ endif()
include(GNUInstallDirs)
if((BUILD_CONVERT OR BUILD_GEOMSERVER OR BUILD_IFCPYTHON) AND(NOT BUILD_IFCGEOM))
message(STATUS "'IfcGeom' is required with current outputs")
set(BUILD_IFCGEOM ON)
endif()
# Specify where to install files
# Specify paths to install files
if(NOT BINDIR)
set(BINDIR bin)
endif()
if(NOT IS_ABSOLUTE ${BINDIR})
set(BINDIR ${CMAKE_INSTALL_BINDIR})
endif()
message(STATUS "BINDIR: ${BINDIR}")
if(NOT INCLUDEDIR)
set(INCLUDEDIR include)
endif()
if(NOT IS_ABSOLUTE ${INCLUDEDIR})
set(INCLUDEDIR ${CMAKE_INSTALL_INCLUDEDIR})
endif()
message(STATUS "INCLUDEDIR: ${INCLUDEDIR}")
if(NOT LIBDIR)
set(LIBDIR lib)
endif()
if(NOT IS_ABSOLUTE ${LIBDIR})
set(LIBDIR ${CMAKE_INSTALL_LIBDIR})
endif()
message(STATUS "LIBDIR: ${LIBDIR}")
set(IFCOPENSHELL_LIBRARY_DIR "") # for *nix rpaths
if(BUILD_SHARED_LIBS)
add_definitions(-DIFC_SHARED_BUILD)
if(MSVC)
message(WARNING "Building DLLs against the static VC run-time. This is not recommended if the DLLs are to be redistributed.")
# C4521: 'identifier' : class 'type' needs to have dll-interface to be used by clients of class 'type2'
# There will be couple hundreds of these so suppress them away, https://msdn.microsoft.com/en-us/library/esew7y1w.aspx
add_definitions(-wd4251)
@@ -170,7 +171,7 @@ if(WASM_BUILD)
set(CMAKE_FIND_ROOT_PATH "")
endif()
if(NOT MINIMAL_BUILD AND GLTF_SUPPORT AND BUILD_CONVERT)
if(GLTF_SUPPORT AND BUILD_CONVERT)
UNIFY_ENVVARS_AND_CACHE(JSON_INCLUDE_DIR)
find_file(json_hpp "json.hpp" ${JSON_INCLUDE_DIR}/nlohmann)
@@ -185,7 +186,7 @@ if(NOT MINIMAL_BUILD AND GLTF_SUPPORT AND BUILD_CONVERT)
endif()
# Add USD support to serializers
if(NOT MINIMAL_BUILD AND USD_SUPPORT)
if(USD_SUPPORT)
UNIFY_ENVVARS_AND_CACHE(USD_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(USD_LIBRARY_DIR)
@@ -303,7 +304,7 @@ if(NOT MINIMAL_BUILD)
find_package(LibXml2 REQUIRED)
endif()
if(NOT MINIMAL_BUILD AND IFCXML_SUPPORT)
if(IFCXML_SUPPORT)
add_definitions(-DWITH_IFCXML)
set(SWIG_DEFINES ${SWIG_DEFINES} -DWITH_IFCXML)
endif()
@@ -452,7 +453,7 @@ if(BUILD_IFCGEOM)
endif(BUILD_IFCGEOM)
if(NOT MINIMAL_BUILD AND COLLADA_SUPPORT)
if(COLLADA_SUPPORT)
# Find OpenCOLLADA
if("${OPENCOLLADA_INCLUDE_DIR}" STREQUAL "")
message(STATUS "No OpenCOLLADA include directory specified")
@@ -536,7 +537,7 @@ if(NOT MINIMAL_BUILD AND COLLADA_SUPPORT)
endif()
endif()
if(NOT MINIMAL_BUILD AND HDF5_SUPPORT)
if(HDF5_SUPPORT)
if("${HDF5_INCLUDE_DIR}" STREQUAL "")
message(STATUS "No HDF5 include directory specified")
else()
@@ -876,20 +877,19 @@ else()
target_link_libraries(IfcParse ${Boost_LIBRARIES} ${BCRYPT_LIBRARIES} ${LIBXML2_LIBRARIES})
endif()
if(BUILD_IFCGEOM)
# IfcGeom
file(GLOB IFCGEOM_H_FILES ../src/ifcgeom/*.h ../src/ifcgeom/*.i)
file(GLOB IFCGEOM_CPP_FILES ../src/ifcgeom/*.cpp)
set(IFCGEOM_FILES ${IFCGEOM_CPP_FILES} ${IFCGEOM_H_FILES})
foreach(schema ${SCHEMA_VERSIONS})
add_library(IfcGeom_ifc${schema} STATIC ${IFCGEOM_FILES})
set_target_properties(IfcGeom_ifc${schema} PROPERTIES COMPILE_FLAGS "-DIFC_GEOM_EXPORTS -DIfcSchema=Ifc${schema}")
if (NOT WASM_BUILD)
target_link_libraries(IfcGeom_ifc${schema} IfcParse ${OPENCASCADE_LIBRARIES})
endif()
endforeach()
foreach(schema ${SCHEMA_VERSIONS})
add_library(IfcGeom_ifc${schema} STATIC ${IFCGEOM_FILES})
set_target_properties(IfcGeom_ifc${schema} PROPERTIES COMPILE_FLAGS "-DIFC_GEOM_EXPORTS -DIfcSchema=Ifc${schema}")
if (NOT WASM_BUILD)
target_link_libraries(IfcGeom_ifc${schema} IfcParse ${OPENCASCADE_LIBRARIES})
endif()
endforeach()
# IfcGeom (schema agnostic)
file(GLOB SCHEMA_AGNOSTIC_H_FILES ../src/ifcgeom_schema_agnostic/*.h)
@@ -903,11 +903,11 @@ endforeach()
find_package(Threads)
endif()
if (WASM_BUILD)
target_link_libraries(IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
else()
target_link_libraries(IfcGeom IfcParse ${IFCGEOM_SCHEMA_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
endif()
if (WASM_BUILD)
target_link_libraries(IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
else()
target_link_libraries(IfcGeom IfcParse ${IFCGEOM_SCHEMA_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
endif()
endif(BUILD_IFCGEOM)
@@ -920,16 +920,16 @@ if(BUILD_CONVERT OR BUILD_IFCPYTHON)
file(GLOB SERIALIZERS_S_CPP_FILES ../src/serializers/schema_dependent/*.cpp)
set(SERIALIZERS_S_FILES ${SERIALIZERS_S_H_FILES} ${SERIALIZERS_S_CPP_FILES})
foreach(schema ${SCHEMA_VERSIONS})
add_library(Serializers_ifc${schema} STATIC ${SERIALIZERS_S_FILES})
set_target_properties(Serializers_ifc${schema} PROPERTIES COMPILE_FLAGS "-DIFC_GEOM_EXPORTS -DIfcSchema=Ifc${schema}")
if (WASM_BUILD)
target_link_libraries(Serializers_ifc${schema} ${HDF5_LIBRARIES})
else()
target_link_libraries(Serializers_ifc${schema} IfcGeom ${OPENCASCADE_LIBRARIES} ${HDF5_LIBRARIES})
endif()
endforeach()
foreach(schema ${SCHEMA_VERSIONS})
add_library(Serializers_ifc${schema} STATIC ${SERIALIZERS_S_FILES})
set_target_properties(Serializers_ifc${schema} PROPERTIES COMPILE_FLAGS "-DIFC_GEOM_EXPORTS -DIfcSchema=Ifc${schema}")
if (WASM_BUILD)
target_link_libraries(Serializers_ifc${schema} ${HDF5_LIBRARIES})
else()
target_link_libraries(Serializers_ifc${schema} IfcGeom ${OPENCASCADE_LIBRARIES} ${HDF5_LIBRARIES})
endif()
endforeach()
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}")
@@ -962,7 +962,7 @@ if(BUILD_CONVERT)
endif(BUILD_CONVERT)
# IfcGeomServer
if(NOT MINIMAL_BUILD AND BUILD_GEOMSERVER)
if(BUILD_GEOMSERVER)
file(GLOB CPP_FILES ../src/ifcgeomserver/*.cpp)
file(GLOB H_FILES ../src/ifcgeomserver/*.h)
set(SOURCE_FILES ${CPP_FILES} ${H_FILES})
@@ -1019,7 +1019,7 @@ if(BUILD_DOCUMENTATION)
add_subdirectory(../docs docs)
endif()
if(NOT MINIMAL_BUILD AND BUILD_IFCPYTHON)
if(BUILD_IFCPYTHON)
add_subdirectory(../src/ifcwrap ifcwrap)
endif()
@@ -1027,21 +1027,8 @@ if(BUILD_EXAMPLES)
add_subdirectory(../src/examples examples)
endif()
if(NOT MINIMAL_BUILD AND BUILD_IFCMAX)
foreach(max_year RANGE 2014 2030)
set(max_sdk "$ENV{ADSK_3DSMAX_SDK_${max_year}}")
if(NOT "${max_sdk}" STREQUAL "")
message(STATUS "Autodesk 3ds Max SDK found at ${max_sdk}")
set(HAS_MAX TRUE)
endif()
endforeach()
if(HAS_MAX)
add_subdirectory(../src/ifcmax ifcmax)
else()
message(STATUS "Autodesk 3ds Max SDK not found, is required to build IFCMax.")
endif()
if(BUILD_IFCMAX)
add_subdirectory(../src/ifcmax ifcmax)
endif()
if(NOT MINIMAL_BUILD)
+3 -82
View File
@@ -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`.
+4 -10
View File
@@ -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
+3 -1
View File
@@ -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,
@@ -47,6 +47,13 @@
<g id="dot">
<circle r="1" fill="black" stroke="black" style="stroke-width: 0.25;" />
</g>
<g id="setout-tag">
<circle fill="white" stroke="black" r="2.5" style="stroke-width: 0.5"/>
<path d="M 0 0 L -2.5 0 A 2.5 2.5 0 0 1 0 -2.5 Z" fill="black"/>
<path d="M 0 0 L 2.5 0 A 2.5 2.5 0 0 1 0 2.5 Z" fill="black"/>
<text x="3" y="-8" class="regular" text-anchor="start" dominant-baseline="auto" data-type="text-template"></text>
<text x="3" y="-4" class="regular" text-anchor="start" dominant-baseline="auto" data-type="text-template"></text>
</g>
<g id="setout-point">
<circle fill="white" stroke="black" r="2.5" style="stroke-width: 0.5"/>
<path d="M 0 0 L -2.5 0 A 2.5 2.5 0 0 1 0 -2.5 Z" fill="black"/>

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

@@ -1,17 +1,17 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('ViewDefinition[DesignTransferView]'),'2;1');
FILE_NAME('/dev/null','2023-09-02T16:09:02+10:00',(),(),'IfcOpenShell v0.7.0-fbd8ea1ed','IfcOpenShell v0.7.0-fbd8ea1ed','Nobody');
FILE_NAME('/dev/null','2023-09-07T13:40:06+10:00',(),(),'IfcOpenShell v0.7.0-fbd8ea1ed','IfcOpenShell v0.7.0-fbd8ea1ed','Nobody');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPROJECT('1UlLTVwDzDvR$QNEAxFx6F',$,'BlenderBIM Demo',$,$,$,$,(#12,#16),#7);
#2=IFCPROJECTLIBRARY('3QN02XEh1FQh0fii364VEi',$,'BlenderBIM Demo Library',$,$,$,$,$,$);
#3=IFCRELDECLARES('0aiEyU5lL7mAdB5xwdXbUN',$,$,$,#1,(#2));
#1=IFCPROJECT('1arVsMni92iPUaaryGTUz2',$,'BlenderBIM Demo',$,$,$,$,(#12,#16),#7);
#2=IFCPROJECTLIBRARY('1FfDG5b21DuBDOO4eUE9rF',$,'BlenderBIM Demo Library',$,$,$,$,$,$);
#3=IFCRELDECLARES('3tfDIC3Tf9wP8EUOoFeycK',$,$,$,#1,(#2));
#4=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
#5=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.);
#6=IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.);
#7=IFCUNITASSIGNMENT((#6,#4,#5));
#7=IFCUNITASSIGNMENT((#5,#6,#4));
#8=IFCCARTESIANPOINT((0.,0.,0.));
#9=IFCDIRECTION((0.,0.,1.));
#10=IFCDIRECTION((1.,0.,0.));
@@ -25,82 +25,82 @@ DATA;
#18=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Body','Plan',*,*,*,*,#16,$,.PLAN_VIEW.,$);
#19=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Annotation','Plan',*,*,*,*,#16,$,.PLAN_VIEW.,$);
#20=IFCMATERIAL('Unknown',$,$);
#21=IFCWALLTYPE('0Ir6HewafB2hnPRigxrUnM',$,'WAL50',$,$,$,$,$,$,.NOTDEFINED.);
#21=IFCWALLTYPE('2o8NqQQbHA5BeErrgwfr7d',$,'WAL50',$,$,$,$,$,$,.NOTDEFINED.);
#22=IFCMATERIALLAYERSET((#24),$,$);
#23=IFCRELASSOCIATESMATERIAL('3S$YbWQCnDCA2tmoikSnrh',$,$,$,(#21),#22);
#23=IFCRELASSOCIATESMATERIAL('1Q$5DZKr98n82Eitz2dWey',$,$,$,(#21),#22);
#24=IFCMATERIALLAYER(#20,0.05,$,$,$,$,$);
#25=IFCRELDECLARES('1eLCJqssP2kvQIdmPlRK19',$,$,$,#2,(#38,#82,#77,#42,#26,#48,#72,#59,#92,#30,#96,#63,#54,#87,#153,#34,#67,#21,#210));
#26=IFCWALLTYPE('2oUFJayonDlBTcbJtxYFSv',$,'WAL100',$,$,$,$,$,$,.NOTDEFINED.);
#25=IFCRELDECLARES('1KXKUEn8z0g8lvyEsCTWD3',$,$,$,#2,(#38,#82,#77,#42,#26,#48,#72,#59,#92,#30,#96,#63,#21,#54,#87,#153,#34,#67,#210));
#26=IFCWALLTYPE('3KlIOv_P9A79tkt8D_jq_m',$,'WAL100',$,$,$,$,$,$,.NOTDEFINED.);
#27=IFCMATERIALLAYERSET((#29),$,$);
#28=IFCRELASSOCIATESMATERIAL('0n3vPKxufDgO48f4yfGtUd',$,$,$,(#26),#27);
#28=IFCRELASSOCIATESMATERIAL('32B9Wx8Z1FxhIN7m77MiHG',$,$,$,(#26),#27);
#29=IFCMATERIALLAYER(#20,0.1,$,$,$,$,$);
#30=IFCWALLTYPE('0UDhWxnIXDMOxZ2yPDCUMP',$,'WAL200',$,$,$,$,$,$,.NOTDEFINED.);
#30=IFCWALLTYPE('2DqrSeel1AGw6h3b$ujafZ',$,'WAL200',$,$,$,$,$,$,.NOTDEFINED.);
#31=IFCMATERIALLAYERSET((#33),$,$);
#32=IFCRELASSOCIATESMATERIAL('2Boxz3uaP0LOy6luKau2R0',$,$,$,(#30),#31);
#32=IFCRELASSOCIATESMATERIAL('2FeYUpt3D4tAvwgGmwXWZn',$,$,$,(#30),#31);
#33=IFCMATERIALLAYER(#20,0.2,$,$,$,$,$);
#34=IFCWALLTYPE('20fPutWQrFyeMIiKxc$MkU',$,'WAL300',$,$,$,$,$,$,.NOTDEFINED.);
#34=IFCWALLTYPE('2jNdEOFIP1eQb3WZePlFGY',$,'WAL300',$,$,$,$,$,$,.NOTDEFINED.);
#35=IFCMATERIALLAYERSET((#37),$,$);
#36=IFCRELASSOCIATESMATERIAL('2qAjlTTxjBkvdp80A9XdMZ',$,$,$,(#34),#35);
#36=IFCRELASSOCIATESMATERIAL('2bFeM6xevAwucuwMlauX77',$,$,$,(#34),#35);
#37=IFCMATERIALLAYER(#20,0.3,$,$,$,$,$);
#38=IFCCOVERINGTYPE('100hdtcxzBdQATomAi0tv5',$,'COV10',$,$,$,$,$,$,.NOTDEFINED.);
#38=IFCCOVERINGTYPE('3310gCgH59w9tu$DfmiCEN',$,'COV10',$,$,$,$,$,$,.NOTDEFINED.);
#39=IFCMATERIALLAYERSET((#41),$,$);
#40=IFCRELASSOCIATESMATERIAL('3tO1xRn9b06utMa1TZC$WW',$,$,$,(#38),#39);
#40=IFCRELASSOCIATESMATERIAL('3pTNmWd_jBoOWyHc8Yf4qI',$,$,$,(#38),#39);
#41=IFCMATERIALLAYER(#20,0.01,$,$,$,$,$);
#42=IFCCOVERINGTYPE('1sO51oul95ABWezHr7P7Dk',$,'COV20',$,$,(#46),$,$,$,.NOTDEFINED.);
#42=IFCCOVERINGTYPE('2sksWFuPzCgeNk1ofXS6PZ',$,'COV20',$,$,(#46),$,$,$,.NOTDEFINED.);
#43=IFCMATERIALLAYERSET((#45),$,$);
#44=IFCRELASSOCIATESMATERIAL('0w6lexCg19TuiqqTxo91zc',$,$,$,(#42),#43);
#44=IFCRELASSOCIATESMATERIAL('36rl5NddT1ux6ejkQAkT2z',$,$,$,(#42),#43);
#45=IFCMATERIALLAYER(#20,0.02,$,$,$,$,$);
#46=IFCPROPERTYSET('3ytgFxH0571uBCG4mBNXI9',$,'EPset_Parametric',$,(#47));
#46=IFCPROPERTYSET('2VZJ9xfdn1NeOY6uEmnnYW',$,'EPset_Parametric',$,(#47));
#47=IFCPROPERTYSINGLEVALUE('LayerSetDirection',$,IFCLABEL('AXIS2'),$);
#48=IFCCOVERINGTYPE('09GKC1aO56qwEPF9halcxJ',$,'COV30',$,$,(#52),$,$,$,.NOTDEFINED.);
#48=IFCCOVERINGTYPE('1uU6rxt95AC91lZ0jTRG6G',$,'COV30',$,$,(#52),$,$,$,.NOTDEFINED.);
#49=IFCMATERIALLAYERSET((#51),$,$);
#50=IFCRELASSOCIATESMATERIAL('0rDrnpiX58DgQhcURj_WlB',$,$,$,(#48),#49);
#50=IFCRELASSOCIATESMATERIAL('2lFv4xQYr5bwtiSsMTVCgz',$,$,$,(#48),#49);
#51=IFCMATERIALLAYER(#20,0.03,$,$,$,$,$);
#52=IFCPROPERTYSET('14u6OUyPL5y8O$QkEnB6Rr',$,'EPset_Parametric',$,(#53));
#52=IFCPROPERTYSET('0HnrRhcd16Yfbgtci4sqCG',$,'EPset_Parametric',$,(#53));
#53=IFCPROPERTYSINGLEVALUE('LayerSetDirection',$,IFCLABEL('AXIS3'),$);
#54=IFCRAMPTYPE('2G4JrZ99j2B9SkIwWDk0sC',$,'RAM200',$,$,$,$,$,$,.NOTDEFINED.);
#54=IFCRAMPTYPE('3nonXCVn58jhVV08N8c97B',$,'RAM200',$,$,$,$,$,$,.NOTDEFINED.);
#55=IFCMATERIALLAYERSET((#57),$,$);
#56=IFCRELASSOCIATESMATERIAL('2nNlFzNPj0_h6_NwdVJvW$',$,$,$,(#54),#55);
#56=IFCRELASSOCIATESMATERIAL('2NdIrmbUP6bh4x$p_RY31C',$,$,$,(#54),#55);
#57=IFCMATERIALLAYER(#20,0.2,$,$,$,$,$);
#58=IFCCIRCLEPROFILEDEF(.AREA.,$,$,0.3);
#59=IFCPILETYPE('3uIc5iHjP5Lel3X6KXyN8W',$,'P1',$,$,$,$,$,$,.NOTDEFINED.);
#59=IFCPILETYPE('2uuf5kq5TDDfO9jGN7XIHh',$,'P1',$,$,$,$,$,$,.NOTDEFINED.);
#60=IFCMATERIALPROFILESET($,$,(#62),$);
#61=IFCRELASSOCIATESMATERIAL('13buLvs1r5vAyhgbabb2g1',$,$,$,(#59),#60);
#61=IFCRELASSOCIATESMATERIAL('3hi821Ffj8YR9yFjp226LU',$,$,$,(#59),#60);
#62=IFCMATERIALPROFILE($,$,#20,#58,$,$);
#63=IFCSLABTYPE('2CbahfxvnFpPoHyMBpGTQm',$,'FLR150',$,$,$,$,$,$,.NOTDEFINED.);
#63=IFCSLABTYPE('2janmz3nH4P9IHaOVvGY_B',$,'FLR150',$,$,$,$,$,$,.NOTDEFINED.);
#64=IFCMATERIALLAYERSET((#66),$,$);
#65=IFCRELASSOCIATESMATERIAL('2Irb7$9xL8YOXW99sF0SOp',$,$,$,(#63),#64);
#65=IFCRELASSOCIATESMATERIAL('0og5Zg4ib67g6XWgOgqBWy',$,$,$,(#63),#64);
#66=IFCMATERIALLAYER(#20,0.2,$,$,$,$,$);
#67=IFCSLABTYPE('04dx2Vg4vEp9ZOKGI1ifWb',$,'FLR250',$,$,$,$,$,$,.NOTDEFINED.);
#67=IFCSLABTYPE('26UKpEaTb9fh4qrqi3Ymzj',$,'FLR250',$,$,$,$,$,$,.NOTDEFINED.);
#68=IFCMATERIALLAYERSET((#70),$,$);
#69=IFCRELASSOCIATESMATERIAL('04tl3Me6T6xOb$WEvv_dL0',$,$,$,(#67),#68);
#69=IFCRELASSOCIATESMATERIAL('1BeBXPqen9TQEE42SxzhjF',$,$,$,(#67),#68);
#70=IFCMATERIALLAYER(#20,0.3,$,$,$,$,$);
#71=IFCRECTANGLEPROFILEDEF(.AREA.,'500x600',$,0.5,0.6);
#72=IFCCOLUMNTYPE('13rk_6wmDApeVQWfrArKbl',$,'C1',$,$,$,$,$,$,.NOTDEFINED.);
#72=IFCCOLUMNTYPE('1KnWzBBoLCNfJ3_J79LT7d',$,'C1',$,$,$,$,$,$,.NOTDEFINED.);
#73=IFCMATERIALPROFILESET($,$,(#75),$);
#74=IFCRELASSOCIATESMATERIAL('0X3OUScT9E1BjOvJWiUrv3',$,$,$,(#72),#73);
#74=IFCRELASSOCIATESMATERIAL('1W8OWQjEL05gH5hEpve6Lh',$,$,$,(#72),#73);
#75=IFCMATERIALPROFILE($,$,#20,#71,$,$);
#76=IFCCIRCLEHOLLOWPROFILEDEF(.AREA.,'500.0x5.0 CHS',$,0.25,0.005);
#77=IFCCOLUMNTYPE('0IaS5T1Uv6DQu$BJ5I3bks',$,'C2',$,$,$,$,$,$,.NOTDEFINED.);
#77=IFCCOLUMNTYPE('33jPFERfL9bfTeWfTL5kZ7',$,'C2',$,$,$,$,$,$,.NOTDEFINED.);
#78=IFCMATERIALPROFILESET($,$,(#80),$);
#79=IFCRELASSOCIATESMATERIAL('1HWcDe7a17WwPeKCHnckid',$,$,$,(#77),#78);
#79=IFCRELASSOCIATESMATERIAL('3czOrQw2v9BfS0PeQCrzWq',$,$,$,(#77),#78);
#80=IFCMATERIALPROFILE($,$,#20,#76,$,$);
#81=IFCRECTANGLEHOLLOWPROFILEDEF(.AREA.,'150x75x2.0 RHS',$,0.075,0.15,0.002,0.005,0.005);
#82=IFCCOLUMNTYPE('13p6$J2Jv84RFI5QAp6FTQ',$,'C3',$,$,$,$,$,$,.NOTDEFINED.);
#82=IFCCOLUMNTYPE('21YcNdKNj2$95U55yNx1Nw',$,'C3',$,$,$,$,$,$,.NOTDEFINED.);
#83=IFCMATERIALPROFILESET($,$,(#85),$);
#84=IFCRELASSOCIATESMATERIAL('1GWCzHY657$9kUJrD0AEir',$,$,$,(#82),#83);
#84=IFCRELASSOCIATESMATERIAL('3n6Cru3xr6jvDptU9u7QEF',$,$,$,(#82),#83);
#85=IFCMATERIALPROFILE($,$,#20,#81,$,$);
#86=IFCISHAPEPROFILEDEF(.AREA.,'DEMO-I',$,0.1,0.2,0.005,0.01,0.005,$,$);
#87=IFCBEAMTYPE('1Y_PtOyJP9YfZkU59r$vs1',$,'B1',$,$,$,$,$,$,.NOTDEFINED.);
#87=IFCBEAMTYPE('2CsGpD$6nDCxWv9jXTtvq9',$,'B1',$,$,$,$,$,$,.NOTDEFINED.);
#88=IFCMATERIALPROFILESET($,$,(#90),$);
#89=IFCRELASSOCIATESMATERIAL('2CfmfKJ0X45xijxsDUHZ85',$,$,$,(#87),#88);
#89=IFCRELASSOCIATESMATERIAL('2LJExc74j9rgBSBx51HDPK',$,$,$,(#87),#88);
#90=IFCMATERIALPROFILE($,$,#20,#86,$,$);
#91=IFCCSHAPEPROFILEDEF(.AREA.,'DEMO-C',$,0.2,0.1,0.0015,0.03,0.005);
#92=IFCBEAMTYPE('2qogXGNrb9EANilnZjnGjq',$,'B2',$,$,$,$,$,$,.NOTDEFINED.);
#92=IFCBEAMTYPE('32pHN2b0P0awGw$U8PpneO',$,'B2',$,$,$,$,$,$,.NOTDEFINED.);
#93=IFCMATERIALPROFILESET($,$,(#95),$);
#94=IFCRELASSOCIATESMATERIAL('3$1pGIrVLA2RTOmJIsbelu',$,$,$,(#92),#93);
#94=IFCRELASSOCIATESMATERIAL('2wg8R1Drb2xhQ2Y_pRSh3c',$,$,$,(#92),#93);
#95=IFCMATERIALPROFILE($,$,#20,#91,$,$);
#96=IFCWINDOWTYPE('1Dc9pAExL3fxL9jScEo2EF',$,'WT01',$,$,$,(#135,#152),$,$,.NOTDEFINED.,.NOTDEFINED.,$,$);
#96=IFCWINDOWTYPE('1Gg9HfZEr69u8SdWfDs2J3',$,'WT01',$,$,$,(#135,#152),$,$,.NOTDEFINED.,.NOTDEFINED.,$,$);
#97=IFCINDEXEDPOLYGONALFACE((13,17,18,14));
#98=IFCINDEXEDPOLYGONALFACE((5,6,3,4));
#99=IFCINDEXEDPOLYGONALFACE((7,8,2,1));
@@ -157,7 +157,7 @@ DATA;
#150=IFCDIRECTION((0.,0.,1.));
#151=IFCAXIS2PLACEMENT3D(#148,#150,#149);
#152=IFCREPRESENTATIONMAP(#151,#147);
#153=IFCDOORTYPE('05Z1DhJjj11wczPRkvXSPE',$,'DT01',$,$,$,(#196,#209),$,$,.NOTDEFINED.,.NOTDEFINED.,$,$);
#153=IFCDOORTYPE('0NBUmPKyT9WecsIeYJrEqg',$,'DT01',$,$,$,(#196,#209),$,$,.NOTDEFINED.,.NOTDEFINED.,$,$);
#154=IFCINDEXEDPOLYGONALFACE((17,16,15,14));
#155=IFCINDEXEDPOLYGONALFACE((2,3,29,28));
#156=IFCINDEXEDPOLYGONALFACE((27,28,29,30,32,31));
@@ -214,7 +214,7 @@ DATA;
#207=IFCDIRECTION((0.,0.,1.));
#208=IFCAXIS2PLACEMENT3D(#205,#207,#206);
#209=IFCREPRESENTATIONMAP(#208,#204);
#210=IFCFURNITURETYPE('3ZMmSB_rfBnhrBckPTFKvP',$,'BUN01',$,$,$,(#931,#952),$,$,.NOTDEFINED.,.NOTDEFINED.);
#210=IFCFURNITURETYPE('3Kyc6IyarAUw3_8fkNtXIg',$,'BUN01',$,$,$,(#931,#952),$,$,.NOTDEFINED.,.NOTDEFINED.);
#211=IFCINDEXEDPOLYGONALFACE((187,278,44));
#212=IFCINDEXEDPOLYGONALFACE((21,52,60));
#213=IFCINDEXEDPOLYGONALFACE((91,100,31));
@@ -957,140 +957,161 @@ DATA;
#950=IFCDIRECTION((0.,0.,1.));
#951=IFCAXIS2PLACEMENT3D(#948,#950,#949);
#952=IFCREPRESENTATIONMAP(#951,#947);
#953=IFCTYPEPRODUCT('1hmWolVQb9buX2GhHwaVFB',$,'SETOUT-POINT',$,'IfcAnnotation/SYMBOL',(#954),$,$);
#954=IFCPROPERTYSET('2YNCFr62b8vhuhHGspQl4$',$,'EPset_Annotation',$,(#955));
#953=IFCTYPEPRODUCT('0TBMBnD_b66QLUWKD9HLsc',$,'SETOUT-POINT',$,'IfcAnnotation/SYMBOL',(#954),$,$);
#954=IFCPROPERTYSET('2dQzX_K4r1Xet6dz9zBlgs',$,'EPset_Annotation',$,(#955));
#955=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('setout-point'),$);
#956=IFCTYPEPRODUCT('3H4oE1a_9AwQE8OFsCjdfh',$,'CONTROL-POINT',$,'IfcAnnotation/SYMBOL',(#957),$,$);
#957=IFCPROPERTYSET('0ARWSOH$z3kQDRkpEPHWCt',$,'EPset_Annotation',$,(#958));
#956=IFCTYPEPRODUCT('0vo_7PU3H9ygTnrIipqPRI',$,'CONTROL-POINT',$,'IfcAnnotation/SYMBOL',(#957),$,$);
#957=IFCPROPERTYSET('3APQOw$FP1ivxp08UxHsl6',$,'EPset_Annotation',$,(#958));
#958=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('control-point'),$);
#959=IFCTYPEPRODUCT('3YDPnyrWn2wRWgRzb0V7Kx',$,'TRAVERSE-POINT',$,'IfcAnnotation/SYMBOL',(#960),$,$);
#960=IFCPROPERTYSET('1v$MzHU1b82hDqHdtz4e5y',$,'EPset_Annotation',$,(#961));
#959=IFCTYPEPRODUCT('32V27G3$T1OO3TbXg6948F',$,'TRAVERSE-POINT',$,'IfcAnnotation/SYMBOL',(#960),$,$);
#960=IFCPROPERTYSET('0cTSYXMc9B$PTXVDrhBYYW',$,'EPset_Annotation',$,(#961));
#961=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('traverse-point'),$);
#962=IFCTYPEPRODUCT('31Em8$VNL0Mh$9EZ3pXjpL',$,'DASHED',$,'IfcAnnotation/LINEWORK',(#963),$,$);
#963=IFCPROPERTYSET('0K84$piRb31PmkAwZfP5kd',$,'EPset_Annotation',$,(#964));
#962=IFCTYPEPRODUCT('2PXIC7Bg914gJhRh2XeGnN',$,'DASHED',$,'IfcAnnotation/LINEWORK',(#963),$,$);
#963=IFCPROPERTYSET('0RiYsOxp529gXnCkw44wF8',$,'EPset_Annotation',$,(#964));
#964=IFCPROPERTYSINGLEVALUE('Classes',$,IFCLABEL('dashed'),$);
#965=IFCTYPEPRODUCT('3P0K0gDxP4NgwnuLGqlyW6',$,'FINE',$,'IfcAnnotation/LINEWORK',(#966),$,$);
#966=IFCPROPERTYSET('179vwKcVv9uet4_kI4NT9G',$,'EPset_Annotation',$,(#967));
#965=IFCTYPEPRODUCT('1Ou1kA3Vb4HhuNBvM0uGe0',$,'FINE',$,'IfcAnnotation/LINEWORK',(#966),$,$);
#966=IFCPROPERTYSET('2gzly9D0L1qPK2MExeXgRO',$,'EPset_Annotation',$,(#967));
#967=IFCPROPERTYSINGLEVALUE('Classes',$,IFCLABEL('fine'),$);
#968=IFCTYPEPRODUCT('0Rh30iszn41xBCgQJl2w2c',$,'THIN',$,'IfcAnnotation/LINEWORK',(#969),$,$);
#969=IFCPROPERTYSET('0bZV_Tby121uj5O3n08l1l',$,'EPset_Annotation',$,(#970));
#968=IFCTYPEPRODUCT('3lx$KQPRbEZwbQ7Xdfm5gw',$,'THIN',$,'IfcAnnotation/LINEWORK',(#969),$,$);
#969=IFCPROPERTYSET('0vsVpqs6zArvA2bfBtvQew',$,'EPset_Annotation',$,(#970));
#970=IFCPROPERTYSINGLEVALUE('Classes',$,IFCLABEL('thin'),$);
#971=IFCTYPEPRODUCT('3ti_6xztr7qPp9dASz9vP4',$,'MEDIUM',$,'IfcAnnotation/LINEWORK',(#972),$,$);
#972=IFCPROPERTYSET('19ZhF3bxL07evL8gi27Kei',$,'EPset_Annotation',$,(#973));
#971=IFCTYPEPRODUCT('00j$y97p903w2HOb35lAQ2',$,'MEDIUM',$,'IfcAnnotation/LINEWORK',(#972),$,$);
#972=IFCPROPERTYSET('240rEBDOn8PAvfnm_43s55',$,'EPset_Annotation',$,(#973));
#973=IFCPROPERTYSINGLEVALUE('Classes',$,IFCLABEL('medium'),$);
#974=IFCTYPEPRODUCT('2hlcxjzxv8zf0gh1md23xY',$,'THICK',$,'IfcAnnotation/LINEWORK',(#975),$,$);
#975=IFCPROPERTYSET('0wRtpbInzE2AkqX8RIreQr',$,'EPset_Annotation',$,(#976));
#974=IFCTYPEPRODUCT('2tVdFGorj6dfrL4uA1kyW8',$,'THICK',$,'IfcAnnotation/LINEWORK',(#975),$,$);
#975=IFCPROPERTYSET('2IEGHncr1D$R8fKA9zCEJP',$,'EPset_Annotation',$,(#976));
#976=IFCPROPERTYSINGLEVALUE('Classes',$,IFCLABEL('thick'),$);
#977=IFCTYPEPRODUCT('1pefU$PA1ENw7kZG9T3mjr',$,'STRONG',$,'IfcAnnotation/LINEWORK',(#978),$,$);
#978=IFCPROPERTYSET('1RpJZBysn8shqevvAquPQ8',$,'EPset_Annotation',$,(#979));
#977=IFCTYPEPRODUCT('1T5C$$ONTBB8A9a7vv0Gn6',$,'STRONG',$,'IfcAnnotation/LINEWORK',(#978),$,$);
#978=IFCPROPERTYSET('1gRomAS1L2WguA51ZI0E40',$,'EPset_Annotation',$,(#979));
#979=IFCPROPERTYSINGLEVALUE('Classes',$,IFCLABEL('strong'),$);
#980=IFCTYPEPRODUCT('1HKC8zCIv5_ef3dEfa$L5N',$,'DOOR-TAG',$,'IfcAnnotation/TEXT',(#981),(#1000),$);
#981=IFCPROPERTYSET('0aC8JrqFX6HhKUhjdLw4mN',$,'EPset_Annotation',$,(#982));
#982=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('door-tag'),$);
#980=IFCTYPEPRODUCT('2LmKYrAEr2K8EwJyCAsyc4',$,'SETOUT-TAG',$,'IfcAnnotation/TEXT',(#981),(#1000),$);
#981=IFCPROPERTYSET('0nF9du8qDAF9aldqjCUsbX',$,'EPset_Annotation',$,(#982));
#982=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('setout-tag'),$);
#983=IFCCARTESIANPOINT((0.,0.,0.));
#984=IFCDIRECTION((0.,0.,1.));
#985=IFCDIRECTION((1.,0.,0.));
#986=IFCAXIS2PLACEMENT3D(#983,#984,#985);
#987=IFCPLANAREXTENT(1000.,1000.);
#988=IFCTEXTLITERALWITHEXTENT('{{type.Name}}',#986,.RIGHT.,#987,'center');
#988=IFCTEXTLITERALWITHEXTENT('E ``round({{easting}}, 0.001)``',#986,.RIGHT.,#987,'center');
#989=IFCCARTESIANPOINT((0.,0.,0.));
#990=IFCDIRECTION((0.,0.,1.));
#991=IFCDIRECTION((1.,0.,0.));
#992=IFCAXIS2PLACEMENT3D(#989,#990,#991);
#993=IFCPLANAREXTENT(1000.,1000.);
#994=IFCTEXTLITERALWITHEXTENT('{{Name}}',#992,.RIGHT.,#993,'center');
#994=IFCTEXTLITERALWITHEXTENT('N ``round({{northing}}, 0.001)``',#992,.RIGHT.,#993,'center');
#995=IFCSHAPEREPRESENTATION(#19,'Annotation','Annotation2D',(#988,#994));
#996=IFCCARTESIANPOINT((0.,0.,0.));
#997=IFCDIRECTION((1.,0.,0.));
#998=IFCDIRECTION((0.,0.,1.));
#999=IFCAXIS2PLACEMENT3D(#996,#998,#997);
#1000=IFCREPRESENTATIONMAP(#999,#995);
#1001=IFCTYPEPRODUCT('2Jy_wl$FX8h9mgYT1$Y9oc',$,'WINDOW-TAG',$,'IfcAnnotation/TEXT',(#1002),(#1015),$);
#1002=IFCPROPERTYSET('2ncCyO$LPCzv7kqXZK_Omh',$,'EPset_Annotation',$,(#1003));
#1003=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('window-tag'),$);
#1001=IFCTYPEPRODUCT('1cdhVmqEPF6e13_TFOdlQw',$,'DOOR-TAG',$,'IfcAnnotation/TEXT',(#1002),(#1021),$);
#1002=IFCPROPERTYSET('1MUPGQolnDEgp0NxcMN56E',$,'EPset_Annotation',$,(#1003));
#1003=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('door-tag'),$);
#1004=IFCCARTESIANPOINT((0.,0.,0.));
#1005=IFCDIRECTION((0.,0.,1.));
#1006=IFCDIRECTION((1.,0.,0.));
#1007=IFCAXIS2PLACEMENT3D(#1004,#1005,#1006);
#1008=IFCPLANAREXTENT(1000.,1000.);
#1009=IFCTEXTLITERALWITHEXTENT('{{Name}}',#1007,.RIGHT.,#1008,'center');
#1010=IFCSHAPEREPRESENTATION(#19,'Annotation','Annotation2D',(#1009));
#1011=IFCCARTESIANPOINT((0.,0.,0.));
#1009=IFCTEXTLITERALWITHEXTENT('{{type.Name}}',#1007,.RIGHT.,#1008,'center');
#1010=IFCCARTESIANPOINT((0.,0.,0.));
#1011=IFCDIRECTION((0.,0.,1.));
#1012=IFCDIRECTION((1.,0.,0.));
#1013=IFCDIRECTION((0.,0.,1.));
#1014=IFCAXIS2PLACEMENT3D(#1011,#1013,#1012);
#1015=IFCREPRESENTATIONMAP(#1014,#1010);
#1016=IFCTYPEPRODUCT('1LTLrGzxH4qhuWGNeTlTRi',$,'SPACE-TAG',$,'IfcAnnotation/TEXT',(#1017),(#1042),$);
#1017=IFCPROPERTYSET('2JpTIsSZf3zQUWK48RLIcL',$,'EPset_Annotation',$,(#1018));
#1018=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('space-tag'),$);
#1019=IFCCARTESIANPOINT((0.,0.,0.));
#1020=IFCDIRECTION((0.,0.,1.));
#1021=IFCDIRECTION((1.,0.,0.));
#1022=IFCAXIS2PLACEMENT3D(#1019,#1020,#1021);
#1023=IFCPLANAREXTENT(1000.,1000.);
#1024=IFCTEXTLITERALWITHEXTENT('{{Name}}',#1022,.RIGHT.,#1023,'center');
#1013=IFCAXIS2PLACEMENT3D(#1010,#1011,#1012);
#1014=IFCPLANAREXTENT(1000.,1000.);
#1015=IFCTEXTLITERALWITHEXTENT('{{Name}}',#1013,.RIGHT.,#1014,'center');
#1016=IFCSHAPEREPRESENTATION(#19,'Annotation','Annotation2D',(#1009,#1015));
#1017=IFCCARTESIANPOINT((0.,0.,0.));
#1018=IFCDIRECTION((1.,0.,0.));
#1019=IFCDIRECTION((0.,0.,1.));
#1020=IFCAXIS2PLACEMENT3D(#1017,#1019,#1018);
#1021=IFCREPRESENTATIONMAP(#1020,#1016);
#1022=IFCTYPEPRODUCT('0OuDi3gRH2CxW9mrtE0vXw',$,'WINDOW-TAG',$,'IfcAnnotation/TEXT',(#1023),(#1036),$);
#1023=IFCPROPERTYSET('2X7dAo_1P5ruRnlKA4kIl6',$,'EPset_Annotation',$,(#1024));
#1024=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('window-tag'),$);
#1025=IFCCARTESIANPOINT((0.,0.,0.));
#1026=IFCDIRECTION((0.,0.,1.));
#1027=IFCDIRECTION((1.,0.,0.));
#1028=IFCAXIS2PLACEMENT3D(#1025,#1026,#1027);
#1029=IFCPLANAREXTENT(1000.,1000.);
#1030=IFCTEXTLITERALWITHEXTENT('{{Description}}',#1028,.RIGHT.,#1029,'center');
#1031=IFCCARTESIANPOINT((0.,0.,0.));
#1032=IFCDIRECTION((0.,0.,1.));
#1030=IFCTEXTLITERALWITHEXTENT('{{Name}}',#1028,.RIGHT.,#1029,'center');
#1031=IFCSHAPEREPRESENTATION(#19,'Annotation','Annotation2D',(#1030));
#1032=IFCCARTESIANPOINT((0.,0.,0.));
#1033=IFCDIRECTION((1.,0.,0.));
#1034=IFCAXIS2PLACEMENT3D(#1031,#1032,#1033);
#1035=IFCPLANAREXTENT(1000.,1000.);
#1036=IFCTEXTLITERALWITHEXTENT('``round({{Qto_SpaceBaseQuantities.NetFloorArea}} or 0., 2)``',#1034,.RIGHT.,#1035,'center');
#1037=IFCSHAPEREPRESENTATION(#19,'Annotation','Annotation2D',(#1024,#1030,#1036));
#1038=IFCCARTESIANPOINT((0.,0.,0.));
#1039=IFCDIRECTION((1.,0.,0.));
#1040=IFCDIRECTION((0.,0.,1.));
#1041=IFCAXIS2PLACEMENT3D(#1038,#1040,#1039);
#1042=IFCREPRESENTATIONMAP(#1041,#1037);
#1043=IFCTYPEPRODUCT('3Or$DhaVf6ygGBUpRb2PMs',$,'MATERIAL-TAG',$,'IfcAnnotation/TEXT',(#1044),(#1057),$);
#1044=IFCPROPERTYSET('2YfvIc6dPDdwWjERHZZlof',$,'EPset_Annotation',$,(#1045));
#1045=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('rectangle-tag'),$);
#1034=IFCDIRECTION((0.,0.,1.));
#1035=IFCAXIS2PLACEMENT3D(#1032,#1034,#1033);
#1036=IFCREPRESENTATIONMAP(#1035,#1031);
#1037=IFCTYPEPRODUCT('3WEV_9wQn6AQTESgjW36PH',$,'SPACE-TAG',$,'IfcAnnotation/TEXT',(#1038),(#1063),$);
#1038=IFCPROPERTYSET('02FAX8dIzAlunWD5ak$6sU',$,'EPset_Annotation',$,(#1039));
#1039=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('space-tag'),$);
#1040=IFCCARTESIANPOINT((0.,0.,0.));
#1041=IFCDIRECTION((0.,0.,1.));
#1042=IFCDIRECTION((1.,0.,0.));
#1043=IFCAXIS2PLACEMENT3D(#1040,#1041,#1042);
#1044=IFCPLANAREXTENT(1000.,1000.);
#1045=IFCTEXTLITERALWITHEXTENT('{{Name}}',#1043,.RIGHT.,#1044,'center');
#1046=IFCCARTESIANPOINT((0.,0.,0.));
#1047=IFCDIRECTION((0.,0.,1.));
#1048=IFCDIRECTION((1.,0.,0.));
#1049=IFCAXIS2PLACEMENT3D(#1046,#1047,#1048);
#1050=IFCPLANAREXTENT(1000.,1000.);
#1051=IFCTEXTLITERALWITHEXTENT('{{material.Name}}',#1049,.RIGHT.,#1050,'center');
#1052=IFCSHAPEREPRESENTATION(#19,'Annotation','Annotation2D',(#1051));
#1053=IFCCARTESIANPOINT((0.,0.,0.));
#1051=IFCTEXTLITERALWITHEXTENT('{{Description}}',#1049,.RIGHT.,#1050,'center');
#1052=IFCCARTESIANPOINT((0.,0.,0.));
#1053=IFCDIRECTION((0.,0.,1.));
#1054=IFCDIRECTION((1.,0.,0.));
#1055=IFCDIRECTION((0.,0.,1.));
#1056=IFCAXIS2PLACEMENT3D(#1053,#1055,#1054);
#1057=IFCREPRESENTATIONMAP(#1056,#1052);
#1058=IFCTYPEPRODUCT('01Hhk_DbjDROhyr91Hp$aV',$,'TYPE-TAG',$,'IfcAnnotation/TEXT',(#1059),(#1072),$);
#1059=IFCPROPERTYSET('0LyRD7lHr17uJo7XNzCvZq',$,'EPset_Annotation',$,(#1060));
#1060=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('capsule-tag'),$);
#1061=IFCCARTESIANPOINT((0.,0.,0.));
#1062=IFCDIRECTION((0.,0.,1.));
#1063=IFCDIRECTION((1.,0.,0.));
#1064=IFCAXIS2PLACEMENT3D(#1061,#1062,#1063);
#1065=IFCPLANAREXTENT(1000.,1000.);
#1066=IFCTEXTLITERALWITHEXTENT('{{type.Name}}',#1064,.RIGHT.,#1065,'center');
#1067=IFCSHAPEREPRESENTATION(#19,'Annotation','Annotation2D',(#1066));
#1068=IFCCARTESIANPOINT((0.,0.,0.));
#1055=IFCAXIS2PLACEMENT3D(#1052,#1053,#1054);
#1056=IFCPLANAREXTENT(1000.,1000.);
#1057=IFCTEXTLITERALWITHEXTENT('``round({{Qto_SpaceBaseQuantities.NetFloorArea}}, 0.01)``',#1055,.RIGHT.,#1056,'center');
#1058=IFCSHAPEREPRESENTATION(#19,'Annotation','Annotation2D',(#1045,#1051,#1057));
#1059=IFCCARTESIANPOINT((0.,0.,0.));
#1060=IFCDIRECTION((1.,0.,0.));
#1061=IFCDIRECTION((0.,0.,1.));
#1062=IFCAXIS2PLACEMENT3D(#1059,#1061,#1060);
#1063=IFCREPRESENTATIONMAP(#1062,#1058);
#1064=IFCTYPEPRODUCT('1evqvQLLL1zxdsIWqEn0lK',$,'MATERIAL-TAG',$,'IfcAnnotation/TEXT',(#1065),(#1078),$);
#1065=IFCPROPERTYSET('2KPJlGyer0UBmphfR7952k',$,'EPset_Annotation',$,(#1066));
#1066=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('rectangle-tag'),$);
#1067=IFCCARTESIANPOINT((0.,0.,0.));
#1068=IFCDIRECTION((0.,0.,1.));
#1069=IFCDIRECTION((1.,0.,0.));
#1070=IFCDIRECTION((0.,0.,1.));
#1071=IFCAXIS2PLACEMENT3D(#1068,#1070,#1069);
#1072=IFCREPRESENTATIONMAP(#1071,#1067);
#1073=IFCTYPEPRODUCT('0a8MHfTUzA5AV8H6p1bqC9',$,'NAME-TAG',$,'IfcAnnotation/TEXT',(#1074),(#1087),$);
#1074=IFCPROPERTYSET('2EboL9u1j68Q_Mp8dBa7ux',$,'EPset_Annotation',$,(#1075));
#1075=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('capsule-tag'),$);
#1076=IFCCARTESIANPOINT((0.,0.,0.));
#1077=IFCDIRECTION((0.,0.,1.));
#1078=IFCDIRECTION((1.,0.,0.));
#1079=IFCAXIS2PLACEMENT3D(#1076,#1077,#1078);
#1080=IFCPLANAREXTENT(1000.,1000.);
#1081=IFCTEXTLITERALWITHEXTENT('{{Name}}',#1079,.RIGHT.,#1080,'center');
#1082=IFCSHAPEREPRESENTATION(#19,'Annotation','Annotation2D',(#1081));
#1083=IFCCARTESIANPOINT((0.,0.,0.));
#1070=IFCAXIS2PLACEMENT3D(#1067,#1068,#1069);
#1071=IFCPLANAREXTENT(1000.,1000.);
#1072=IFCTEXTLITERALWITHEXTENT('{{material.Name}}',#1070,.RIGHT.,#1071,'center');
#1073=IFCSHAPEREPRESENTATION(#19,'Annotation','Annotation2D',(#1072));
#1074=IFCCARTESIANPOINT((0.,0.,0.));
#1075=IFCDIRECTION((1.,0.,0.));
#1076=IFCDIRECTION((0.,0.,1.));
#1077=IFCAXIS2PLACEMENT3D(#1074,#1076,#1075);
#1078=IFCREPRESENTATIONMAP(#1077,#1073);
#1079=IFCTYPEPRODUCT('3Nem6d4xX87O3deyWDi3AW',$,'TYPE-TAG',$,'IfcAnnotation/TEXT',(#1080),(#1093),$);
#1080=IFCPROPERTYSET('15bspgnA9CEuFox6xFjoTL',$,'EPset_Annotation',$,(#1081));
#1081=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('capsule-tag'),$);
#1082=IFCCARTESIANPOINT((0.,0.,0.));
#1083=IFCDIRECTION((0.,0.,1.));
#1084=IFCDIRECTION((1.,0.,0.));
#1085=IFCDIRECTION((0.,0.,1.));
#1086=IFCAXIS2PLACEMENT3D(#1083,#1085,#1084);
#1087=IFCREPRESENTATIONMAP(#1086,#1082);
#1085=IFCAXIS2PLACEMENT3D(#1082,#1083,#1084);
#1086=IFCPLANAREXTENT(1000.,1000.);
#1087=IFCTEXTLITERALWITHEXTENT('{{type.Name}}',#1085,.RIGHT.,#1086,'center');
#1088=IFCSHAPEREPRESENTATION(#19,'Annotation','Annotation2D',(#1087));
#1089=IFCCARTESIANPOINT((0.,0.,0.));
#1090=IFCDIRECTION((1.,0.,0.));
#1091=IFCDIRECTION((0.,0.,1.));
#1092=IFCAXIS2PLACEMENT3D(#1089,#1091,#1090);
#1093=IFCREPRESENTATIONMAP(#1092,#1088);
#1094=IFCTYPEPRODUCT('0klFX9AjnEnPBkdIURv8XD',$,'NAME-TAG',$,'IfcAnnotation/TEXT',(#1095),(#1108),$);
#1095=IFCPROPERTYSET('1d53tifbv2rwcoFFJosiHf',$,'EPset_Annotation',$,(#1096));
#1096=IFCPROPERTYSINGLEVALUE('Symbol',$,IFCLABEL('capsule-tag'),$);
#1097=IFCCARTESIANPOINT((0.,0.,0.));
#1098=IFCDIRECTION((0.,0.,1.));
#1099=IFCDIRECTION((1.,0.,0.));
#1100=IFCAXIS2PLACEMENT3D(#1097,#1098,#1099);
#1101=IFCPLANAREXTENT(1000.,1000.);
#1102=IFCTEXTLITERALWITHEXTENT('{{Name}}',#1100,.RIGHT.,#1101,'center');
#1103=IFCSHAPEREPRESENTATION(#19,'Annotation','Annotation2D',(#1102));
#1104=IFCCARTESIANPOINT((0.,0.,0.));
#1105=IFCDIRECTION((1.,0.,0.));
#1106=IFCDIRECTION((0.,0.,1.));
#1107=IFCAXIS2PLACEMENT3D(#1104,#1106,#1105);
#1108=IFCREPRESENTATIONMAP(#1107,#1103);
ENDSEC;
END-ISO-10303-21;
+7 -35
View File
@@ -36,24 +36,6 @@ cwd = os.path.dirname(os.path.realpath(__file__))
global_subscription_owner = object()
def mode_callback(obj, data):
objects = bpy.context.selected_objects
if bpy.context.active_object:
objects += [bpy.context.active_object]
for obj in objects:
if (
obj.mode != "EDIT"
or not obj.data
or not isinstance(obj.data, (bpy.types.Mesh, bpy.types.Curve, bpy.types.TextCurve))
or not obj.BIMObjectProperties.ifc_definition_id
):
continue
if obj.data.BIMMeshProperties.ifc_definition_id:
tool.Ifc.edit(obj)
elif IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id).is_a("IfcGridAxis"):
tool.Ifc.edit(obj)
def name_callback(obj, data):
try:
obj.name
@@ -195,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()
@@ -230,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()
@@ -239,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()
+1 -1
View File
@@ -325,7 +325,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")
-2
View File
@@ -220,7 +220,6 @@ class IfcStore:
if isinstance(obj, bpy.types.Material):
blenderbim.bim.handler.subscribe_to(obj, "diffuse_color", blenderbim.bim.handler.color_callback)
elif isinstance(obj, bpy.types.Object):
blenderbim.bim.handler.subscribe_to(obj, "mode", blenderbim.bim.handler.mode_callback)
blenderbim.bim.handler.subscribe_to(
obj, "active_material_index", blenderbim.bim.handler.active_material_index_callback
)
@@ -249,7 +248,6 @@ class IfcStore:
if isinstance(obj, bpy.types.Material):
blenderbim.bim.handler.subscribe_to(obj, "diffuse_color", blenderbim.bim.handler.color_callback)
elif isinstance(obj, bpy.types.Object):
blenderbim.bim.handler.subscribe_to(obj, "mode", blenderbim.bim.handler.mode_callback)
blenderbim.bim.handler.subscribe_to(
obj, "active_material_index", blenderbim.bim.handler.active_material_index_callback
)
+50 -18
View File
@@ -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
@@ -158,12 +146,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 +169,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)
@@ -275,6 +266,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,6 +311,8 @@ class IfcImporter:
self.profile_code("Merging by colour")
self.set_default_context()
self.profile_code("Setting default context")
self.setup_viewport_camera()
self.setup_arrays()
self.update_progress(100)
bpy.context.window_manager.progress_end()
@@ -358,6 +353,7 @@ class IfcImporter:
)
if self.body_contexts:
self.settings.set_context_ids(self.body_contexts)
self.settings_body_2d.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 = [
@@ -508,6 +504,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:
@@ -1913,6 +1929,22 @@ class IfcImporter:
obj.matrix_world = matrix_world
tool.Geometry.record_object_position(obj)
def setup_viewport_camera(self):
context_override = tool.Blender.get_viewport_context()
with bpy.context.temp_override(**context_override):
bpy.ops.object.select_all(action="SELECT")
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):
@@ -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"}
@@ -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_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")
@@ -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"}
@@ -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
@@ -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,11 @@ 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):
pass
# depth: bpy.props.FloatProperty(name="Depth", default=0.1, subtype="DISTANCE", description="Flooring depth")
@@ -0,0 +1,166 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2023 @Andrej730
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BlenderBIM Add-on is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import os
import bpy
import blenderbim.tool as tool
from blenderbim.bim.helper import prop_with_search
from 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
bl_keymap = tool.Blender.get_default_selection_keypmap() + (
("bim.covering_hotkey", {"type": "A", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_A")]}),
)
@classmethod
def draw_settings(cls, context, layout, ws_tool):
CoveringToolUI.draw(context, layout, ifc_element_type="IfcCoveringType")
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):
if AuthoringData.data["ifc_classes"]:
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
active_obj = bpy.context.active_object
element = tool.Ifc.get_entity(active_obj)
if element and bpy.context.selected_objects and element.is_a("IfcWall"):
op = row.operator("bim.add_instance_flooring_coverings_from_walls")
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)
@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)
if element and bpy.context.selected_objects and element.is_a("IfcWall"):
bpy.ops.bim.add_instance_flooring_coverings_from_walls()
else:
bpy.ops.bim.add_constr_type_instance()
@@ -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):
@@ -205,6 +204,7 @@ class ExportIfcCsv(bpy.types.Operator):
delimiter=sep,
include_global_id=props.include_global_id,
null=props.null_value,
empty=props.empty_value,
bool_true=props.true_value,
bool_false=props.false_value,
sort=sort,
@@ -244,12 +244,13 @@ class ImportIfcCsv(bpy.types.Operator):
attributes=attributes,
delimiter=sep,
null=props.null_value,
empty=props.empty_value,
bool_true=props.true_value,
bool_false=props.false_value,
)
if not props.should_load_from_memory:
ifc_file.write(props.csv_ifc_file)
purge_module_data()
refresh_ui_data()
return {"FINISHED"}
@@ -69,6 +69,7 @@ class CsvProperties(PropertyGroup):
should_preserve_existing: BoolProperty(default=False, name="Preserve Existing")
include_global_id: BoolProperty(default=True, name="Include GlobalId")
null_value: StringProperty(default="N/A", name="Null Value")
empty_value: StringProperty(default="-", name="Empty String Value")
true_value: StringProperty(default="YES", name="True Value")
false_value: StringProperty(default="NO", name="False Value")
csv_delimiter: EnumProperty(
@@ -74,6 +74,8 @@ class BIM_PT_ifccsv(Panel):
row = layout.row()
row.prop(props, "null_value")
row = layout.row()
row.prop(props, "empty_value")
row = layout.row()
row.prop(props, "true_value")
row = layout.row()
row.prop(props, "false_value")
@@ -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"}
@@ -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:
@@ -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 = []
@@ -1926,7 +1931,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 3D_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 +1949,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:
@@ -180,10 +180,6 @@ def format_distance(
frac = 0
inches += 1
# Check values and compose string
if inches == 12:
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
@@ -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):
@@ -345,8 +345,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 +391,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")
@@ -831,7 +831,9 @@ class SvgWriter:
symbol_xml.attrib.pop("id")
# NOTE: zip makes sure that we iterate over the shortest list
for field, text_literal in zip(template_text_fields, text_literals):
field.text = tool.Drawing.replace_text_literal_variables(text_literal.Literal, product)
field.text = tool.Drawing.replace_text_literal_variables(
text_literal.Literal, product or element
)
field.attrib["class"] = classes_str
if fill_bg:
@@ -849,7 +851,7 @@ class SvgWriter:
line_number = 0
for text_literal in text_literals:
text = tool.Drawing.replace_text_literal_variables(text_literal.Literal, product)
text = tool.Drawing.replace_text_literal_variables(text_literal.Literal, product or element)
text_tags = self.create_text_tag(
text,
text_position_svg,
@@ -883,7 +885,6 @@ class SvgWriter:
symbol_position_svg = point * self.svg_scale
self.svg.add(self.svg.use(f"#{svg_id}", insert=symbol_position_svg))
def draw_point_annotation(self, obj, classes):
x_offset = self.raw_width / 2
y_offset = self.raw_height / 2
@@ -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,16 @@ import bpy
from . import ui, prop, operator
classes = (
operator.SelectCobieIfcFile,
operator.SelectCobieJsonFile,
operator.ExecuteIfcCobie,
prop.COBieProperties,
ui.BIM_PT_cobie,
operator.SelectFMIfcFile,
operator.ExecuteIfcFM,
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,82 @@
# 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 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")
def execute(self, context):
context.scene.BIMFMProperties.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):
import ifcfm
props = context.scene.BIMFMProperties
ifc_file = tool.Ifc.get()
if not (ifc_file and props.should_load_from_memory):
ifc_file = ifcopenshell.open(props.ifc_file)
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(self.filepath)
elif props.format == "xlsx":
writer.write_xlsx(self.filepath)
return {"FINISHED"}
@@ -0,0 +1,54 @@
# 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 bpy.props import (
PointerProperty,
StringProperty,
EnumProperty,
BoolProperty,
IntProperty,
FloatProperty,
FloatVectorProperty,
CollectionProperty,
)
class BIMFMProperties(PropertyGroup):
ifc_file: StringProperty(default="", name="IFC File")
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",
)
@@ -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,13 @@ 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="")
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 = layout.row(align=True)
row.prop(props, "cobie_json_file")
row.operator("bim.select_cobie_json_file", icon="FILE_FOLDER", text="")
row.prop(props, "format")
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", text="Convert To Spreadsheet")
@@ -32,6 +32,7 @@ classes = (
operator.OverrideDuplicateMoveLinkedMacro,
operator.OverrideDuplicateMoveMacro,
operator.OverrideJoin,
operator.OverrideMeshSeparate,
operator.OverrideModeSetEdit,
operator.OverrideModeSetObject,
operator.OverrideOriginSet,
@@ -53,6 +54,7 @@ classes = (
ui.BIM_PT_mesh,
ui.BIM_PT_workarounds,
ui.BIM_MT_object_set_origin,
ui.BIM_MT_separate,
)
@@ -73,6 +75,7 @@ def register():
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")
@@ -116,6 +119,7 @@ def unregister():
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
@@ -57,6 +57,39 @@ 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
# You cannot separate meshes if the representation is mapped.
relating_type = tool.Root.get_element_type(tool.Ifc.get_entity(obj))
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 +121,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",
@@ -216,7 +256,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):
@@ -272,7 +313,8 @@ class UpdateRepresentation(bpy.types.Operator, Operator):
if self.ifc_representation_class == "IfcTessellatedFaceSet":
# We are explicitly casting to a tessellation, so remove all parametric materials.
element_type = ifcopenshell.util.element.get_type(product)
ifcopenshell.api.run("material.unassign_material", tool.Ifc.get(), product=element_type)
if element_type: # Some invalid IFCs use material sets without a type.
ifcopenshell.api.run("material.unassign_material", tool.Ifc.get(), product=element_type)
ifcopenshell.api.run("material.unassign_material", tool.Ifc.get(), product=product)
else:
# These objects are parametrically based on an axis and should not be modified as a mesh
@@ -498,6 +540,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:
@@ -507,6 +551,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()
@@ -527,6 +572,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"
@@ -537,7 +606,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
@@ -652,6 +721,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)
@@ -662,7 +738,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
@@ -674,44 +750,111 @@ 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()
if obj.data:
new_obj.data = obj.data.copy()
temp_data = None
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
# 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:
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):
@@ -730,57 +873,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):
@@ -800,28 +896,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
@@ -1019,12 +1094,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,
@@ -1059,6 +1129,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))
@@ -1067,7 +1148,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"}
@@ -1152,12 +1233,7 @@ class RefreshAggregate(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,
@@ -1171,22 +1247,33 @@ class RefreshAggregate(bpy.types.Operator):
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)
@@ -1199,13 +1286,25 @@ 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
@@ -1214,7 +1313,7 @@ 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()
return {"FINISHED"}
@@ -1407,6 +1506,7 @@ class OverrideModeSetEdit(bpy.types.Operator):
continue
if tool.Geometry.is_meshlike(representation):
tool.Ifc.edit(obj)
if getattr(element, "HasOpenings", None):
# Mesh elements with openings must disable openings
# so that you can edit the original topology.
@@ -1425,7 +1525,9 @@ class OverrideModeSetEdit(bpy.types.Operator):
else:
obj.select_set(False)
continue
if len(selected_objs) > 1 and (not context.selected_objects or len(context.selected_objects) != len(selected_objs)):
if len(selected_objs) > 1 and (
not context.selected_objects or len(context.selected_objects) != len(selected_objs)
):
# We are trying to edit at least one non-mesh-like object : Display a hint to the user
self.report({"INFO"}, "Only mesh-compatible representations may be edited concurrently in edit mode.")
@@ -1537,6 +1639,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)
@@ -1551,6 +1656,8 @@ class OverrideModeSetObject(bpy.types.Operator):
self.edited_objs.append(obj)
elif getattr(element, "HasOpenings", None):
self.unchanged_objs_with_openings.append(obj)
else:
tool.Ifc.finish_edit(obj)
if self.edited_objs:
return context.window_manager.invoke_props_dialog(self)
@@ -32,6 +32,21 @@ 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_object_set_origin(Menu):
bl_idname = "BIM_MT_object_set_origin"
bl_label = "IFC Set Origin"
@@ -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,7 @@ classes = (
slab.SetArcIndex,
space.GenerateSpace,
space.GenerateSpacesFromWalls,
covering.AddInstanceFlooringCoveringsFromWalls,
space.ToggleSpaceVisibility,
mep.FitFlowSegments,
mep.RegenerateDistributionElement,
@@ -179,6 +181,7 @@ classes = (
roof.SetGableRoofEdgeAngle,
mep.MEPAddObstruction,
mep.MEPAddTransition,
mep.MEPAddBend,
)
addon_keymaps = []
@@ -206,15 +206,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,13 +238,26 @@ 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):
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"}
try:
parent_element = tool.Ifc.get().by_guid(self.parent)
parent_element = tool.Ifc.get().by_guid(array_pset["Parent"])
except RuntimeError:
self.report({"ERROR"}, f"Couldn't find array parent by guid '{self.parent}'")
self.report({"ERROR"}, f"Couldn't find array parent by guid '{array_pset['Parent']}'")
return {"CANCELLED"}
array_objects = tool.Blender.Modifier.Array.get_all_objects(parent_element)
@@ -0,0 +1,64 @@
# 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 AddInstanceFlooringCoveringsFromWalls(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_instance_flooring_coverings_from_walls"
bl_label = "Add Typed Covering 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)
@@ -68,6 +68,14 @@ def update_door_modifier_representation(context, obj):
},
}
def get_active_representation_context(obj):
active_representation = tool.Geometry.get_active_representation(obj)
if active_representation:
return active_representation.ContextOfItems
return ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
previously_active_context = get_active_representation_context(obj)
# ELEVATION_VIEW representation
profile = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Profile", "ELEVATION_VIEW")
if profile:
@@ -78,6 +86,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 +122,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 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,
+495 -59
View File
@@ -35,11 +35,12 @@ import blenderbim.core.type
import blenderbim.core.root
import blenderbim.core.geometry
import blenderbim.tool as tool
from math import pi, degrees, radians
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
from blenderbim.bim.module.model.profile import DumbProfileJoiner
from blenderbim.tool.cad import VTX_PRECISION
V = lambda *x: Vector([float(i) for i in x])
@@ -47,7 +48,8 @@ V = lambda *x: Vector([float(i) for i in x])
class RegenerateDistributionElement(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.regenerate_distribution_element"
bl_description = (
"Regenerates the positions and segment lengths of a distribution element and all connected elements."
"Regenerates the positions and segment lengths of a distribution element and all connected elements.\n"
"Will try to adjust as less elements as possible, never rotate them. Segments will also try to change their length to fit"
)
bl_label = "Regenerate Distribution Element"
bl_options = {"REGISTER", "UNDO"}
@@ -201,6 +203,7 @@ class FitFlowSegments(bpy.types.Operator, tool.Ifc.Operator):
is_on_axis2 = tool.Cad.is_point_on_edge(intersect2, axis2)
if not is_on_axis1 and not is_on_axis2:
fitting_type = "BEND"
bpy.ops.bim.mep_add_bend()
elif is_on_axis1 and is_on_axis2:
fitting_type = "CROSS"
else:
@@ -334,7 +337,7 @@ class MEPGenerator:
class_name = "".join(split_camel_case(element.is_a())[:-1] + [mep_class_type])
return class_name
def get_compatible_fitting_type(self, segment_or_segments, port_or_ports, predefined_type):
def get_compatible_fitting_type(self, segment_or_segments, port_or_ports, predefined_type, bbim_data=None):
"""
returns a dict of compatible fitting_type and start_port_match flag to correctly place the fitting.
@@ -347,9 +350,12 @@ class MEPGenerator:
There lies the problem that it won't be
able to identify the fittings that were not yet connected to any segments yet.
`bbim_data` is used to find compatible fitting build with BBIM parametrically (BBIM_Fitting pset).
All data in `bbim_data` supposed to be in project units.
"""
# TODO: check angle, start, end and offset for transitions
if not isinstance(segment_or_segments, collections.abc.Iterable):
segments = [segment_or_segments]
ports = [port_or_ports]
@@ -357,6 +363,12 @@ class MEPGenerator:
segments = segment_or_segments
ports = port_or_ports
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):
segment_type = ifcopenshell.util.element.get_type(segment)
@@ -365,6 +377,53 @@ class MEPGenerator:
return
segments_data.append((segment_type, port.PredefinedType, port.SystemType))
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)
fitting_bbim_data = tool.Model.get_modeling_bbim_pset_data(fitting_type_obj, "BBIM_Fitting")
if not fitting_bbim_data:
return False
fitting_bbim_data = fitting_bbim_data["data_dict"]
def compare_value(key, second_key=None):
second_key = second_key or key
requested_value = bbim_data[key]
fitting_value = fitting_bbim_data[second_key]
if isinstance(requested_value, float):
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)
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
def are_connected_elements_compatible(segments_data, fitting_data):
# prevent arguments mutation, not using deepcopy because of the errors with ifc elements
segments_data = [copy(i) for i in segments_data]
@@ -391,7 +450,7 @@ 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 exact cases
# but I couldn't pin it down to exact cases
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):
@@ -406,17 +465,17 @@ class MEPGenerator:
if predefined_type == "OBSTRUCTION":
return packed_data
for port in ports:
port_local_position = V(*port.ObjectPlacement.RelativePlacement.Location.Coordinates)
if tool.Cad.is_x(port_local_position.length, 0.0):
start_port = port
break
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
@@ -450,12 +509,14 @@ class MEPGenerator:
fitting_data.append((element_type, port.PredefinedType, port.SystemType))
# if we skipped the occurrence we still can other occurrences
# if we skipped the occurrence we still need to check other occurrences
# otherwise checking 1 occurrence is enough
if not skipped_the_occurrence:
if are_connected_elements_compatible(segments_data, fitting_data):
if compatible_with_bbim_data(fitting_type) and are_connected_elements_compatible(
segments_data, fitting_data
):
return pack_return_data(fitting_type, ports, segments_data)
return
break
def create_obstruction_type(self, segment):
# code is very similar to "bim.add_type"
@@ -525,6 +586,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,
@@ -590,10 +653,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", 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)
@@ -639,63 +705,69 @@ class MEPAddTransition(bpy.types.Operator, tool.Ifc.Operator):
start_object_z_basis = start_object_rotation.to_matrix().col[2] # z basis vector
keep_only_z_axis = lambda p_ws: p_ws.dot(start_object_z_basis) * start_object_z_basis
# TODO: support cases when segments are partially or completely overlapping each other
if not tool.Cad.are_edges_parallel(start_axis, end_axis):
self.report({"ERROR"}, f"Failed to add transition - segments are not parallel.")
return {"CANCELLED"}
# TODO: support different profiles rotation by local Z
# check rotation difference
end_object_rotation = end_object.matrix_world.to_quaternion()
rotation_difference_z = (
start_object.matrix_world.to_quaternion().rotation_difference(end_object_rotation).to_euler().z
)
def is_multiple_of_pi(value):
n = round(value / pi)
return tool.Cad.is_x(abs(value - n * pi), 0)
if not is_multiple_of_pi(rotation_difference_z):
self.report(
{"ERROR"},
"There is some rotation difference between profiles by local Z axis: "
f"{round(degrees(rotation_difference_z))} deg, this kind of transition is not yet supported.",
)
return {"CANCELLED"}
# setup start / end points
start_segment_data = MEPGenerator().get_segment_data(start_element)
end_segment_data = MEPGenerator().get_segment_data(end_element)
end_port = end_segment_data["start_port"]
start_port = start_segment_data["end_port"]
points_ports_map = {
start_segment_data["start_point"]: start_segment_data["start_port"],
start_segment_data["end_point"]: start_segment_data["end_port"],
end_segment_data["start_point"]: end_segment_data["start_port"],
end_segment_data["end_point"]: end_segment_data["end_port"],
}
# transition points
start_point, end_point = tool.Cad.closest_points(
(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"]),
)
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()
flip_profile_offset = base_transition_dir.dot(start_object_z_basis) < 0
if tool.Cad.are_edges_collinear(start_axis, end_axis):
profile_offset = None
profile_offset = V(0, 0)
else:
to_start_object_space = start_object_rotation.inverted()
profile_offset = (
(to_start_object_space @ end_object.location) - (to_start_object_space @ start_object.location)
).xy
if tool.Cad.is_x(profile_offset.length_squared, 0):
profile_offset = None
else:
profile_offset = profile_offset / si_conversion
if flip_profile_offset:
profile_offset *= V(1, -1)
profile_offset = profile_offset / si_conversion
if flip_profile_offset:
profile_offset *= V(1, -1)
# world space profile offset
profile_offset_ws = (
start_object_rotation @ (profile_offset * si_conversion).to_3d() if profile_offset else V(0, 0, 0)
)
# will need entire_length to check that transition length fill fit
first_segment_start, second_segment_end = [
p
for p in (
start_segment_data["start_point"],
start_segment_data["end_point"],
end_segment_data["start_point"],
end_segment_data["end_point"],
)
if p not in (start_point, end_point)
]
profile_offset_ws = start_object_rotation @ (profile_offset * si_conversion).to_3d()
def get_segments_length():
start_dir = (start_point - first_segment_start).normalized()
@@ -707,8 +779,6 @@ class MEPAddTransition(bpy.types.Operator, tool.Ifc.Operator):
# can't rely on (end_point-start_point) here because
# transition might change the segments length and therefore direction will be changed
segments_dir = (start_point - first_segment_start).normalized()
start_port = points_ports_map[start_point]
end_port = points_ports_map[end_point]
# add transition representation
builder = ShapeBuilder(ifc_file)
@@ -717,6 +787,7 @@ class MEPAddTransition(bpy.types.Operator, tool.Ifc.Operator):
end_element,
self.start_length / si_conversion,
self.end_length / si_conversion,
angle=degrees(self.angle),
profile_offset=profile_offset,
)
@@ -741,31 +812,30 @@ 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
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 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),
}
# find the compatible fitting type
fitting_data = MEPGenerator().get_compatible_fitting_type(
[start_element, end_element], [start_port, end_port], "TRANSITION"
[start_element, end_element], [start_port, end_port], "TRANSITION", bbim_data=parametric_data
)
transition_type = fitting_data["fitting_type"] if fitting_data else None
start_port_match = fitting_data["start_port_match"] if fitting_data else True
if transition_type:
# TODO: handle the case without creating a representation in the first place?
ifcopenshell.api.run("geometry.remove_representation", ifc_file, representation=rep)
start_port_match = fitting_data["start_port_match"] if fitting_data else True
# create new fitting type if nothing is compatible
if not transition_type:
else: # create new fitting type if nothing is compatible
mesh = bpy.data.meshes.new("Transition")
obj = bpy.data.objects.new("Transition", mesh)
transition_type = blenderbim.core.root.assign_class(
@@ -779,6 +849,7 @@ 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",
@@ -786,6 +857,7 @@ class MEPAddTransition(bpy.types.Operator, tool.Ifc.Operator):
pset=pset,
properties={"Data": json.dumps(transition_data, default=list)},
)
tool.System.add_ports(obj, offset_end_port=profile_offset_ws)
# NOTE: at this point we loose current blender objects selection
# create transition element
@@ -809,10 +881,374 @@ 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")
tool.Ifc.run("system.connect_port", port1=ports[1], port2=end_port, direction="NOTDEFINED")
return {"FINISHED"}
class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.mep_add_bend"
bl_label = "Add Bend"
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", min=0
)
end_length: bpy.props.FloatProperty(
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", min=0
)
def _execute(self, context):
start_element, end_element = None, None
ifc_file = tool.Ifc.get()
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
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)
start_object = tool.Ifc.get_object(start_element)
end_object = tool.Ifc.get_object(end_element)
elif len(context.selected_objects) == 2:
start_object = context.active_object
end_object = next(o for o in context.selected_objects if o != context.active_object)
start_element = tool.Ifc.get_entity(start_object)
end_element = tool.Ifc.get_entity(end_object)
if not start_element or not end_element:
self.report({"ERROR"}, f"Two IFC elements should be selected for the bend.")
return {"CANCELLED"}
else:
self.report({"ERROR"}, f"Two IFC elements should be provided for the bend.")
return {"CANCELLED"}
# check rotation difference
def rotation_difference_check():
end_object_rotation = end_object.matrix_world.to_quaternion()
rotation_difference = (
start_object.matrix_world.to_quaternion().rotation_difference(end_object_rotation).to_euler()
)
def is_multiple_of_pi(value):
n = round(value / pi)
return tool.Cad.is_x(abs(value - n * pi), 0)
if not is_multiple_of_pi(rotation_difference.z):
error_msg = (
"There is some rotation difference between profiles by local Z axis: "
f"{round(degrees(rotation_difference.z))} deg, adding a bend is not possible."
)
return error_msg
if error_msg := rotation_difference_check():
self.report({"ERROR"}, error_msg)
return {"CANCELLED"}
# check segments types
def types_check():
start_type = ifcopenshell.util.element.get_type(start_element)
end_type = ifcopenshell.util.element.get_type(end_element)
if not start_type or not end_type:
return False
return start_type == end_type
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.",
)
return {"CANCELLED"}
profile = tool.Model.get_flow_segment_profile(start_element)
if not profile.is_a("IfcRectangleProfileDef") and not profile.is_a("IfcCircleProfileDef"):
self.report(
{"ERROR"},
"For now Only IfcRectangleProfileDef/IfcCircleProfileDef profiles supported for a bend, "
f"the segments are {profile.is_a()}",
)
return {"CANCELLED"}
def get_dim(profile):
if profile.is_a("IfcRectangleProfileDef"):
return V(profile.XDim / 2, profile.YDim / 2)
elif profile.is_a("IfcCircleProfileDef"):
return V(profile.Radius, profile.Radius)
return None
# setup start / end points
start_object_rotation = start_object.matrix_world.to_quaternion().to_matrix()
start_segment_data = MEPGenerator().get_segment_data(start_element)
end_segment_data = MEPGenerator().get_segment_data(end_element)
points_ports_map = {
start_segment_data["start_point"]: start_segment_data["start_port"],
start_segment_data["end_point"]: start_segment_data["end_port"],
end_segment_data["start_point"]: end_segment_data["start_port"],
end_segment_data["end_point"]: end_segment_data["end_port"],
}
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_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"
end_segment_sign = -1 if end_point_on_origin else 1
profile_dim = get_dim(profile) * si_conversion
# 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)
def check_for_double_bends():
# The theory is To avoid double bends, the profile offset should occur along only two axes:
# 1) The local Z-axis of the start segment
# 2) One of the lateral axes (either X or Y)
#
# Double bend required when:
# - there are 2 or 0 lateral axes involved
# - offset appear by the non-lateral axis
#
# 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:
return (
None,
f"For now only one lateral axis is supported for a bend (double bends not supported). Found lateral axes: {len(lateral_axes)}.",
)
non_lateral_axis = 0 if lateral_axes[0] == 1 else 1
non_lateral_axis_offset = profile_offset[non_lateral_axis]
if not tool.Cad.is_x(non_lateral_axis_offset, 0):
return (
None,
"For now offset by non-lateral axis is not supported for a bend (double bends not supported).\n"
f"Detected an offset of {round(non_lateral_axis_offset, 5)} along the local axis {'XY'[non_lateral_axis]} when lateral axis is {'XY'[lateral_axes[0]]}.",
)
return lateral_axes[0], None
lateral_axis, error_msg = check_for_double_bends()
if error_msg:
self.report({"ERROR"}, error_msg)
return {"CANCELLED"}
non_lateral_axis = 0 if lateral_axis == 1 else 1
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)
def get_segments_extend():
segments_intersection = segments_intersection_ws - start_point
segments_intersection = to_start_object_space @ segments_intersection
# 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
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
def check_new_segment_length(start_point, end_point, extend_point):
"""Check if segment is placed too near to the bend point.
The idea is that we can either extend segment toward the bend
but we can shrink it only until it's start.
If the segment is too near it will return offset to fix the problem,
otherwise returns `None`.
"""
base_edge = end_point - start_point
new_edge = extend_point - start_point
projection = new_edge.dot(base_edge.normalized())
if projection < 0 or tool.Cad.is_x(projection, 0):
return projection
return None
# 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:
self.report(
{"ERROR"},
f"Start segment starts too near to the bend, need to offset it atleast by {round(projection, 3)} m.",
)
return {"ERROR"}
end_segment_extend_point = end_point + end_segment_sign * end_segment_extend * get_z_basis(end_object)
projection = check_new_segment_length(second_segment_end, end_point, end_segment_extend_point)
if projection is not None:
self.report(
{"ERROR"},
f"End segment starts too near to the bend, need to offset it atleast by {round(projection, 3)} m.",
)
return {"ERROR"}
DumbProfileJoiner().join_E(start_object, start_segment_extend_point, start_connection)
DumbProfileJoiner().join_E(end_object, end_segment_extend_point, end_connection)
context.view_layer.update() # update matrices
builder = ShapeBuilder(ifc_file)
rep, bend_data = builder.mep_bend_shape(
start_element,
self.start_length / si_conversion,
self.end_length / si_conversion,
angle,
self.radius / si_conversion,
profile_offset / si_conversion,
flip_z_axis=start_segment_sign == -1,
)
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", 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
mesh = bpy.data.meshes.new("Bend")
obj = bpy.data.objects.new("Bend", mesh)
bend_type = blenderbim.core.root.assign_class(
tool.Ifc,
tool.Collector,
tool.Root,
obj=obj,
ifc_class=MEPGenerator().get_mep_element_class_name(start_element, "FittingType"),
predefined_type="BEND",
should_add_representation=False,
)
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)},
)
tool.System.add_ports(obj, offset_end_port=start_object_rotation @ (radial_offset * V(1, 1, 0)))
# NOTE: at this point we loose current blender objects selection
# create transition element
bpy.ops.bim.add_constr_type_instance(relating_type_id=bend_type.id())
fitting_obj = bpy.context.active_object
# adjust fitting object rotation and location
# required since we'll base our `fitting_obj_dir` on this
fitting_obj.matrix_world = start_object.matrix_world
context.view_layer.update()
# 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.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")
tool.Ifc.run("system.connect_port", port1=ports[1], port2=end_port, direction="NOTDEFINED")
self.report({"INFO"}, f"Success!.. kind of. The angle was {round(bend_data['angle'])}")
return {"FINISHED"}
@@ -274,9 +274,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 +307,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])
@@ -185,8 +185,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.
@@ -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"})
@@ -132,7 +132,7 @@ 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
@@ -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)
@@ -23,6 +23,7 @@ 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
@@ -230,182 +231,9 @@ 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
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
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
# In order to run, the active object must be a wall and
# there must be selected walls
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 +242,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"}
@@ -188,10 +188,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="")
@@ -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)
@@ -1119,8 +1120,13 @@ class DumbWallJoiner:
other = tool.Ifc.get_object(rel.RelatedElement)
if connection not in ["ATPATH", "NOTDEFINED"]:
self.join(
obj, other, connection, rel.RelatedConnectionType, is_relating=True, description=rel.Description
)
obj,
other,
connection,
rel.RelatedConnectionType,
is_relating=True,
description=rel.Description
)
for rel in element.ConnectedFrom:
if rel.is_a("IfcRelConnectsPathElements"):
connection = rel.RelatedConnectionType
@@ -1299,7 +1305,7 @@ class DumbWallJoiner:
# The user has moved the wall into an invalid position that cannot connect at the desired end
return False
self.axis[1 if connection1 == "ATEND" else 0] = intersect
self.axis = proposed_axis
# Work out body
@@ -1309,55 +1315,79 @@ class DumbWallJoiner:
tp1 = wall1.matrix_world @ Vector(wall1.bound_box[1])
# Axis lines on bottom, for reference, base, and side axes
bra1 = (Vector((*axis1["reference"][0], bp1[2])), Vector((*axis1["reference"][1], bp1[2])))
bba1 = (Vector((*axis1["base"][0], bp1[2])), Vector((*axis1["base"][1], bp1[2])))
bsa1 = (Vector((*axis1["side"][0], bp1[2])), Vector((*axis1["side"][1], bp1[2])))
bba2 = (Vector((*axis2["base"][0], bp2[2])), Vector((*axis2["base"][1], bp2[2])))
bsa2 = (Vector((*axis2["side"][0], bp2[2])), Vector((*axis2["side"][1], bp2[2])))
def to_3d_axis(axis, z):
return (Vector((*axis[0], z)), Vector((*axis[1], z)))
bra1 = to_3d_axis(axis1["reference"], bp1.z)
bba1 = to_3d_axis(axis1["base"], bp1.z)
tba1 = to_3d_axis(axis1["base"], tp1.z)
bsa1 = to_3d_axis(axis1["side"], bp1.z)
bba2 = to_3d_axis(axis2["base"], bp2.z)
bsa2 = to_3d_axis(axis2["side"], bp2.z)
# Intersecting the walls sides defined by planes gives 4 lines of intersection
# Line point, and line direction
lp1, ld1 = mathutils.geometry.intersect_plane_plane(bba1[0], normal1, bba2[0], normal2)
lp2, ld2 = mathutils.geometry.intersect_plane_plane(bba1[0], normal1, bsa2[0], normal2)
lp3, ld3 = mathutils.geometry.intersect_plane_plane(bsa1[0], normal1, bba2[0], normal2)
lp4, ld4 = mathutils.geometry.intersect_plane_plane(bsa1[0], normal1, bsa2[0], normal2)
lpb1, ldb1 = mathutils.geometry.intersect_plane_plane(bba1[0], normal1, bba2[0], normal2)
lpb2, ldb2 = mathutils.geometry.intersect_plane_plane(bba1[0], normal1, bsa2[0], normal2)
lps1, lds1 = mathutils.geometry.intersect_plane_plane(bsa1[0], normal1, bba2[0], normal2)
lps2, lds2 = mathutils.geometry.intersect_plane_plane(bsa1[0], normal1, bsa2[0], normal2)
# Intersecting the 4 lines gives the 8 possible verts of intersection
# 4 on bottom, and 4 on top. 4 on our base line, 4 on our side line.
bb1 = mathutils.geometry.intersect_line_plane(lp1, lp1 + ld1, bp1, Vector((0, 0, 1)))
bb2 = mathutils.geometry.intersect_line_plane(lp2, lp2 + ld2, bp1, Vector((0, 0, 1)))
bs1 = mathutils.geometry.intersect_line_plane(lp3, lp3 + ld3, bp1, Vector((0, 0, 1)))
bs2 = mathutils.geometry.intersect_line_plane(lp4, lp4 + ld4, bp1, Vector((0, 0, 1)))
tb1 = mathutils.geometry.intersect_line_plane(lp1, lp1 + ld1, tp1, Vector((0, 0, 1)))
tb2 = mathutils.geometry.intersect_line_plane(lp2, lp2 + ld2, tp1, Vector((0, 0, 1)))
ts1 = mathutils.geometry.intersect_line_plane(lp3, lp3 + ld3, tp1, Vector((0, 0, 1)))
ts2 = mathutils.geometry.intersect_line_plane(lp4, lp4 + ld4, tp1, Vector((0, 0, 1)))
# Diagram: https://i.imgur.com/jwWx2Ox.png
# NOTE: bb/bs always equal lpb/lps?
bb1 = mathutils.geometry.intersect_line_plane(lpb1, lpb1 + ldb1, bp1, Vector((0, 0, 1)))
bb2 = mathutils.geometry.intersect_line_plane(lpb2, lpb2 + ldb2, bp1, Vector((0, 0, 1)))
bs1 = mathutils.geometry.intersect_line_plane(lps1, lps1 + lds1, bp1, Vector((0, 0, 1)))
bs2 = mathutils.geometry.intersect_line_plane(lps2, lps2 + lds2, bp1, Vector((0, 0, 1)))
# similar to bb/bs but also have local z offset
tb1 = mathutils.geometry.intersect_line_plane(lpb1, lpb1 + ldb1, tp1, Vector((0, 0, 1)))
tb2 = mathutils.geometry.intersect_line_plane(lpb2, lpb2 + ldb2, tp1, Vector((0, 0, 1)))
ts1 = mathutils.geometry.intersect_line_plane(lps1, lps1 + lds1, tp1, Vector((0, 0, 1)))
ts2 = mathutils.geometry.intersect_line_plane(lps2, lps2 + lds2, tp1, Vector((0, 0, 1)))
# Let's distinguish the 8 points by whether they are nearer or further away from the other end
# These 8 points will be used to find the final body position and clippings.
i = 0 if connection1 == "ATEND" else 1
j = 1 if connection1 == "ATEND" else 0
bbn = tool.Cad.closest_vector(axis1["base"][i].to_3d(), (bb1, bb2))
bbf = bb2 if bbn == bb1 else bb1
bsn = tool.Cad.closest_vector(axis1["side"][i].to_3d(), (bs1, bs2))
bsf = bs2 if bsn == bs1 else bs1
tbn = tool.Cad.closest_vector(axis1["base"][i].to_3d(), (tb1, tb2))
tbf = tb2 if tbn == tb1 else tb1
tsn = tool.Cad.closest_vector(axis1["side"][i].to_3d(), (ts1, ts2))
tsf = ts2 if tsn == ts1 else ts1
connected_at_end = connection1 == "ATEND"
i = 0 if connected_at_end else 1
def get_closest_and_furthest_vectors(ref_point_2d, vectors, clamp_axis=None):
def clamp_point_by_direction(point, edge):
percent = tool.Cad.edge_percent(point, edge)
if percent < 0:
return edge[0]
return point
# When there is a small angle between walls, intersection points can occur outside the wall's axis.
# Which can lead to inaccuracies - therefore we bottom clamp them to stay within the axis
if clamp_axis:
# if wall connected at the start then reference point will be at the end
# therefore we reverse the axis
if not connected_at_end:
clamp_axis = clamp_axis[::-1]
vectors = tuple([clamp_point_by_direction(v, clamp_axis) for v in vectors])
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))
tbn, tbf = get_closest_and_furthest_vectors(axis1["base"][i], (tb1, tb2), tba1)
tsn, tsf = get_closest_and_furthest_vectors(axis1["side"][i], (ts1, ts2))
j = 1 if connected_at_end else 0
if description == "MITRE":
# Mitre joints are an unofficial convention
bsf_ = tool.Cad.point_on_edge(bsf, bba1)
tbf_ = tool.Cad.point_on_edge(tbf, bba1)
tsf_ = tool.Cad.point_on_edge(tsf, bba1)
new_body = tool.Cad.furthest_vector(bba1[i], (bbf, bsf_)).copy()
new_body = tool.Cad.furthest_vector(bba1[i], (new_body, tbf_)).copy()
new_body = tool.Cad.furthest_vector(bba1[i], (bbf, bsf_))
new_body = tool.Cad.furthest_vector(bba1[i], (new_body, tbf_))
new_body = tool.Cad.furthest_vector(bba1[i], (new_body, tsf_)).copy()
self.body[j] = tool.Cad.point_on_edge(new_body, bra1).to_2d()
if connection1 == connection2:
if (connection1 == "ATEND" and angle > 0) or (connection1 != "ATEND" and angle < 0):
if (connected_at_end and angle > 0) or (not connected_at_end and angle < 0):
pt = bbf.to_2d().to_3d()
x_axis = bsn - bbf
y_axis = tbf - bbf
@@ -1366,7 +1396,7 @@ class DumbWallJoiner:
x_axis = bsf - bbn
y_axis = tbn - bbn
else:
if (connection1 == "ATEND" and angle < 0) or (connection1 != "ATEND" and angle > 0):
if (connected_at_end and angle < 0) or (not connected_at_end and angle > 0):
pt = bbf.to_2d().to_3d()
x_axis = bsn - bbf
y_axis = tbf - bbf
@@ -1377,6 +1407,9 @@ class DumbWallJoiner:
if connection1 != "ATEND":
y_axis *= -1
x_axis.normalize()
y_axis.normalize()
z_axis = x_axis.cross(y_axis)
y_axis = z_axis.cross(x_axis)
@@ -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,8 @@ def update_simple_openings(element, opening_width, opening_height):
has_replaced_opening_representation = True
tool.Model.reload_body_representation(voided_objs)
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 +118,14 @@ def update_window_modifier_representation(context, obj):
}
representation_data["panel_properties"].append(panel_data)
def get_active_representation_context(obj):
active_representation = tool.Geometry.get_active_representation(obj)
if active_representation:
return active_representation.ContextOfItems
return ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
previously_active_context = get_active_representation_context(obj)
# ELEVATION_VIEW representation
profile = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Profile", "ELEVATION_VIEW")
if profile:
@@ -128,6 +136,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 +151,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 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,
@@ -24,7 +24,7 @@ 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.model.prop import get_ifc_class
@@ -300,7 +300,12 @@ class BimToolUI:
add_layout_hotkey_operator(cls.layout, "Extend", "S_E", "")
add_layout_hotkey_operator(cls.layout, "Butt", "S_T", "")
add_layout_hotkey_operator(cls.layout, "Mitre", "S_Y", "")
add_layout_hotkey_operator(
cls.layout,
"Mitre",
"S_Y",
"Join two intersecting walls using a mitre joint.\nOther selected wall is connected to the active",
)
add_layout_hotkey_operator(cls.layout, "Merge", "S_M", bpy.ops.bim.merge_wall.__doc__)
add_layout_hotkey_operator(cls.layout, "Flip", "S_F", bpy.ops.bim.flip_wall.__doc__)
@@ -342,20 +347,25 @@ class BimToolUI:
"IfcDuctSegment",
"IfcPipeSegment",
):
add_layout_hotkey_operator(cls.layout, "Extend", "S_E", "")
add_layout_hotkey_operator(cls.layout, "Add Fitting", "S_F", "")
add_layout_hotkey_operator(
cls.layout, "Regen MEP", "S_G", bpy.ops.bim.regenerate_distribution_element.__doc__
)
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")
cls.layout.operator("bim.mep_connect_elements")
else:
add_layout_hotkey_operator(cls.layout, "Edit Axis", "A_E", "")
add_layout_hotkey_operator(cls.layout, "Butt", "S_T", "")
add_layout_hotkey_operator(cls.layout, "Mitre", "S_Y", "")
add_layout_hotkey_operator(cls.layout, "Rotate 90", "S_R", bpy.ops.bim.rotate_90.__doc__)
add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.recalculate_profile.__doc__)
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
@@ -364,7 +374,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",
@@ -385,11 +396,7 @@ 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")
@@ -427,6 +434,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()
@@ -580,22 +592,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)
@@ -669,7 +679,15 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
if self.active_material_usage == "LAYER2":
bpy.ops.bim.recalculate_wall()
elif self.active_material_usage == "PROFILE":
bpy.ops.bim.recalculate_profile()
if self.active_class in (
"IfcCableCarrierSegment",
"IfcCableSegment",
"IfcDuctSegment",
"IfcPipeSegment",
):
bpy.ops.bim.regenerate_distribution_element()
else:
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"):
@@ -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()
@@ -70,6 +70,7 @@ addon_keymaps = []
def register():
bpy.types.Scene.BIMProjectProperties = bpy.props.PointerProperty(type=prop.BIMProjectProperties)
bpy.types.TOPBAR_MT_file.prepend(ui.file_menu)
bpy.types.TOPBAR_MT_file_context_menu.prepend(ui.file_menu)
wm = bpy.context.window_manager
if wm.keyconfigs.addon:
km = wm.keyconfigs.addon.keymaps.get("Window")
@@ -87,6 +88,7 @@ def register():
def unregister():
bpy.types.TOPBAR_MT_file.remove(ui.file_menu)
bpy.types.TOPBAR_MT_file_context_menu.remove(ui.file_menu)
del bpy.types.Scene.BIMProjectProperties
wm = bpy.context.window_manager
@@ -355,8 +355,8 @@ class AppendEntireLibrary(bpy.types.Operator):
self.file = IfcStore.get_file()
self.library = IfcStore.library_file
lib_elements = ifcopenshell.util.selector.Selector().parse(
self.library, ".IfcTypeProduct | .IfcMaterial | .IfcCostSchedule| .IfcProfileDef"
lib_elements = ifcopenshell.util.selector.filter_elements(
self.library, "IfcTypeProduct, IfcMaterial, IfcCostSchedule, IfcProfileDef"
)
for element in lib_elements:
bpy.ops.bim.append_library_element(definition=element.id())
@@ -429,7 +429,7 @@ class AppendLibraryElement(bpy.types.Operator):
except:
# TODO Remove this terrible code when I refactor this into the core
pass
blenderbim.bim.handler.purge_module_data()
blenderbim.bim.handler.refresh_ui_data()
return {"FINISHED"}
def import_material_from_ifc(self, element, context):
@@ -768,12 +768,12 @@ class LoadProjectElements(bpy.types.Operator):
return elements
def get_whitelist_elements(self):
selector = ifcopenshell.util.selector.Selector()
return set(selector.parse(self.file, self.props.filter_query))
return set(ifcopenshell.util.selector.filter_elements(self.file, self.props.filter_query))
def get_blacklist_elements(self):
selector = ifcopenshell.util.selector.Selector()
return set(self.file.by_type("IfcElement")) - set(selector.parse(self.file, self.props.filter_query))
return set(self.file.by_type("IfcElement")) - set(
ifcopenshell.util.selector.filter_elements(self.file, self.props.filter_query)
)
class ToggleFilterCategories(bpy.types.Operator):
@@ -895,6 +895,7 @@ class ReloadLink(bpy.types.Operator):
for c in bpy.data.collections
if "IfcProject" in c.name and c.library and os.path.basename(c.library.filepath) == selected_filename
]
for library in get_linked_ifcs() or []:
library.reload()
return {"FINISHED"}
@@ -920,7 +921,9 @@ class ToggleLinkSelectability(bpy.types.Operator):
def get_linked_collections(self):
return [
c for c in bpy.data.collections if "IfcProject" in c.name and c.library and c.library.filepath == self.filepath
c
for c in bpy.data.collections
if "IfcProject" in c.name and c.library and c.library.filepath == self.filepath
]
@@ -974,7 +977,9 @@ class ToggleLinkVisibility(bpy.types.Operator):
def get_linked_collections(self):
return [
c for c in bpy.data.collections if "IfcProject" in c.name and c.library and c.library.filepath == self.filepath
c
for c in bpy.data.collections
if "IfcProject" in c.name and c.library and c.library.filepath == self.filepath
]
@@ -1079,7 +1084,7 @@ class ExportIFC(bpy.types.Operator):
save_blend_file = bool(bpy.data.is_saved and bpy.data.is_dirty and bpy.data.filepath)
if save_blend_file:
bpy.ops.wm.save_mainfile(filepath=bpy.data.filepath)
blenderbim.bim.handler.purge_module_data()
blenderbim.bim.handler.refresh_ui_data()
self.report(
{"INFO"},
f'IFC Project "{os.path.basename(output_file)}" {"" if not save_blend_file else "And Current Blend File Are"} Saved',
@@ -398,3 +398,10 @@ class BIM_PT_purge(Panel):
layout.operator("bim.purge_unused_profiles")
layout.operator("bim.purge_unused_types")
layout.operator("bim.purge_unused_representations")
layout.separator()
layout.label(text="Purge unused elements by class:")
row = layout.row(align=True)
row.prop(context.scene.BIMDebugProperties, "ifc_class_purge", text="")
row.operator("bim.purge_unused_elements_by_class", text="", icon="TRASH")
row.operator("bim.print_unused_elements_stats", text="", icon="INFO")
@@ -71,10 +71,10 @@ mapper = {
'Area' : "get_net_side_area",
},
'Qto_DuctSegmentBaseQuantities' : {
'Length' : None,
'Length' : "get_length",
'GrossCrossSectionArea' : None,
'NetCrossSectionArea' : None,
'OuterSurfaceArea' : None,
'OuterSurfaceArea' : "get_outer_surface_area",
'GrossWeight' : None,
},
'Qto_TransformerBaseQuantities' : {
@@ -203,10 +203,10 @@ mapper = {
'Weight' : None,
},
'Qto_DuctFittingBaseQuantities' : {
'Length' : None,
'Length' : "get_length",
'GrossCrossSectionArea' : None,
'NetCrossSectionArea' : None,
'OuterSurfaceArea' : None,
'OuterSurfaceArea' : "get_outer_surface_area",
'GrossWeight' : None,
},
'Qto_UnitaryControlElementBaseQuantities' : {
@@ -91,7 +91,7 @@ class ObjectPsetsData(Data):
return []
psets = blenderbim.bim.schema.ifc.psetqto.get_applicable(element.is_a(), pset_only=True)
psetnames = cls.format_pset_enum(psets)
assigned_names = ifcopenshell.util.element.get_psets(element, psets_only=True).keys()
assigned_names = ifcopenshell.util.element.get_psets(element, psets_only=True, should_inherit=False).keys()
return [p for p in psetnames if p[0] not in assigned_names]
@classmethod
@@ -31,6 +31,9 @@ NetFloorArea: it doesn't count the following entities contained in the spatial e
NetVolume: like NetFloorArea, it doesn't count the following entities contained in the spatial entity: IfcColumn and IfcWall
Also, the entire IfcColumn (or IfcWall) object volume is substracted, so it should be better to substract only the shared volume between IfcSpace and IfcColumn. Look at todo list
DUCT SEGMENTS AND DUCT FITTINGS
The length is calculated like a beam. Also outer surface area. Gross cross section area, net cross section area and weight are not calculated right now because the parametrically cross section area seems filled (without hole).
WEIGHT
The object weight is calculated by multiplying the object mass density with the object volume (net or gross).
It's only calculated if the object material has a MassDensity property in the Pset_MaterialCommon.
@@ -223,7 +223,7 @@ class EnablePsetEditing(bpy.types.Operator):
new.is_selected = enum in selected_enum_items
else:
if prop.is_a("IfcPropertySingleValue"):
value = prop.NominalValue.wrappedValue
value = prop.NominalValue.wrappedValue if prop.NominalValue else None
elif prop.is_a("IfcPhysicalSimpleQuantity"):
value = prop[3]
new_prop = self.props.properties.add()
@@ -356,24 +356,7 @@ class AddPset(bpy.types.Operator, Operator):
obj_type: bpy.props.StringProperty()
def _execute(self, context):
self.file = IfcStore.get_file()
pset_name = get_pset_props(context, self.obj, self.obj_type).pset_name
if self.obj_type == "Object":
if context.selected_objects:
objects = [o.name for o in tool.Blender.get_selected_objects()]
else:
objects = [context.active_object.name]
else:
objects = [self.obj]
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)
if pset_name in blenderbim.bim.schema.ifc.psetqto.get_applicable_names(element.is_a(), pset_only=True):
bpy.ops.bim.enable_pset_editing(
pset_id=0, pset_name=pset_name, pset_type="PSET", obj=obj, obj_type=self.obj_type
)
core.add_pset(tool.Ifc, tool.Pset, tool.Blender, obj_name=self.obj, obj_type=self.obj_type)
class AddQto(bpy.types.Operator, Operator):
@@ -22,7 +22,7 @@ from . import ui, prop, operator
classes = (
operator.AddPropEnum,
operator.AddPropTemplate,
operator.AddPsetFile,
operator.AddPsetTemplateFile,
operator.AddPsetTemplate,
operator.DeletePropEnum,
operator.DisableEditingPropTemplate,
@@ -33,7 +33,7 @@ classes = (
operator.EnableEditingPsetTemplate,
operator.RemovePropTemplate,
operator.RemovePsetTemplate,
operator.SavePsetTemplateFile,
operator.RemovePsetTemplateFile,
prop.PsetTemplate,
prop.EnumerationValues,
prop.PropTemplate,
@@ -56,12 +56,12 @@ class PsetTemplatesData:
pset_dir = os.path.join(bpy.context.scene.BIMProperties.data_dir, "pset")
files = os.listdir(pset_dir)
for f in files:
results.append((os.path.join(pset_dir, f), f.strip(".ifc"), "Global Pset Template"))
results.append((os.path.join(pset_dir, f), os.path.splitext(os.path.basename(f))[0], "Global Pset Template"))
pset_dir = tool.Ifc.resolve_uri(bpy.context.scene.BIMProperties.pset_dir)
if os.path.isdir(pset_dir):
for path in pathlib.Path(pset_dir).glob("*.ifc"):
results.append((str(path), os.path.basename(str(path)).strip(".ifc"), "Project Pset Template"))
results.append((str(path), os.path.splitext(os.path.basename(str(path)))[0], "Project Pset Template"))
return sorted(results, key=lambda x: x[1])
@@ -44,9 +44,9 @@ class Operator:
IfcStore.pset_template_file.redo()
class AddPsetFile(bpy.types.Operator):
bl_idname = "bim.add_pset_file"
bl_label = "Add Pset File"
class AddPsetTemplateFile(bpy.types.Operator):
bl_idname = "bim.add_pset_template_file"
bl_label = "Add Pset Template File"
bl_options = {"REGISTER", "UNDO"}
def invoke(self, context, event):
@@ -58,24 +58,15 @@ class AddPsetFile(bpy.types.Operator):
def execute(self, context):
template = ifcopenshell.file()
filepath = os.path.join(
context.scene.BIMProperties.data_dir,
"pset",
self.props.new_template_filename + ".ifc",
)
filepath = os.path.join(context.scene.BIMProperties.data_dir, "pset", self.props.new_template_filename + ".ifc")
template.create_entity(
"IFCPROPERTYSETTEMPLATE",
**{
"GlobalId": ifcopenshell.guid.new(),
"Name": "Name",
"Description": "Description",
"TemplateType": "PSET_TYPEDRIVENONLY",
"ApplicableEntity": "IfcTypeObject",
}
)
pset_template = ifcopenshell.api.run("pset_template.add_pset_template", template)
ifcopenshell.api.run("pset_template.add_prop_template", template, pset_template=pset_template)
template.write(filepath)
self.props.new_template_filename = ""
blenderbim.bim.handler.refresh_ui_data()
blenderbim.bim.schema.reload(tool.Ifc.get().schema)
context.scene.BIMPsetTemplateProperties.pset_template_files = filepath
return {"FINISHED"}
@@ -86,7 +77,10 @@ class AddPsetTemplate(bpy.types.Operator, Operator):
def _execute(self, context):
template = ifcopenshell.api.run("pset_template.add_pset_template", IfcStore.pset_template_file)
ifcopenshell.api.run("pset_template.add_prop_template", IfcStore.pset_template_file, pset_template=template)
IfcStore.pset_template_file.write(IfcStore.pset_template_path)
blenderbim.bim.handler.refresh_ui_data()
blenderbim.bim.schema.reload(tool.Ifc.get().schema)
context.scene.BIMPsetTemplateProperties.pset_templates = str(template.id())
@@ -104,6 +98,9 @@ class RemovePsetTemplate(bpy.types.Operator, Operator):
IfcStore.pset_template_file,
**{"pset_template": IfcStore.pset_template_file.by_id(int(props.pset_templates))}
)
IfcStore.pset_template_file.write(IfcStore.pset_template_path)
blenderbim.bim.handler.refresh_ui_data()
blenderbim.bim.schema.reload(tool.Ifc.get().schema)
class EnableEditingPsetTemplate(bpy.types.Operator):
@@ -215,6 +212,9 @@ class EditPsetTemplate(bpy.types.Operator, Operator):
}
)
bpy.ops.bim.disable_editing_pset_template()
IfcStore.pset_template_file.write(IfcStore.pset_template_path)
blenderbim.bim.handler.refresh_ui_data()
blenderbim.bim.schema.reload(tool.Ifc.get().schema)
class SavePsetTemplateFile(bpy.types.Operator):
@@ -223,7 +223,21 @@ class SavePsetTemplateFile(bpy.types.Operator):
def execute(self, context):
IfcStore.pset_template_file.write(IfcStore.pset_template_path)
blenderbim.bim.handler.purge_module_data()
blenderbim.bim.handler.refresh_ui_data()
blenderbim.bim.schema.reload(tool.Ifc.get().schema)
return {"FINISHED"}
class RemovePsetTemplateFile(bpy.types.Operator):
bl_idname = "bim.remove_pset_template_file"
bl_label = "Remove Pset Template File"
def execute(self, context):
try:
os.remove(IfcStore.pset_template_path)
except:
pass
blenderbim.bim.handler.refresh_ui_data()
blenderbim.bim.schema.reload(tool.Ifc.get().schema)
return {"FINISHED"}
@@ -239,9 +253,12 @@ class AddPropTemplate(bpy.types.Operator, Operator):
ifcopenshell.api.run(
"pset_template.add_prop_template",
IfcStore.pset_template_file,
**{"pset_template": IfcStore.pset_template_file.by_id(pset_template_id)}
pset_template=IfcStore.pset_template_file.by_id(pset_template_id),
)
bpy.ops.bim.disable_editing_prop_template()
IfcStore.pset_template_file.write(IfcStore.pset_template_path)
blenderbim.bim.handler.refresh_ui_data()
blenderbim.bim.schema.reload(tool.Ifc.get().schema)
class RemovePropTemplate(bpy.types.Operator, Operator):
@@ -257,6 +274,9 @@ class RemovePropTemplate(bpy.types.Operator, Operator):
IfcStore.pset_template_file,
**{"prop_template": IfcStore.pset_template_file.by_id(self.prop_template)}
)
IfcStore.pset_template_file.write(IfcStore.pset_template_path)
blenderbim.bim.handler.refresh_ui_data()
blenderbim.bim.schema.reload(tool.Ifc.get().schema)
class EditPropTemplate(bpy.types.Operator, Operator):
@@ -285,6 +305,9 @@ class EditPropTemplate(bpy.types.Operator, Operator):
}
)
bpy.ops.bim.disable_editing_prop_template()
IfcStore.pset_template_file.write(IfcStore.pset_template_path)
blenderbim.bim.handler.refresh_ui_data()
blenderbim.bim.schema.reload(tool.Ifc.get().schema)
# TODO -This will need to go into the
# api code at some point - vulevukusej
@@ -38,6 +38,7 @@ from bpy.props import (
def updatePsetTemplateFiles(self, context):
IfcStore.pset_template_file = None
PsetTemplatesData.is_loaded = False
PsetTemplatesData.data["pset_template_files"] = PsetTemplatesData.pset_template_files()
PsetTemplatesData.data["pset_templates"] = PsetTemplatesData.pset_templates()
PsetTemplatesData.data["prop_templates"] = PsetTemplatesData.prop_templates()
@@ -179,7 +180,7 @@ class BIMPsetTemplateProperties(PropertyGroup):
pset_template_files: EnumProperty(
items=getPsetTemplateFiles, name="Pset Template Files", update=updatePsetTemplateFiles
)
pset_templates: EnumProperty(items=getPsetTemplates, name="Pset Template Files", update=updatePsetTemplates)
pset_templates: EnumProperty(items=getPsetTemplates, name="Pset Templates", update=updatePsetTemplates)
active_pset_template_id: IntProperty(name="Active Pset Template Id")
active_prop_template_id: IntProperty(name="Active Prop Template Id")
active_pset_template: PointerProperty(type=PsetTemplate)
@@ -38,9 +38,9 @@ class BIM_PT_pset_template(Panel):
self.props = context.scene.BIMPsetTemplateProperties
row = self.layout.row(align=True)
prop_with_search(row, self.props, "pset_template_files", text="")
row.operator("bim.save_pset_template_file", text="", icon="EXPORT")
row.operator("bim.add_pset_file", icon="ADD", text="")
prop_with_search(row, self.props, "pset_template_files", text="", icon="FILE")
row.operator("bim.add_pset_template_file", icon="ADD", text="")
row.operator("bim.remove_pset_template_file", icon="X", text="")
row = self.layout.row(align=True)
@@ -49,7 +49,6 @@ classes = (
operator.ExpandResource,
operator.GoToResource,
operator.ImportResources,
operator.LoadResourceProperties,
operator.LoadResources,
operator.RemoveResource,
operator.RemoveResourceQuantity,
@@ -33,19 +33,6 @@ class LoadResources(bpy.types.Operator):
return {"FINISHED"}
class LoadResourceProperties(bpy.types.Operator):
bl_idname = "bim.load_resource_properties"
bl_label = "Load Resource Properties"
bl_options = {"REGISTER", "UNDO"}
resource: bpy.props.IntProperty()
def execute(self, context):
core.load_resource_properties(
tool.Resource, resource=tool.Ifc.get().by_id(self.resource) if self.resource else None
)
return {"FINISHED"}
class AddResource(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_resource"
bl_label = "Add Resource"
@@ -437,4 +424,4 @@ class CalculateResourceUsage(bpy.types.Operator, tool.Ifc.Operator):
return False
def _execute(self, context):
core.calculate_resource_usage(tool.Ifc, tool.Resource, resource=tool.Ifc.get().by_id(tool.Resource.get_highlighted_resource()))
core.calculate_resource_usage(tool.Ifc, tool.Resource, resource=tool.Resource.get_highlighted_resource())
@@ -81,8 +81,7 @@ def update_active_resource_index(self, context):
def updateResourceUsage(self, context):
props = context.scene.BIMResourceProperties
if not props.is_resource_update_enabled:
if not context.scene.BIMResourceProperties.is_resource_update_enabled:
return
if not self.schedule_usage:
return
@@ -212,4 +212,4 @@ class CopyClass(bpy.types.Operator, Operator):
objects = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects
for obj in objects:
core.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=obj)
blenderbim.bim.handler.purge_module_data()
blenderbim.bim.handler.refresh_ui_data()
@@ -33,10 +33,6 @@ from bpy.props import (
)
def purge():
pass
def get_ifc_predefined_types(self, context):
if not IfcClassData.is_loaded:
IfcClassData.load()
@@ -20,7 +20,7 @@ import bpy
from . import ui, prop, operator
classes = (
operator.ActivateIfcBuildingStoreyFilter,
operator.ActivateContainerFilter,
operator.ActivateIfcClassFilter,
operator.AddFilter,
operator.AddFilterGroup,
@@ -58,6 +58,7 @@ classes = (
prop.SearchQueryGroup,
prop.IfcSelectorProperties,
ui.BIM_PT_search,
ui.BIM_PT_filter,
ui.BIM_PT_colour_by_property,
ui.BIM_PT_select_similar,
ui.BIM_UL_colourscheme,
@@ -466,8 +466,8 @@ class ToggleFilterSelection(Operator):
if props.filter_type == "CLASSES":
for ifc_class in props.filter_classes:
ifc_class.is_selected = self.selecting_actionbool
elif props.filter_type == "BUILDINGSTOREYS":
for building_storey in props.filter_building_storeys:
elif props.filter_type == "CONTAINER":
for building_storey in props.filter_container:
building_storey.is_selected = self.selecting_actionbool
return {"FINISHED"}
@@ -525,11 +525,11 @@ class ActivateIfcClassFilter(Operator):
row.operator("bim.toggle_filter_selection", text="Deselect All").action = "DESELECT"
class ActivateIfcBuildingStoreyFilter(Operator):
class ActivateContainerFilter(Operator):
"""Filter the current selection by Building Storey"""
bl_idname = "bim.activate_ifc_building_storey_filter"
bl_label = "Filter by Building Storey"
bl_idname = "bim.activate_ifc_container_filter"
bl_label = "Filter by Container"
@classmethod
def poll(cls, context):
@@ -540,27 +540,29 @@ class ActivateIfcBuildingStoreyFilter(Operator):
def invoke(self, context, event):
props = bpy.context.scene.BIMSearchProperties
props.filter_building_storeys.clear()
props.filter_container.clear()
ifc_building_storeys = {}
containers = {}
containers.setdefault("None", 0)
for obj in context.selected_objects:
storey = tool.Misc.get_object_storey(obj)
if not storey:
container = tool.Spatial.get_container(tool.Ifc.get_entity(obj))
if not container:
containers["None"] += 1
continue
ifc_building_storeys.setdefault(storey.Name, 0)
ifc_building_storeys[storey.Name] += 1
containers.setdefault(container.Name, 0)
containers[container.Name] += 1
for name, total in dict(sorted(ifc_building_storeys.items())).items():
new = props.filter_building_storeys.add()
for name, total in dict(sorted(containers.items())).items():
new = props.filter_container.add()
new.name = name
new.total = total
props.filter_type = "BUILDINGSTOREYS"
props.filter_type = "CONTAINER"
return context.window_manager.invoke_props_dialog(self, width=250)
def execute(self, context):
bpy.context.scene.BIMSearchProperties.filter_building_storeys.clear()
bpy.context.scene.BIMSearchProperties.filter_container.clear()
return {"FINISHED"}
def draw(self, context):
@@ -568,12 +570,12 @@ class ActivateIfcBuildingStoreyFilter(Operator):
"BIM_UL_ifc_building_storey_filter",
"",
context.scene.BIMSearchProperties,
"filter_building_storeys",
"filter_container",
context.scene.BIMSearchProperties,
"filter_building_storeys_index",
"filter_container_index",
rows=20
if len(bpy.context.scene.BIMSearchProperties.filter_building_storeys) > 20
else len(bpy.context.scene.BIMSearchProperties.filter_building_storeys),
if len(bpy.context.scene.BIMSearchProperties.filter_container) > 20
else len(bpy.context.scene.BIMSearchProperties.filter_container),
)
row = self.layout.row(align=True)
row.operator("bim.toggle_filter_selection", text="Select All").action = "SELECT"
@@ -70,15 +70,15 @@ def update_is_class_selected(self, context):
new.obj = obj
def update_is_level_selected(self, context):
def update_is_container_selected(self, context):
if self.is_selected:
for obj in self.unselected_objects:
obj.obj.select_set(True)
self.unselected_objects.clear()
else:
for obj in context.selected_objects:
level = tool.Misc.get_object_storey(obj)
if level and level.Name == self.name:
container = tool.Spatial.get_container(tool.Ifc.get_entity(obj))
if (container and container.Name == self.name) or (not container and self.name== "None"):
obj.select_set(False)
new = self.unselected_objects.add()
new.obj = obj
@@ -93,7 +93,7 @@ class BIMFilterClasses(PropertyGroup):
class BIMFilterBuildingStoreys(PropertyGroup):
name: StringProperty(name="Name")
is_selected: BoolProperty(name="Is Level Selected", default=True, update=update_is_level_selected)
is_selected: BoolProperty(name="Is Level Selected", default=True, update=update_is_container_selected)
total: IntProperty(name="Total")
unselected_objects: CollectionProperty(type=ObjProperty, name="Unfiltered Objects")
@@ -140,8 +140,8 @@ class BIMSearchProperties(PropertyGroup):
filter_type: StringProperty(name="Filter Type")
filter_classes: CollectionProperty(type=BIMFilterClasses, name="Filter Classes")
filter_classes_index: IntProperty(name="Filter Classes Index")
filter_building_storeys: CollectionProperty(type=BIMFilterBuildingStoreys, name="Filter Level")
filter_building_storeys_index: IntProperty(name="Filter Level Index")
filter_container: CollectionProperty(type=BIMFilterBuildingStoreys, name="Filter Level")
filter_container_index: IntProperty(name="Filter Level Index")
def get_classes(self, ifc_product):
@@ -42,11 +42,21 @@ class BIM_PT_search(Panel):
row = self.layout.row(align=True)
row.operator("bim.search", text="Search", icon="VIEWZOOM")
return # Temporary for now whilst searching is being upgraded.
return
class BIM_PT_filter(Panel):
bl_label = "Filter Selection"
bl_idname = "BIM_PT_filter"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_tab_grouping_and_filtering"
def draw(self, context):
row = self.layout.row(align=True)
row.operator("bim.activate_ifc_class_filter", icon="FILTER")
row.operator("bim.activate_ifc_building_storey_filter", icon="FILTER")
row.operator("bim.activate_ifc_container_filter", icon="FILTER")
class BIM_PT_colour_by_property(Panel):
@@ -23,7 +23,6 @@ import time
import calendar
import isodate
import pystache
import webbrowser
import blenderbim.core.sequence as core
import blenderbim.tool as tool
import blenderbim.bim.module.sequence.helper as helper
@@ -51,7 +50,7 @@ class EnableStatusFilters(bpy.types.Operator):
pset = element.PartOfPset[0]
if pset.Name.startswith("Pset_") and pset.Name.endswith("Common"):
statuses.update(element.EnumerationValues)
elif pset.Name == "EPset_Status": # Our secret sauce
elif pset.Name == "EPset_Status": # Our secret sauce
statuses.update(element.EnumerationValues)
elif element.Name == "UserDefinedStatus":
statuses.add(element.NominalValue)
@@ -67,6 +66,7 @@ class EnableStatusFilters(bpy.types.Operator):
class DisableStatusFilters(bpy.types.Operator):
bl_idname = "bim.disable_status_filters"
bl_label = "Disable Status Filters"
bl_description = "Deactivate status filters panel.\nCan be used to refresh the displayed statuses"
def execute(self, context):
props = context.scene.BIMStatusProperties
@@ -77,6 +77,7 @@ class DisableStatusFilters(bpy.types.Operator):
class ActivateStatusFilters(bpy.types.Operator):
bl_idname = "bim.activate_status_filters"
bl_label = "Activate Status Filters"
bl_description = "Filter and display objects based on currently selected IFC statuses"
def execute(self, context):
props = context.scene.BIMStatusProperties
@@ -92,6 +93,7 @@ class ActivateStatusFilters(bpy.types.Operator):
query = " + ".join(query)
if not query:
self.report({"INFO"}, "No statuses selected.")
return {"FINISHED"}
visible_elements = ifcopenshell.util.selector.filter_elements(tool.Ifc.get(), query)
@@ -107,6 +109,7 @@ class ActivateStatusFilters(bpy.types.Operator):
class SelectStatusFilter(bpy.types.Operator):
bl_idname = "bim.select_status_filter"
bl_label = "Select Status Filter"
bl_description = "Select elements with currently selected status"
name: bpy.props.StringProperty()
def execute(self, context):
@@ -368,6 +371,7 @@ class EditTaskTime(bpy.types.Operator, tool.Ifc.Operator):
core.edit_task_time(
tool.Ifc,
tool.Sequence,
tool.Resource,
task_time=tool.Ifc.get().by_id(context.scene.BIMWorkScheduleProperties.active_task_time_id),
)
@@ -504,7 +508,7 @@ class AssignProcess(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
if self.related_object_type == "RESOURCE":
core.assign_resource(tool.Ifc, tool.Sequence, task=tool.Ifc.get().by_id(self.task))
core.assign_resource(tool.Ifc, tool.Sequence, tool.Resource, task=tool.Ifc.get().by_id(self.task))
elif self.related_object_type == "PRODUCT":
if self.related_object:
core.assign_input_products(
@@ -537,6 +541,7 @@ class UnassignProcess(bpy.types.Operator):
core.unassign_resource(
tool.Ifc,
tool.Sequence,
tool.Resource,
task=tool.Ifc.get().by_id(self.task),
resource=tool.Ifc.get().by_id(self.resource),
)
@@ -224,10 +224,8 @@ def updateTaskDuration(self, context):
else:
task_time = tool.Ifc.run("sequence.add_task_time", task=task)
tool.Ifc.run("sequence.edit_task_time", task_time=task_time, attributes={"ScheduleDuration": duration})
SequenceData.load()
blenderbim.core.sequence.load_task_properties(tool.Sequence)
bpy.ops.bim.load_task_properties()
tool.Sequence.load_resources()
tool.Sequence.refresh_task_resources()
def get_schedule_predefined_types(self, context):
@@ -331,22 +329,32 @@ def get_saved_color_schemes(self, context):
def updateAssignedResourceName(self, context):
pass
def updateAssignedResourceUsage(self, context):
if not context.scene.BIMResourceProperties.is_resource_update_enabled:
return
if not self.schedule_usage:
return
resource = tool.Ifc.get().by_id(self.ifc_definition_id)
if resource.Usage and resource.Usage.ScheduleUsage == self.schedule_usage:
return
tool.Resource.run_edit_resource_time(resource, attributes={
"ScheduleUsage": self.schedule_usage
})
tool.Resource.run_edit_resource_time(resource, attributes={"ScheduleUsage": self.schedule_usage})
tool.Sequence.load_task_properties()
tool.Resource.load_resource_properties()
tool.Sequence.refresh_task_resources()
blenderbim.bim.module.resource.data.refresh()
blenderbim.bim.module.sequence.data.refresh()
refresh_sequence_data()
blenderbim.bim.module.pset.data.refresh()
def update_task_bar_list(self, context):
if not context.scene.BIMWorkScheduleProperties.is_task_update_enabled:
return
if self.has_bar_visual:
tool.Sequence.add_task_bar(self.ifc_definition_id)
else:
tool.Sequence.remove_task_bar(self.ifc_definition_id)
class Task(PropertyGroup):
name: StringProperty(name="Name", update=updateTaskName)
identification: StringProperty(name="Identification", update=updateTaskIdentification)
@@ -354,7 +362,7 @@ class Task(PropertyGroup):
has_children: BoolProperty(name="Has Children")
is_selected: BoolProperty(name="Is Selected")
is_expanded: BoolProperty(name="Is Expanded")
has_bar_visual: BoolProperty(name="Show Task Bar Animation", default=False)
has_bar_visual: BoolProperty(name="Show Task Bar Animation", default=False, update=update_task_bar_list)
level_index: IntProperty(name="Level Index")
duration: StringProperty(name="Duration", update=updateTaskDuration)
start: StringProperty(name="Start", update=updateTaskTimeStart)
@@ -405,7 +413,7 @@ class ISODuration(PropertyGroup):
class IFCStatus(PropertyGroup):
name: StringProperty(name="Name")
is_visible: BoolProperty(name="Is Visible", default=True)
is_visible: BoolProperty(name="Is Visible", default=True, update=lambda x, y: bpy.ops.bim.activate_status_filters())
class BIMStatusProperties(PropertyGroup):
@@ -456,6 +464,7 @@ class BIMWorkScheduleProperties(PropertyGroup):
active_task_time_id: IntProperty(name="Active Task Time Id")
task_time_attributes: CollectionProperty(name="Task Time Attributes", type=Attribute)
contracted_tasks: StringProperty(name="Contracted Task Items", default="[]")
task_bars: StringProperty(name="Checked Task Items", default="[]")
is_task_update_enabled: BoolProperty(name="Is Task Update Enabled", default=True)
editing_sequence_type: StringProperty(name="Editing Sequence Type")
active_sequence_id: IntProperty(name="Active Sequence Id")
@@ -52,7 +52,7 @@ class BIM_PT_status(Panel):
return
row = self.layout.row(align=True)
row.operator("bim.activate_status_filters", icon="TIME")
row.label(text="Statuses found in the project:")
row.operator("bim.disable_status_filters", icon="CANCEL", text="")
for status in self.props.statuses:
@@ -556,7 +556,7 @@ class BIM_PT_animation_tools(Panel):
row = self.layout.row(align=True)
row.alignment = "RIGHT"
if self.animation_props.saved_color_schemes:
if AnimationColorSchemeData.data["saved_color_schemes"]:
row.prop(self.animation_props, "saved_color_schemes", text="Color Scheme", icon="SEQUENCE_COLOR_04")
else:
row.label(text="No Color Scheme Saved", icon="INFO")
@@ -727,7 +727,11 @@ class BIM_PT_task_icom(Panel):
if total_task_outputs:
op = row2.operator("bim.unassign_product", icon="REMOVE", text="")
op.task = task.ifc_definition_id
if not context.selected_objects and self.props.active_task_output_index < total_task_outputs:
if (
total_task_outputs
and not context.selected_objects
and self.props.active_task_output_index < total_task_outputs
):
output_id = self.props.task_outputs[self.props.active_task_output_index].ifc_definition_id
op.relating_product = output_id
@@ -770,6 +774,7 @@ class BIM_UL_task_resources(UIList):
row.prop(item, "name", emboss=False, text="")
row.prop(item, "schedule_usage", emboss=False, text="")
class BIM_UL_animation_colors(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
@@ -143,7 +143,7 @@ class CopyToContainer(bpy.types.Operator, tool.Ifc.Operator):
old_to_new[tool.Ifc.get_entity(obj)] = result_objs
# Recreate decompositions
tool.Root.recreate_decompositions(relationships, old_to_new)
blenderbim.bim.handler.purge_module_data()
blenderbim.bim.handler.refresh_ui_data()
class SelectContainer(bpy.types.Operator, tool.Ifc.Operator):
@@ -44,10 +44,9 @@ def update_elevation(self, context):
def update_active_container_index(self, context):
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
self.active_container_id = self.containers[self.active_container_index].ifc_definition_id
self.container_name = self.containers[self.active_container_index].name
self.elevation = self.containers[self.active_container_index].elevation * si_conversion
self.elevation = self.containers[self.active_container_index].elevation
def updateContainerName(self, context):
@@ -78,7 +77,7 @@ class BIMObjectSpatialProperties(PropertyGroup):
class BIMContainer(PropertyGroup):
name: StringProperty(name="Name", update=updateContainerName)
elevation: FloatProperty(name="Elevation")
elevation: FloatProperty(name="Elevation", subtype="DISTANCE")
level_index: IntProperty(name="Level Index")
has_children: BoolProperty(name="Has Children")
is_expanded: BoolProperty(name="Is Expanded")
@@ -147,7 +147,7 @@ class BIM_UL_containers_manager(UIList):
split1 = row.split(factor=0.7)
split1.prop(item, "name", emboss=False, text="")
split2 = row.split(factor=1)
split2.label(text=str(item.elevation), icon="BLANK1")
split2.label(icon="BLANK1", text=tool.Unit.blender_format_unit(item.elevation))
def draw_hierarchy(self, row, item):
for i in range(0, item.level_index):
@@ -17,7 +17,7 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
from . import ui, prop, operator
from . import ui, prop, operator, decorator
classes = (
operator.AddPort,
@@ -51,7 +51,9 @@ classes = (
def register():
bpy.types.Scene.BIMSystemProperties = bpy.props.PointerProperty(type=prop.BIMSystemProperties)
bpy.app.handlers.load_post.append(decorator.toggle_decorations_on_load)
def unregister():
del bpy.types.Scene.BIMSystemProperties
bpy.app.handlers.load_post.remove(decorator.toggle_decorations_on_load)
@@ -27,6 +27,7 @@ def refresh():
SystemData.is_loaded = False
ObjectSystemData.is_loaded = False
PortData.is_loaded = False
SystemDecorationData.is_loaded = False
class SystemData:
@@ -67,16 +68,18 @@ class ObjectSystemData:
cls.data = {
"systems": cls.systems(),
"total_systems": cls.total_systems(),
# AFTER SYSTEMS
"connected_elements": cls.connected_elements(),
}
cls.is_loaded = True
@classmethod
def systems(cls):
results = []
element = tool.Ifc.get_entity(bpy.context.active_object)
if not element:
cls.element = tool.Ifc.get_entity(bpy.context.active_object)
if not cls.element:
return results
for system in ifcopenshell.util.system.get_element_systems(element):
for system in ifcopenshell.util.system.get_element_systems(cls.element):
results.append({"id": system.id(), "name": system.Name or "Unnamed", "ifc_class": system.is_a()})
return results
@@ -84,6 +87,12 @@ class ObjectSystemData:
def total_systems(cls):
return len(tool.Ifc.get().by_type("IfcSystem"))
@classmethod
def connected_elements(cls):
if not cls.element:
return set()
return tool.System.get_connected_elements(cls.element)
class PortData:
data = {}
@@ -101,12 +110,13 @@ class PortData:
"port_connected_object": cls.port_connected_object() if is_port else None,
"port_relating_object": cls.port_relating_object() if is_port else None,
}
# AFTER located_ports_data
cls.data["selected_objects_flow_direction"] = cls.selected_objects_flow_direction() if not is_port else None
cls.is_loaded = True
@classmethod
def total_ports(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
return len(ifcopenshell.util.system.get_ports(element))
return len(ifcopenshell.util.system.get_ports(cls.element))
@classmethod
def is_port(cls):
@@ -126,17 +136,57 @@ class PortData:
@classmethod
def located_ports_data(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
ports = ifcopenshell.util.system.get_ports(element)
ports = ifcopenshell.util.system.get_ports(cls.element)
data = []
for port in ports:
port_obj = tool.Ifc.get_object(port)
connected_port = tool.System.get_connected_port(port)
if connected_port:
connected_element = tool.Ifc.get_object(tool.System.get_port_relating_element(connected_port))
connected_obj = tool.Ifc.get_object(tool.System.get_port_relating_element(connected_port))
else:
connected_element = None
connected_obj = None
data.append((port, port_obj, connected_element))
data.append((port, port_obj, connected_obj))
return data
@classmethod
def selected_objects_flow_direction(cls):
for port, port_obj, connected_obj in cls.data["located_ports_data"]:
if connected_obj in bpy.context.selected_objects:
return port.FlowDirection
class SystemDecorationData:
data = {}
elements_ports_positions = {}
is_loaded = False
@classmethod
def load(cls):
cls.data = {}
cls.is_loaded = True
cls.elements_ports_positions = {}
@classmethod
def get_element_ports_data(cls, element):
"""returns element's port data, caches the data until UI update
Port data includes:
- local port position in SI units
- port flow direction
"""
if element not in cls.elements_ports_positions:
ports = tool.System.get_ports(element)
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
ports_data = []
for port in ports:
position = tool.Model.get_element_matrix(port, keep_local=True).translation * si_conversion
port_data = {
"position": position,
"flow_direction": port.FlowDirection,
}
ports_data.append(port_data)
cls.elements_ports_positions[element] = ports_data
return cls.elements_ports_positions[element]
@@ -0,0 +1,145 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2023 Dion Moult <dion@thinkmoult.com>, @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 bpy
import gpu
import bmesh
import blenderbim.tool as tool
from math import sin, cos, radians
from bpy.types import SpaceView3D
from mathutils import Vector, Matrix
from gpu_extras.batch import batch_for_shader
import ifcopenshell
from blenderbim.bim.module.system.data import SystemDecorationData
from bpy.app.handlers import persistent
ERROR_ELEMENTS_COLOR = (1, 0.2, 0.322, 1) # RED
UNSPECIAL_ELEMENT_COLOR = (0.2, 0.2, 0.2, 1) # GREY
def transparent_color(color, alpha=0.1):
color = [i for i in color]
color[3] = alpha
return color
@persistent
def toggle_decorations_on_load(*args):
if bpy.context.scene.BIMSystemProperties.should_draw_decorations:
SystemDecorator.install(bpy.context)
else:
SystemDecorator.uninstall()
class SystemDecorator:
installed = None
@classmethod
def install(cls, context, get_custom_bmesh=None, draw_faces=False, exit_edit_mode_callback=None):
"""Note that operators that change mesh in `exit_edit_mode_callback` can freeze blender.
The workaround is to move their code to function and use it for callback.
Example: https://devtalk.blender.org/t/calling-operator-that-saves-bmesh-freezes-blender-forever/28595"""
if cls.installed:
cls.uninstall()
handler = cls()
cls.installed = SpaceView3D.draw_handler_add(
handler, (context, get_custom_bmesh, draw_faces, exit_edit_mode_callback), "WINDOW", "POST_VIEW"
)
@classmethod
def uninstall(cls):
try:
SpaceView3D.draw_handler_remove(cls.installed, "WINDOW")
except ValueError:
pass
cls.installed = None
def draw_batch(self, shader_type, content_pos, color, indices=None):
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
batch.draw(shader)
def draw_faces(self, bm, vertices_coords):
"""mutates original bm (triangulates it)
so the triangulation edges will be shown too
"""
traingulated_bm = bm
bmesh.ops.triangulate(traingulated_bm, faces=traingulated_bm.faces)
face_indices = [[v.index for v in f.verts] for f in traingulated_bm.faces]
faces_color = transparent_color(self.addon_prefs.decorator_color_special)
self.draw_batch("TRIS", vertices_coords, faces_color, face_indices)
def __call__(self, context, get_custom_bmesh=None, draw_faces=False, exit_edit_mode_callback=None):
self.addon_prefs = context.preferences.addons["blenderbim"].preferences
selected_elements_color = self.addon_prefs.decorator_color_selected
unselected_elements_color = self.addon_prefs.decorator_color_unselected
special_elements_color = self.addon_prefs.decorator_color_special
gpu.state.point_size_set(6)
gpu.state.blend_set("ALPHA")
### Actually drawing
all_vertices = []
error_vertices = []
selected_vertices = []
unselected_vertices = []
# special = associated with arcs/circles
special_vertices = []
special_vertex_indices = {}
selected_edges = []
unselected_edges = []
arc_edges = []
roof_angle_edges = []
preview_edges = []
# NOTE: using live update because Data wouldn't allow
# live time update of objects positions
decoration_data = tool.System.get_decoration_data()
all_vertices = decoration_data["all_vertices"]
preview_edges = decoration_data["preview_edges"]
special_vertices = decoration_data["special_vertices"]
selected_edges = decoration_data["selected_edges"]
selected_vertices = decoration_data["selected_vertices"]
### Actually drawing
# 3D_POLYLINE_UNIFORM_COLOR is good for smoothed lines since `bgl.enable(GL_LINE_SMOOTH)` is deprecated
self.line_shader = gpu.shader.from_builtin("3D_POLYLINE_UNIFORM_COLOR")
self.line_shader.bind()
# POLYLINE_UNIFORM_COLOR specific uniforms
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
self.line_shader.uniform_float("lineWidth", 2.0)
# general shader
self.shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
self.shader.bind()
self.draw_batch("LINES", all_vertices, transparent_color(unselected_elements_color), unselected_edges)
self.draw_batch("LINES", all_vertices, selected_elements_color, selected_edges)
self.draw_batch("LINES", all_vertices, UNSPECIAL_ELEMENT_COLOR, arc_edges)
self.draw_batch("LINES", all_vertices, special_elements_color, preview_edges)
self.draw_batch("LINES", all_vertices, special_elements_color, roof_angle_edges)
self.draw_batch("POINTS", unselected_vertices, transparent_color(unselected_elements_color, 0.5))
self.draw_batch("POINTS", error_vertices, ERROR_ELEMENTS_COLOR)
self.draw_batch("POINTS", special_vertices, special_elements_color)
self.draw_batch("POINTS", selected_vertices, selected_elements_color)
@@ -219,12 +219,15 @@ class DisconnectPort(bpy.types.Operator, Operator):
class MEPConnectElements(bpy.types.Operator, Operator):
bl_idname = "bim.mep_connect_elements"
bl_label = "Connect MEP Elements"
bl_description = "Connects two selected elements if they have ports with matching location"
bl_description = "Connects two selected elements by their closest located ports and adjusts them"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return len(context.selected_objects) == 2
if not len(context.selected_objects) == 2:
cls.poll_message_set("Need to select 2 objects.")
return False
return True
def _execute(self, context):
obj1 = context.active_object
@@ -233,23 +236,32 @@ class MEPConnectElements(bpy.types.Operator, Operator):
el1 = tool.Ifc.get_entity(obj1)
el2 = tool.Ifc.get_entity(obj2)
connected_elements = ifcopenshell.util.system.get_connected_to(el1)
connected_elements += ifcopenshell.util.system.get_connected_to(el2)
if el2 in connected_elements:
self.report({"ERROR"}, "MEP elements are already connected to each other.")
return {"CANCELLED"}
obj1_ports = [p for p in tool.System.get_ports(el1) if not tool.System.get_connected_port(p)]
obj2_ports = [p for p in tool.System.get_ports(el2) if not tool.System.get_connected_port(p)]
if not obj1_ports or not obj2_ports:
self.report({"ERROR"}, "Couldn't find free ports to connect.")
return
return {"CANCELLED"}
ports_distance = dict()
for port1 in obj1_ports:
port1_location = tool.Model.get_element_matrix(port1).translation
for port2 in obj2_ports:
port2_location = tool.Model.get_element_matrix(port2).translation
if tool.Cad.are_vectors_equal(port1_location, port2_location):
core.connect_port(tool.Ifc, port1, port2)
return {"FINISHED"}
distance = (port1_location - port2_location).length
ports_distance[(port1, port2)] = distance
self.report({"ERROR"}, "Couldn't find any matching ports to connect.")
return {"CANCELLED"}
closest_ports = min(ports_distance, key=lambda x: ports_distance[x])
core.connect_port(tool.Ifc, *closest_ports)
bpy.ops.bim.regenerate_distribution_element()
return {"FINISHED"}
class SetFlowDirection(bpy.types.Operator, Operator):
@@ -258,12 +270,59 @@ class SetFlowDirection(bpy.types.Operator, Operator):
bl_options = {"REGISTER", "UNDO"}
direction: bpy.props.StringProperty()
@classmethod
def description(cls, context, operator):
if not PortData.is_loaded:
PortData.load()
port = PortData.data["is_port"]
if port:
return f"Set port flow direction to {operator.direction}"
else:
return f"Set flow direction to {operator.direction} for active element relatively to the selected"
@classmethod
def poll(cls, context):
if not PortData.is_loaded:
PortData.load()
port = PortData.data["is_port"]
if not port and not len(context.selected_objects) == 2:
cls.poll_message_set("Need to select port or 2 connected objects.")
return False
return True
def _execute(self, context):
port = tool.Ifc.get_entity(context.active_object)
second_port = tool.System.get_connected_port(port)
if not second_port:
self.report({"ERROR"}, "To set flow direction port has to be connected to another one.")
return
core.set_flow_direction(
tool.Ifc, tool.System, port=tool.Ifc.get_entity(context.active_object), direction=self.direction
)
element = tool.Ifc.get_entity(context.active_object)
if element.is_a("IfcDistributionPort"):
second_port = tool.System.get_connected_port(element)
if not second_port:
self.report({"ERROR"}, "To set flow direction port has to be connected to another one.")
return
core.set_flow_direction(tool.Ifc, tool.System, port=element, direction=self.direction)
return {"FINISHED"}
selected_elements = [
entity
for entity in (tool.Ifc.get_entity(o) for o in context.selected_objects)
if entity and tool.System.is_mep_element(element)
]
if len(selected_elements) != 2:
self.report({"ERROR"}, "To set flow direction selected two connected MEP elements or just 1 port.")
return {"CANCELLED"}
other_element = selected_elements[selected_elements[0] == element]
active_element_ports = tool.System.get_ports(element)
other_element_ports = tool.System.get_ports(other_element)
for port in active_element_ports:
connected_port = tool.System.get_connected_port(port)
if connected_port in other_element_ports:
core.set_flow_direction(tool.Ifc, tool.System, port=port, direction=self.direction)
tool.Blender.update_viewport()
return {"FINISHED"}
self.report({"ERROR"}, "Selected elements are not connected to set the flow direction")
return {"CANCELLED"}
@@ -18,6 +18,7 @@
import bpy
from blenderbim.bim.module.system.data import SystemData
import blenderbim.bim.module.system.decorator as decorator
from blenderbim.bim.prop import StrProperty, Attribute
from bpy.types import PropertyGroup
from bpy.props import (
@@ -44,6 +45,14 @@ class System(PropertyGroup):
ifc_definition_id: IntProperty(name="IFC Definition ID")
def toggle_decorations(self, context):
toggle = self.should_draw_decorations
if toggle:
decorator.SystemDecorator.install(context)
else:
decorator.SystemDecorator.uninstall()
class BIMSystemProperties(PropertyGroup):
system_attributes: CollectionProperty(name="System Attributes", type=Attribute)
is_editing: BoolProperty(name="Is Editing", default=False)
@@ -52,3 +61,6 @@ class BIMSystemProperties(PropertyGroup):
active_system_index: IntProperty(name="Active System Index")
active_system_id: IntProperty(name="Active System Id")
system_class: EnumProperty(items=get_system_class, name="Class")
should_draw_decorations: BoolProperty(
name="Should Draw Decorations", description="Toggle system decorations", update=toggle_decorations
)
@@ -24,10 +24,10 @@ from blenderbim.bim.module.system.data import SystemData, ObjectSystemData, Port
FLOW_DIRECTION_TO_ICON = {
"SOURCE": "FORWARD",
"SINK": "BACK",
"SOURCE": "REMOVE",
"SINK": "ADD",
"SOURCEANDSINK": "ARROW_LEFTRIGHT",
"NOTDEFINED": "RESTRICT_INSTANCED_ON",
"NOTDEFINED": "CHECKBOX_DEHLT",
}
@@ -101,6 +101,10 @@ class BIM_PT_object_systems(Panel):
if not ObjectSystemData.is_loaded:
ObjectSystemData.load()
self.props = context.scene.BIMSystemProperties
row = self.layout.row(align=True)
row.prop(self.props, "should_draw_decorations")
if self.props.is_editing:
row = self.layout.row()
row.alignment = "RIGHT"
@@ -175,6 +179,19 @@ class BIM_PT_ports(Panel):
if total_ports == 0:
return
row = self.layout.row(align=True)
row.label(text="Change Flow Direction:")
current_flow_direction = PortData.data["selected_objects_flow_direction"]
for flow_direction in FLOW_DIRECTION_TO_ICON.keys():
row.operator(
"bim.set_flow_direction",
icon=FLOW_DIRECTION_TO_ICON[flow_direction],
depress=flow_direction == current_flow_direction,
text="",
).direction = flow_direction
row.enabled = len(context.selected_objects) == 2
row = self.layout.row(align=True)
row.label(text="Ports located on object and connected objects:")
row = self.layout.row(align=True)
@@ -266,16 +283,15 @@ class BIM_PT_port(Panel):
else:
row.label(text="Port is not connected to any element")
# TODO: replace with enum property?
row = layout.row(align=True)
row.label(text="Change Flow Direction:")
for flow_direction in FLOW_DIRECTION_TO_ICON.keys():
row = layout.row()
row.operator(
"bim.set_flow_direction", icon=FLOW_DIRECTION_TO_ICON[flow_direction], text=flow_direction
"bim.set_flow_direction",
icon=FLOW_DIRECTION_TO_ICON[flow_direction],
depress=flow_direction == current_flow_direction,
text="",
).direction = flow_direction
if flow_direction == current_flow_direction:
row.enabled = False
class BIM_UL_systems(UIList):
@@ -131,7 +131,8 @@ class SelectRequirement(bpy.types.Operator):
props.failed_entities.clear()
for e in failed_entities:
new_entity = props.failed_entities.add()
new_entity.element = e["element"]
new_entity.ifc_id = e["id"]
new_entity.element = f'{e["class"]}/{e["name"]}'
new_entity.reason = e["reason"]
return {"FINISHED"}
@@ -31,10 +31,6 @@ from bpy.props import (
)
def purge():
pass
def update_active_specification_index(self, context):
TesterData.load()
@@ -45,8 +41,9 @@ class Specification(PropertyGroup):
class FailedEntities(PropertyGroup):
reason: StringProperty(name="Reason")
ifc_id: IntProperty(name="IFC ID")
element: StringProperty(name="Element")
reason: StringProperty(name="Reason")
class IfcTesterProperties(PropertyGroup):
@@ -115,17 +115,9 @@ class BIM_UL_tester_failed_entities(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
props = context.scene.IfcTesterProperties
if item:
if props.should_load_from_memory:
ifc_file = tool.Ifc.get()
ifc_id = int(item.element[1 : item.element.find("=")])
entity = ifc_file.by_id(ifc_id)
report_entity = f"[#{ifc_id}][{entity.is_a()}] {entity.Name}"
else:
report_entity = item.element
row = layout.row(align=True)
row.label(text=report_entity)
row.label(text=item.element)
row.label(text=item.reason)
if props.should_load_from_memory:
op = row.operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF")
op.ifc_id = entity.id()
op.ifc_id = item.ifc_id

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