diff --git a/README.md b/README.md
index e9416797e8..e8670871c8 100644
--- a/README.md
+++ b/README.md
@@ -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 |
diff --git a/cmake/CMakeLists.txt b/cmake/CMakeLists.txt
index 0f428738bf..05b9c9cfb5 100644
--- a/cmake/CMakeLists.txt
+++ b/cmake/CMakeLists.txt
@@ -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)
diff --git a/src/bcf/README.md b/src/bcf/README.md
index a792c4f79f..aa1e4951e3 100644
--- a/src/bcf/README.md
+++ b/src/bcf/README.md
@@ -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`.
diff --git a/src/blenderbim/Makefile b/src/blenderbim/Makefile
index 736a2277a1..08f291afa1 100644
--- a/src/blenderbim/Makefile
+++ b/src/blenderbim/Makefile
@@ -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
diff --git a/src/blenderbim/blenderbim/bim/__init__.py b/src/blenderbim/blenderbim/bim/__init__.py
index de25096754..ccafb12784 100644
--- a/src/blenderbim/blenderbim/bim/__init__.py
+++ b/src/blenderbim/blenderbim/bim/__init__.py
@@ -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,
diff --git a/src/blenderbim/blenderbim/bim/data/assets/symbols.svg b/src/blenderbim/blenderbim/bim/data/assets/symbols.svg
index 5aca6a35eb..4ae888d5e5 100644
--- a/src/blenderbim/blenderbim/bim/data/assets/symbols.svg
+++ b/src/blenderbim/blenderbim/bim/data/assets/symbols.svg
@@ -47,6 +47,13 @@
+
+
+
+
+
+
+
diff --git a/src/blenderbim/blenderbim/bim/data/libraries/IFC4 Demo Library.ifc b/src/blenderbim/blenderbim/bim/data/libraries/IFC4 Demo Library.ifc
index 190b13d9a4..7bec97bf36 100644
--- a/src/blenderbim/blenderbim/bim/data/libraries/IFC4 Demo Library.ifc
+++ b/src/blenderbim/blenderbim/bim/data/libraries/IFC4 Demo Library.ifc
@@ -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;
diff --git a/src/blenderbim/blenderbim/bim/handler.py b/src/blenderbim/blenderbim/bim/handler.py
index c8a2882475..7f2657e5ab 100644
--- a/src/blenderbim/blenderbim/bim/handler.py
+++ b/src/blenderbim/blenderbim/bim/handler.py
@@ -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()
diff --git a/src/blenderbim/blenderbim/bim/helper.py b/src/blenderbim/blenderbim/bim/helper.py
index aa2100b0c5..85b0ddf607 100644
--- a/src/blenderbim/blenderbim/bim/helper.py
+++ b/src/blenderbim/blenderbim/bim/helper.py
@@ -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")
diff --git a/src/blenderbim/blenderbim/bim/ifc.py b/src/blenderbim/blenderbim/bim/ifc.py
index c35a942aaa..312527beb4 100644
--- a/src/blenderbim/blenderbim/bim/ifc.py
+++ b/src/blenderbim/blenderbim/bim/ifc.py
@@ -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
)
diff --git a/src/blenderbim/blenderbim/bim/import_ifc.py b/src/blenderbim/blenderbim/bim/import_ifc.py
index 5c20aea10c..762a482382 100644
--- a/src/blenderbim/blenderbim/bim/import_ifc.py
+++ b/src/blenderbim/blenderbim/bim/import_ifc.py
@@ -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):
diff --git a/src/blenderbim/blenderbim/bim/module/aggregate/operator.py b/src/blenderbim/blenderbim/bim/module/aggregate/operator.py
index d09179f6b1..bee67a413e 100644
--- a/src/blenderbim/blenderbim/bim/module/aggregate/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/aggregate/operator.py
@@ -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"}
diff --git a/src/blenderbim/blenderbim/bim/module/bsdd/__init__.py b/src/blenderbim/blenderbim/bim/module/bsdd/__init__.py
new file mode 100644
index 0000000000..63f62931a6
--- /dev/null
+++ b/src/blenderbim/blenderbim/bim/module/bsdd/__init__.py
@@ -0,0 +1,42 @@
+# BlenderBIM Add-on - OpenBIM Blender Add-on
+# Copyright (C) 2023 Dion Moult
+#
+# 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 .
+
+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
diff --git a/src/blenderbim/blenderbim/bim/module/bsdd/operator.py b/src/blenderbim/blenderbim/bim/module/bsdd/operator.py
new file mode 100644
index 0000000000..f3ecba2204
--- /dev/null
+++ b/src/blenderbim/blenderbim/bim/module/bsdd/operator.py
@@ -0,0 +1,132 @@
+# BlenderBIM Add-on - OpenBIM Blender Add-on
+# Copyright (C) 2023 Dion Moult
+#
+# 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 .
+
+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"}
diff --git a/src/blenderbim/blenderbim/bim/module/bsdd/prop.py b/src/blenderbim/blenderbim/bim/module/bsdd/prop.py
new file mode 100644
index 0000000000..cf8e3e95e1
--- /dev/null
+++ b/src/blenderbim/blenderbim/bim/module/bsdd/prop.py
@@ -0,0 +1,66 @@
+# BlenderBIM Add-on - OpenBIM Blender Add-on
+# Copyright (C) 2023 Dion Moult
+#
+# 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 .
+
+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)
diff --git a/src/blenderbim/blenderbim/bim/module/bsdd/ui.py b/src/blenderbim/blenderbim/bim/module/bsdd/ui.py
new file mode 100644
index 0000000000..8577c3242a
--- /dev/null
+++ b/src/blenderbim/blenderbim/bim/module/bsdd/ui.py
@@ -0,0 +1,71 @@
+# BlenderBIM Add-on - OpenBIM Blender Add-on
+# Copyright (C) 2023 Dion Moult
+#
+# 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 .
+
+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)
diff --git a/src/blenderbim/blenderbim/bim/module/clash/operator.py b/src/blenderbim/blenderbim/bim/module/clash/operator.py
index 933cc06b48..6013caf2b4 100644
--- a/src/blenderbim/blenderbim/bim/module/clash/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/clash/operator.py
@@ -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"}
diff --git a/src/blenderbim/blenderbim/bim/module/classification/__init__.py b/src/blenderbim/blenderbim/bim/module/classification/__init__.py
index f5fdcaaaed..d59b2656b3 100644
--- a/src/blenderbim/blenderbim/bim/module/classification/__init__.py
+++ b/src/blenderbim/blenderbim/bim/module/classification/__init__.py
@@ -21,7 +21,9 @@ from . import ui, prop, operator
classes = (
operator.AddClassification,
+ operator.AddClassificationFromBSDD,
operator.AddClassificationReference,
+ operator.AddClassificationReferenceFromBSDD,
operator.ChangeClassificationLevel,
operator.DisableEditingClassification,
operator.DisableEditingClassificationReference,
diff --git a/src/blenderbim/blenderbim/bim/module/classification/operator.py b/src/blenderbim/blenderbim/bim/module/classification/operator.py
index d9b1b56845..8ec27ad731 100644
--- a/src/blenderbim/blenderbim/bim/module/classification/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/classification/operator.py
@@ -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"
diff --git a/src/blenderbim/blenderbim/bim/module/classification/prop.py b/src/blenderbim/blenderbim/bim/module/classification/prop.py
index eee7b14952..22c4e6af5c 100644
--- a/src/blenderbim/blenderbim/bim/module/classification/prop.py
+++ b/src/blenderbim/blenderbim/bim/module/classification/prop.py
@@ -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")
diff --git a/src/blenderbim/blenderbim/bim/module/classification/ui.py b/src/blenderbim/blenderbim/bim/module/classification/ui.py
index 5a4de6bf65..f63c887fdf 100644
--- a/src/blenderbim/blenderbim/bim/module/classification/ui.py
+++ b/src/blenderbim/blenderbim/bim/module/classification/ui.py
@@ -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)
diff --git a/src/blenderbim/blenderbim/bim/module/cobie/operator.py b/src/blenderbim/blenderbim/bim/module/cobie/operator.py
deleted file mode 100644
index 3f1a512116..0000000000
--- a/src/blenderbim/blenderbim/bim/module/cobie/operator.py
+++ /dev/null
@@ -1,124 +0,0 @@
-# BlenderBIM Add-on - OpenBIM Blender Add-on
-# Copyright (C) 2020, 2021 Dion Moult
-#
-# 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 .
-
-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"}
diff --git a/src/blenderbim/blenderbim/bim/module/covering/__init__.py b/src/blenderbim/blenderbim/bim/module/covering/__init__.py
new file mode 100644
index 0000000000..73c39a93af
--- /dev/null
+++ b/src/blenderbim/blenderbim/bim/module/covering/__init__.py
@@ -0,0 +1,41 @@
+# BlenderBIM Add-on - OpenBIM Blender Add-on
+# Copyright (C) 2020, 2021 Dion Moult
+#
+# 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 .
+
+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
diff --git a/src/blenderbim/blenderbim/bim/module/covering/ops.authoring.covering.dat b/src/blenderbim/blenderbim/bim/module/covering/ops.authoring.covering.dat
new file mode 100644
index 0000000000..8a860d3149
Binary files /dev/null and b/src/blenderbim/blenderbim/bim/module/covering/ops.authoring.covering.dat differ
diff --git a/src/blenderbim/blenderbim/bim/module/cobie/prop.py b/src/blenderbim/blenderbim/bim/module/covering/prop.py
similarity index 71%
rename from src/blenderbim/blenderbim/bim/module/cobie/prop.py
rename to src/blenderbim/blenderbim/bim/module/covering/prop.py
index 567a0f9960..d66aac26cd 100644
--- a/src/blenderbim/blenderbim/bim/module/cobie/prop.py
+++ b/src/blenderbim/blenderbim/bim/module/covering/prop.py
@@ -17,6 +17,8 @@
# along with BlenderBIM Add-on. If not, see .
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")
diff --git a/src/blenderbim/blenderbim/bim/module/covering/workspace.py b/src/blenderbim/blenderbim/bim/module/covering/workspace.py
new file mode 100644
index 0000000000..336d3bc2ef
--- /dev/null
+++ b/src/blenderbim/blenderbim/bim/module/covering/workspace.py
@@ -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 .
+
+
+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()
+
diff --git a/src/blenderbim/blenderbim/bim/module/csv/operator.py b/src/blenderbim/blenderbim/bim/module/csv/operator.py
index daa85fbab9..78e980e99e 100644
--- a/src/blenderbim/blenderbim/bim/module/csv/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/csv/operator.py
@@ -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"}
diff --git a/src/blenderbim/blenderbim/bim/module/csv/prop.py b/src/blenderbim/blenderbim/bim/module/csv/prop.py
index db1ae07071..c04fc18ee6 100644
--- a/src/blenderbim/blenderbim/bim/module/csv/prop.py
+++ b/src/blenderbim/blenderbim/bim/module/csv/prop.py
@@ -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(
diff --git a/src/blenderbim/blenderbim/bim/module/csv/ui.py b/src/blenderbim/blenderbim/bim/module/csv/ui.py
index 425bb2cb8d..49e096c2bc 100644
--- a/src/blenderbim/blenderbim/bim/module/csv/ui.py
+++ b/src/blenderbim/blenderbim/bim/module/csv/ui.py
@@ -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")
diff --git a/src/blenderbim/blenderbim/bim/module/debug/__init__.py b/src/blenderbim/blenderbim/bim/module/debug/__init__.py
index 1c0de63341..a05c74561a 100644
--- a/src/blenderbim/blenderbim/bim/module/debug/__init__.py
+++ b/src/blenderbim/blenderbim/bim/module/debug/__init__.py
@@ -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,
)
diff --git a/src/blenderbim/blenderbim/bim/module/debug/operator.py b/src/blenderbim/blenderbim/bim/module/debug/operator.py
index 2b67815858..bad542c984 100644
--- a/src/blenderbim/blenderbim/bim/module/debug/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/debug/operator.py
@@ -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.")
diff --git a/src/blenderbim/blenderbim/bim/module/debug/prop.py b/src/blenderbim/blenderbim/bim/module/debug/prop.py
index 08e7699a61..fff62ef57b 100644
--- a/src/blenderbim/blenderbim/bim/module/debug/prop.py
+++ b/src/blenderbim/blenderbim/bim/module/debug/prop.py
@@ -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="")
diff --git a/src/blenderbim/blenderbim/bim/module/debug/ui.py b/src/blenderbim/blenderbim/bim/module/debug/ui.py
index e8629f7854..205c08f480 100644
--- a/src/blenderbim/blenderbim/bim/module/debug/ui.py
+++ b/src/blenderbim/blenderbim/bim/module/debug/ui.py
@@ -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()
diff --git a/src/blenderbim/blenderbim/bim/module/diff/operator.py b/src/blenderbim/blenderbim/bim/module/diff/operator.py
index 350f28867f..b11aa9c288 100644
--- a/src/blenderbim/blenderbim/bim/module/diff/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/diff/operator.py
@@ -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:
diff --git a/src/blenderbim/blenderbim/bim/module/drawing/decoration.py b/src/blenderbim/blenderbim/bim/module/drawing/decoration.py
index 6b21e95bee..2c0bf10b1f 100644
--- a/src/blenderbim/blenderbim/bim/module/drawing/decoration.py
+++ b/src/blenderbim/blenderbim/bim/module/drawing/decoration.py
@@ -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:
diff --git a/src/blenderbim/blenderbim/bim/module/drawing/helper.py b/src/blenderbim/blenderbim/bim/module/drawing/helper.py
index 95bd35b77b..56a7887d27 100644
--- a/src/blenderbim/blenderbim/bim/module/drawing/helper.py
+++ b/src/blenderbim/blenderbim/bim/module/drawing/helper.py
@@ -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
diff --git a/src/blenderbim/blenderbim/bim/module/drawing/operator.py b/src/blenderbim/blenderbim/bim/module/drawing/operator.py
index 1285187b40..caa9ed66ed 100644
--- a/src/blenderbim/blenderbim/bim/module/drawing/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/drawing/operator.py
@@ -26,7 +26,6 @@ import shutil
import hashlib
import shapely
import subprocess
-import webbrowser
import numpy as np
import multiprocessing
import ifcopenshell
diff --git a/src/blenderbim/blenderbim/bim/module/drawing/scheduler.py b/src/blenderbim/blenderbim/bim/module/drawing/scheduler.py
index a703075772..7050ea9ec4 100644
--- a/src/blenderbim/blenderbim/bim/module/drawing/scheduler.py
+++ b/src/blenderbim/blenderbim/bim/module/drawing/scheduler.py
@@ -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):
diff --git a/src/blenderbim/blenderbim/bim/module/drawing/sheeter.py b/src/blenderbim/blenderbim/bim/module/drawing/sheeter.py
index f452bcc158..ee0e5506e8 100644
--- a/src/blenderbim/blenderbim/bim/module/drawing/sheeter.py
+++ b/src/blenderbim/blenderbim/bim/module/drawing/sheeter.py
@@ -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")
diff --git a/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py b/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py
index 23cbf1ab0f..0ded5f35e2 100644
--- a/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py
+++ b/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py
@@ -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
diff --git a/src/blenderbim/blenderbim/bim/module/cobie/__init__.py b/src/blenderbim/blenderbim/bim/module/fm/__init__.py
similarity index 71%
rename from src/blenderbim/blenderbim/bim/module/cobie/__init__.py
rename to src/blenderbim/blenderbim/bim/module/fm/__init__.py
index a1ad500ae2..de3f0f9448 100644
--- a/src/blenderbim/blenderbim/bim/module/cobie/__init__.py
+++ b/src/blenderbim/blenderbim/bim/module/fm/__init__.py
@@ -1,5 +1,5 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
-# Copyright (C) 2020, 2021 Dion Moult
+# Copyright (C) 2023 Dion Moult
#
# 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
diff --git a/src/blenderbim/blenderbim/bim/module/fm/operator.py b/src/blenderbim/blenderbim/bim/module/fm/operator.py
new file mode 100644
index 0000000000..d48502790a
--- /dev/null
+++ b/src/blenderbim/blenderbim/bim/module/fm/operator.py
@@ -0,0 +1,82 @@
+# BlenderBIM Add-on - OpenBIM Blender Add-on
+# Copyright (C) 2023 Dion Moult
+#
+# 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 .
+
+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"}
diff --git a/src/blenderbim/blenderbim/bim/module/fm/prop.py b/src/blenderbim/blenderbim/bim/module/fm/prop.py
new file mode 100644
index 0000000000..f6a3074da3
--- /dev/null
+++ b/src/blenderbim/blenderbim/bim/module/fm/prop.py
@@ -0,0 +1,54 @@
+# BlenderBIM Add-on - OpenBIM Blender Add-on
+# Copyright (C) 2023 Dion Moult
+#
+# 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 .
+
+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",
+ )
diff --git a/src/blenderbim/blenderbim/bim/module/cobie/ui.py b/src/blenderbim/blenderbim/bim/module/fm/ui.py
similarity index 63%
rename from src/blenderbim/blenderbim/bim/module/cobie/ui.py
rename to src/blenderbim/blenderbim/bim/module/fm/ui.py
index a78cff2b62..8ef8a4dd43 100644
--- a/src/blenderbim/blenderbim/bim/module/cobie/ui.py
+++ b/src/blenderbim/blenderbim/bim/module/fm/ui.py
@@ -1,5 +1,5 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
-# Copyright (C) 2020, 2021 Dion Moult
+# Copyright (C) 2023 Dion Moult
#
# 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")
diff --git a/src/blenderbim/blenderbim/bim/module/geometry/__init__.py b/src/blenderbim/blenderbim/bim/module/geometry/__init__.py
index c6f2a1e1c9..29503856bc 100644
--- a/src/blenderbim/blenderbim/bim/module/geometry/__init__.py
+++ b/src/blenderbim/blenderbim/bim/module/geometry/__init__.py
@@ -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
diff --git a/src/blenderbim/blenderbim/bim/module/geometry/operator.py b/src/blenderbim/blenderbim/bim/module/geometry/operator.py
index 226162559f..af1db13bd6 100644
--- a/src/blenderbim/blenderbim/bim/module/geometry/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/geometry/operator.py
@@ -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)
diff --git a/src/blenderbim/blenderbim/bim/module/geometry/ui.py b/src/blenderbim/blenderbim/bim/module/geometry/ui.py
index 92a8167443..a91cf340ee 100644
--- a/src/blenderbim/blenderbim/bim/module/geometry/ui.py
+++ b/src/blenderbim/blenderbim/bim/module/geometry/ui.py
@@ -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"
diff --git a/src/blenderbim/blenderbim/bim/module/misc/operator.py b/src/blenderbim/blenderbim/bim/module/misc/operator.py
index 3de1e0846e..6806c3635a 100644
--- a/src/blenderbim/blenderbim/bim/module/misc/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/misc/operator.py
@@ -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
diff --git a/src/blenderbim/blenderbim/bim/module/model/__init__.py b/src/blenderbim/blenderbim/bim/module/model/__init__.py
index eed2604fe1..7786a0d8e5 100644
--- a/src/blenderbim/blenderbim/bim/module/model/__init__.py
+++ b/src/blenderbim/blenderbim/bim/module/model/__init__.py
@@ -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 = []
diff --git a/src/blenderbim/blenderbim/bim/module/model/array.py b/src/blenderbim/blenderbim/bim/module/model/array.py
index 5b1606154b..087bf1e436 100644
--- a/src/blenderbim/blenderbim/bim/module/model/array.py
+++ b/src/blenderbim/blenderbim/bim/module/model/array.py
@@ -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)
diff --git a/src/blenderbim/blenderbim/bim/module/model/covering.py b/src/blenderbim/blenderbim/bim/module/model/covering.py
new file mode 100644
index 0000000000..da5f09cb06
--- /dev/null
+++ b/src/blenderbim/blenderbim/bim/module/model/covering.py
@@ -0,0 +1,64 @@
+# BlenderBIM Add-on - OpenBIM Blender Add-on
+# Copyright (C) 2023 Dion Moult
+#
+# 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 .
+
+
+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)
+
diff --git a/src/blenderbim/blenderbim/bim/module/model/door.py b/src/blenderbim/blenderbim/bim/module/model/door.py
index e470b3e3ac..d51ba058a1 100644
--- a/src/blenderbim/blenderbim/bim/module/model/door.py
+++ b/src/blenderbim/blenderbim/bim/module/model/door.py
@@ -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,
diff --git a/src/blenderbim/blenderbim/bim/module/model/mep.py b/src/blenderbim/blenderbim/bim/module/model/mep.py
index e6aaa734da..6306e30544 100644
--- a/src/blenderbim/blenderbim/bim/module/model/mep.py
+++ b/src/blenderbim/blenderbim/bim/module/model/mep.py
@@ -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"}
diff --git a/src/blenderbim/blenderbim/bim/module/model/opening.py b/src/blenderbim/blenderbim/bim/module/model/opening.py
index 714f88f73f..4b4fe6c8e4 100644
--- a/src/blenderbim/blenderbim/bim/module/model/opening.py
+++ b/src/blenderbim/blenderbim/bim/module/model/opening.py
@@ -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])
diff --git a/src/blenderbim/blenderbim/bim/module/model/product.py b/src/blenderbim/blenderbim/bim/module/model/product.py
index 0d56fc63e4..8d67810409 100644
--- a/src/blenderbim/blenderbim/bim/module/model/product.py
+++ b/src/blenderbim/blenderbim/bim/module/model/product.py
@@ -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.
diff --git a/src/blenderbim/blenderbim/bim/module/model/profile.py b/src/blenderbim/blenderbim/bim/module/model/profile.py
index 8bac8d9199..d2576f1185 100644
--- a/src/blenderbim/blenderbim/bim/module/model/profile.py
+++ b/src/blenderbim/blenderbim/bim/module/model/profile.py
@@ -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"})
diff --git a/src/blenderbim/blenderbim/bim/module/model/prop.py b/src/blenderbim/blenderbim/bim/module/model/prop.py
index ab2534cfa8..2723e7904e 100644
--- a/src/blenderbim/blenderbim/bim/module/model/prop.py
+++ b/src/blenderbim/blenderbim/bim/module/model/prop.py
@@ -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
diff --git a/src/blenderbim/blenderbim/bim/module/model/slab.py b/src/blenderbim/blenderbim/bim/module/model/slab.py
index f349aec433..b3cbe091b4 100644
--- a/src/blenderbim/blenderbim/bim/module/model/slab.py
+++ b/src/blenderbim/blenderbim/bim/module/model/slab.py
@@ -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)
diff --git a/src/blenderbim/blenderbim/bim/module/model/space.py b/src/blenderbim/blenderbim/bim/module/model/space.py
index 2af8ae020c..5f0e8edaf3 100644
--- a/src/blenderbim/blenderbim/bim/module/model/space.py
+++ b/src/blenderbim/blenderbim/bim/module/model/space.py
@@ -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"}
diff --git a/src/blenderbim/blenderbim/bim/module/model/ui.py b/src/blenderbim/blenderbim/bim/module/model/ui.py
index 6a0b8005f8..07cd3ec4bd 100644
--- a/src/blenderbim/blenderbim/bim/module/model/ui.py
+++ b/src/blenderbim/blenderbim/bim/module/model/ui.py
@@ -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="")
diff --git a/src/blenderbim/blenderbim/bim/module/model/wall.py b/src/blenderbim/blenderbim/bim/module/model/wall.py
index a10dce6b4c..54b81214a0 100644
--- a/src/blenderbim/blenderbim/bim/module/model/wall.py
+++ b/src/blenderbim/blenderbim/bim/module/model/wall.py
@@ -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)
diff --git a/src/blenderbim/blenderbim/bim/module/model/window.py b/src/blenderbim/blenderbim/bim/module/model/window.py
index 96d3cd5a6b..e432be6797 100644
--- a/src/blenderbim/blenderbim/bim/module/model/window.py
+++ b/src/blenderbim/blenderbim/bim/module/model/window.py
@@ -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,
diff --git a/src/blenderbim/blenderbim/bim/module/model/workspace.py b/src/blenderbim/blenderbim/bim/module/model/workspace.py
index 998de01b1b..6af2600dee 100644
--- a/src/blenderbim/blenderbim/bim/module/model/workspace.py
+++ b/src/blenderbim/blenderbim/bim/module/model/workspace.py
@@ -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"):
diff --git a/src/blenderbim/blenderbim/bim/module/profile/operator.py b/src/blenderbim/blenderbim/bim/module/profile/operator.py
index c8cc8c3925..e507a058bb 100644
--- a/src/blenderbim/blenderbim/bim/module/profile/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/profile/operator.py
@@ -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()
diff --git a/src/blenderbim/blenderbim/bim/module/project/__init__.py b/src/blenderbim/blenderbim/bim/module/project/__init__.py
index 977bc54c64..383b648053 100644
--- a/src/blenderbim/blenderbim/bim/module/project/__init__.py
+++ b/src/blenderbim/blenderbim/bim/module/project/__init__.py
@@ -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
diff --git a/src/blenderbim/blenderbim/bim/module/project/operator.py b/src/blenderbim/blenderbim/bim/module/project/operator.py
index 6c47978d98..ec431b80bb 100644
--- a/src/blenderbim/blenderbim/bim/module/project/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/project/operator.py
@@ -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',
diff --git a/src/blenderbim/blenderbim/bim/module/project/ui.py b/src/blenderbim/blenderbim/bim/module/project/ui.py
index 8b61c4320d..0797238bd1 100644
--- a/src/blenderbim/blenderbim/bim/module/project/ui.py
+++ b/src/blenderbim/blenderbim/bim/module/project/ui.py
@@ -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")
diff --git a/src/blenderbim/blenderbim/bim/module/pset/calc_quantity_function_mapper.py b/src/blenderbim/blenderbim/bim/module/pset/calc_quantity_function_mapper.py
index d366033073..c55b78b00b 100644
--- a/src/blenderbim/blenderbim/bim/module/pset/calc_quantity_function_mapper.py
+++ b/src/blenderbim/blenderbim/bim/module/pset/calc_quantity_function_mapper.py
@@ -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' : {
diff --git a/src/blenderbim/blenderbim/bim/module/pset/data.py b/src/blenderbim/blenderbim/bim/module/pset/data.py
index f30f298dc7..344dcaeed4 100644
--- a/src/blenderbim/blenderbim/bim/module/pset/data.py
+++ b/src/blenderbim/blenderbim/bim/module/pset/data.py
@@ -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
diff --git a/src/blenderbim/blenderbim/bim/module/pset/notes_about_mapped_calculated_quantities.txt b/src/blenderbim/blenderbim/bim/module/pset/notes_about_mapped_calculated_quantities.txt
index fb56c86ad0..73acaecd2f 100644
--- a/src/blenderbim/blenderbim/bim/module/pset/notes_about_mapped_calculated_quantities.txt
+++ b/src/blenderbim/blenderbim/bim/module/pset/notes_about_mapped_calculated_quantities.txt
@@ -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.
diff --git a/src/blenderbim/blenderbim/bim/module/pset/operator.py b/src/blenderbim/blenderbim/bim/module/pset/operator.py
index adbb9cfbcd..340a6b9f8c 100644
--- a/src/blenderbim/blenderbim/bim/module/pset/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/pset/operator.py
@@ -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):
diff --git a/src/blenderbim/blenderbim/bim/module/pset_template/__init__.py b/src/blenderbim/blenderbim/bim/module/pset_template/__init__.py
index 3bc180c204..766045f8af 100644
--- a/src/blenderbim/blenderbim/bim/module/pset_template/__init__.py
+++ b/src/blenderbim/blenderbim/bim/module/pset_template/__init__.py
@@ -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,
diff --git a/src/blenderbim/blenderbim/bim/module/pset_template/data.py b/src/blenderbim/blenderbim/bim/module/pset_template/data.py
index a48a315610..de44529cb5 100644
--- a/src/blenderbim/blenderbim/bim/module/pset_template/data.py
+++ b/src/blenderbim/blenderbim/bim/module/pset_template/data.py
@@ -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])
diff --git a/src/blenderbim/blenderbim/bim/module/pset_template/operator.py b/src/blenderbim/blenderbim/bim/module/pset_template/operator.py
index f2173a51b8..4d4534de80 100644
--- a/src/blenderbim/blenderbim/bim/module/pset_template/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/pset_template/operator.py
@@ -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
diff --git a/src/blenderbim/blenderbim/bim/module/pset_template/prop.py b/src/blenderbim/blenderbim/bim/module/pset_template/prop.py
index d2821ca01a..2dba9b0504 100644
--- a/src/blenderbim/blenderbim/bim/module/pset_template/prop.py
+++ b/src/blenderbim/blenderbim/bim/module/pset_template/prop.py
@@ -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)
diff --git a/src/blenderbim/blenderbim/bim/module/pset_template/ui.py b/src/blenderbim/blenderbim/bim/module/pset_template/ui.py
index 969a355ed3..2f90bd94dd 100644
--- a/src/blenderbim/blenderbim/bim/module/pset_template/ui.py
+++ b/src/blenderbim/blenderbim/bim/module/pset_template/ui.py
@@ -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)
diff --git a/src/blenderbim/blenderbim/bim/module/resource/__init__.py b/src/blenderbim/blenderbim/bim/module/resource/__init__.py
index 8512a370dd..8ab1410e3f 100644
--- a/src/blenderbim/blenderbim/bim/module/resource/__init__.py
+++ b/src/blenderbim/blenderbim/bim/module/resource/__init__.py
@@ -49,7 +49,6 @@ classes = (
operator.ExpandResource,
operator.GoToResource,
operator.ImportResources,
- operator.LoadResourceProperties,
operator.LoadResources,
operator.RemoveResource,
operator.RemoveResourceQuantity,
diff --git a/src/blenderbim/blenderbim/bim/module/resource/operator.py b/src/blenderbim/blenderbim/bim/module/resource/operator.py
index fbdd4374f7..9d64cdb0dd 100644
--- a/src/blenderbim/blenderbim/bim/module/resource/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/resource/operator.py
@@ -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())
diff --git a/src/blenderbim/blenderbim/bim/module/resource/prop.py b/src/blenderbim/blenderbim/bim/module/resource/prop.py
index 9ea20d446c..2f7e539eb9 100644
--- a/src/blenderbim/blenderbim/bim/module/resource/prop.py
+++ b/src/blenderbim/blenderbim/bim/module/resource/prop.py
@@ -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
diff --git a/src/blenderbim/blenderbim/bim/module/root/operator.py b/src/blenderbim/blenderbim/bim/module/root/operator.py
index 3e90b3385b..b112794595 100644
--- a/src/blenderbim/blenderbim/bim/module/root/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/root/operator.py
@@ -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()
diff --git a/src/blenderbim/blenderbim/bim/module/root/prop.py b/src/blenderbim/blenderbim/bim/module/root/prop.py
index 95456da780..cd0b47949f 100644
--- a/src/blenderbim/blenderbim/bim/module/root/prop.py
+++ b/src/blenderbim/blenderbim/bim/module/root/prop.py
@@ -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()
diff --git a/src/blenderbim/blenderbim/bim/module/search/__init__.py b/src/blenderbim/blenderbim/bim/module/search/__init__.py
index 3dd6435f99..da8076d64d 100644
--- a/src/blenderbim/blenderbim/bim/module/search/__init__.py
+++ b/src/blenderbim/blenderbim/bim/module/search/__init__.py
@@ -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,
diff --git a/src/blenderbim/blenderbim/bim/module/search/operator.py b/src/blenderbim/blenderbim/bim/module/search/operator.py
index c929938632..ee018ccef8 100644
--- a/src/blenderbim/blenderbim/bim/module/search/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/search/operator.py
@@ -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"
diff --git a/src/blenderbim/blenderbim/bim/module/search/prop.py b/src/blenderbim/blenderbim/bim/module/search/prop.py
index bdcc3d223c..53b5db1554 100644
--- a/src/blenderbim/blenderbim/bim/module/search/prop.py
+++ b/src/blenderbim/blenderbim/bim/module/search/prop.py
@@ -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):
diff --git a/src/blenderbim/blenderbim/bim/module/search/ui.py b/src/blenderbim/blenderbim/bim/module/search/ui.py
index 706f5952e9..a2a24c7b68 100644
--- a/src/blenderbim/blenderbim/bim/module/search/ui.py
+++ b/src/blenderbim/blenderbim/bim/module/search/ui.py
@@ -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):
diff --git a/src/blenderbim/blenderbim/bim/module/sequence/operator.py b/src/blenderbim/blenderbim/bim/module/sequence/operator.py
index ddcfc309e4..d285a49d33 100644
--- a/src/blenderbim/blenderbim/bim/module/sequence/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/sequence/operator.py
@@ -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),
)
diff --git a/src/blenderbim/blenderbim/bim/module/sequence/prop.py b/src/blenderbim/blenderbim/bim/module/sequence/prop.py
index 4ea2f667d8..add76f452e 100644
--- a/src/blenderbim/blenderbim/bim/module/sequence/prop.py
+++ b/src/blenderbim/blenderbim/bim/module/sequence/prop.py
@@ -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")
diff --git a/src/blenderbim/blenderbim/bim/module/sequence/ui.py b/src/blenderbim/blenderbim/bim/module/sequence/ui.py
index f28794e481..d0e8e15e03 100644
--- a/src/blenderbim/blenderbim/bim/module/sequence/ui.py
+++ b/src/blenderbim/blenderbim/bim/module/sequence/ui.py
@@ -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:
diff --git a/src/blenderbim/blenderbim/bim/module/spatial/operator.py b/src/blenderbim/blenderbim/bim/module/spatial/operator.py
index 451ee442ac..b9c3f00202 100644
--- a/src/blenderbim/blenderbim/bim/module/spatial/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/spatial/operator.py
@@ -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):
diff --git a/src/blenderbim/blenderbim/bim/module/spatial/prop.py b/src/blenderbim/blenderbim/bim/module/spatial/prop.py
index 35ecf3236d..b17b643af7 100644
--- a/src/blenderbim/blenderbim/bim/module/spatial/prop.py
+++ b/src/blenderbim/blenderbim/bim/module/spatial/prop.py
@@ -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")
diff --git a/src/blenderbim/blenderbim/bim/module/spatial/ui.py b/src/blenderbim/blenderbim/bim/module/spatial/ui.py
index 8f73b18a4f..28e65d2982 100644
--- a/src/blenderbim/blenderbim/bim/module/spatial/ui.py
+++ b/src/blenderbim/blenderbim/bim/module/spatial/ui.py
@@ -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):
diff --git a/src/blenderbim/blenderbim/bim/module/system/__init__.py b/src/blenderbim/blenderbim/bim/module/system/__init__.py
index a57b7fb5a8..ea95b8965e 100644
--- a/src/blenderbim/blenderbim/bim/module/system/__init__.py
+++ b/src/blenderbim/blenderbim/bim/module/system/__init__.py
@@ -17,7 +17,7 @@
# along with BlenderBIM Add-on. If not, see .
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)
diff --git a/src/blenderbim/blenderbim/bim/module/system/data.py b/src/blenderbim/blenderbim/bim/module/system/data.py
index 6c71e88dc9..6955fcdbbf 100644
--- a/src/blenderbim/blenderbim/bim/module/system/data.py
+++ b/src/blenderbim/blenderbim/bim/module/system/data.py
@@ -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]
diff --git a/src/blenderbim/blenderbim/bim/module/system/decorator.py b/src/blenderbim/blenderbim/bim/module/system/decorator.py
new file mode 100644
index 0000000000..e3e8518f79
--- /dev/null
+++ b/src/blenderbim/blenderbim/bim/module/system/decorator.py
@@ -0,0 +1,145 @@
+# BlenderBIM Add-on - OpenBIM Blender Add-on
+# Copyright (C) 2023 Dion Moult , @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 .
+
+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)
diff --git a/src/blenderbim/blenderbim/bim/module/system/operator.py b/src/blenderbim/blenderbim/bim/module/system/operator.py
index 71336ec7fb..4d40d9c36d 100644
--- a/src/blenderbim/blenderbim/bim/module/system/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/system/operator.py
@@ -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"}
diff --git a/src/blenderbim/blenderbim/bim/module/system/prop.py b/src/blenderbim/blenderbim/bim/module/system/prop.py
index 3d31414ffd..f1ffa3263c 100644
--- a/src/blenderbim/blenderbim/bim/module/system/prop.py
+++ b/src/blenderbim/blenderbim/bim/module/system/prop.py
@@ -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
+ )
diff --git a/src/blenderbim/blenderbim/bim/module/system/ui.py b/src/blenderbim/blenderbim/bim/module/system/ui.py
index 5b60c151e7..6e20514bac 100644
--- a/src/blenderbim/blenderbim/bim/module/system/ui.py
+++ b/src/blenderbim/blenderbim/bim/module/system/ui.py
@@ -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):
diff --git a/src/blenderbim/blenderbim/bim/module/tester/operator.py b/src/blenderbim/blenderbim/bim/module/tester/operator.py
index 4b88c3ae4b..57ed89429d 100644
--- a/src/blenderbim/blenderbim/bim/module/tester/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/tester/operator.py
@@ -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"}
diff --git a/src/blenderbim/blenderbim/bim/module/tester/prop.py b/src/blenderbim/blenderbim/bim/module/tester/prop.py
index b68600f053..7496d4ba44 100644
--- a/src/blenderbim/blenderbim/bim/module/tester/prop.py
+++ b/src/blenderbim/blenderbim/bim/module/tester/prop.py
@@ -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):
diff --git a/src/blenderbim/blenderbim/bim/module/tester/ui.py b/src/blenderbim/blenderbim/bim/module/tester/ui.py
index 2af3f7af98..31c45bc888 100644
--- a/src/blenderbim/blenderbim/bim/module/tester/ui.py
+++ b/src/blenderbim/blenderbim/bim/module/tester/ui.py
@@ -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
diff --git a/src/blenderbim/blenderbim/bim/module/type/operator.py b/src/blenderbim/blenderbim/bim/module/type/operator.py
index ebb624829d..223ca0ce04 100644
--- a/src/blenderbim/blenderbim/bim/module/type/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/type/operator.py
@@ -530,4 +530,5 @@ class PurgeUnusedTypes(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
- core.purge_unused_types(tool.Ifc, tool.Type)
+ purged_types = core.purge_unused_types(tool.Ifc, tool.Type)
+ self.report({"INFO"}, f"{purged_types} types were purged.")
diff --git a/src/blenderbim/blenderbim/bim/operator.py b/src/blenderbim/blenderbim/bim/operator.py
index 08f591be6e..a8a675ef91 100644
--- a/src/blenderbim/blenderbim/bim/operator.py
+++ b/src/blenderbim/blenderbim/bim/operator.py
@@ -42,13 +42,15 @@ from math import radians
class SetTab(bpy.types.Operator):
bl_idname = "bim.set_tab"
- bl_label = "Set Current Tab"
- bl_options = {"REGISTER", "UNDO"}
+ # NOTE: bl_label is set to empty string intentionally
+ # to avoid showing the operator's name in the tooltips, see #3704
+ bl_label = ""
+ bl_options = {"REGISTER", "UNDO", "INTERNAL"}
tab: bpy.props.StringProperty()
@classmethod
- def description(cls, context, properties):
- return next((t[1] for t in blenderbim.bim.prop.get_tab(None, context) if t[0] == properties.tab), "")
+ def description(cls, context, operator):
+ return next((t[1] for t in blenderbim.bim.prop.get_tab(None, context) if t[0] == operator.tab), "")
def execute(self, context):
if context.area.spaces.active.search_filter:
@@ -195,21 +197,52 @@ class FileAssociate(bpy.types.Operator):
@classmethod
def poll(cls, context):
- if platform.system() == "Linux":
+ if platform.system() in ("Linux", "Windows"):
return True
- cls.poll_message_set("Option available only on Linux.")
- # TODO Windows and Darwin
+ cls.poll_message_set("Option available only on Windows & Linux.")
+ # TODO Darwin
# https://stackoverflow.com/questions/1082889/how-to-change-filetype-association-in-the-registry
return False
+ def draw(self, context):
+ # NOTE: really weird thing on windows that typing this command in cmd works
+ # when even if you create .bat with the command below and run it as administrator it won't
+ # Haven't found a workaround yet to automate process completely.
+ command = "ASSOC .IFC=BLENDERBIM"
+ self.layout.label(text="On the next step to create file association ")
+ self.layout.label(text="the system console will be opened ")
+ self.layout.label(text=f"and you will be asked to type command")
+ self.layout.label(text=f'"{command}"')
+ self.layout.label(text="to create an association.")
+
+ def invoke(self, context, event):
+ if platform.system() == "Windows":
+ return context.window_manager.invoke_props_dialog(self)
+ else:
+ return self.execute(context)
+
def execute(self, context):
src_dir = os.path.join(os.path.dirname(__file__), "../libs/desktop")
binary_path = bpy.app.binary_path
if platform.system() == "Linux":
destdir = os.path.join(os.environ["HOME"], ".local")
self.install_desktop_linux(src_dir=src_dir, destdir=destdir, binary_path=binary_path)
+ elif platform.system() == "Windows":
+ self.install_desktop_windows(src_dir, binary_path)
+ self.report({"INFO"}, "Associations established.")
return {"FINISHED"}
+ def install_desktop_windows(self, src_dir, binary_path):
+ # very important to clear this regitstry key before creating new association
+ # tried to do the regitsry change from powershell/cmd - but even admin rights are not enough
+ # this is why we're using .reg
+ reg_change_path = os.path.join(src_dir, "windows_bbim_association.reg")
+ subprocess.run(["cmd", "/c", reg_change_path])
+
+ ps_script_path = os.path.join(src_dir, "windows_bbim_association.ps1")
+ # NOTE: call powershell with RunAs to get admin rights from user
+ subprocess.run(["powershell", "-file", ps_script_path, binary_path], shell=True)
+
def install_desktop_linux(self, src_dir=None, destdir="/tmp", binary_path="/usr/bin/blender"):
"""Creates linux file assocations and launcher icon"""
@@ -270,17 +303,29 @@ class FileUnassociate(bpy.types.Operator):
@classmethod
def poll(cls, context):
- if platform.system() == "Linux":
+ if platform.system() in ("Linux", "Windows"):
return True
- cls.poll_message_set("Option available only on Linux.")
+ cls.poll_message_set("Option available only on Windows & Linux.")
return False
def execute(self, context):
if platform.system() == "Linux":
destdir = os.path.join(os.environ["HOME"], ".local")
self.uninstall_desktop_linux(destdir=destdir)
+ elif platform.system() == "Windows":
+ self.uninstall_desktop_windows()
return {"FINISHED"}
+ def uninstall_desktop_windows(self):
+ # NOTE: call powershell with RunAs to get admin rights from user
+ cmd = [
+ "powershell",
+ "-Command",
+ "Start-Process -Verb RunAs -Wait cmd -ArgumentList '/c reg delete HKCR\\BLENDERBIM /f'",
+ ]
+ subprocess.run(cmd, check=True)
+ self.report({"INFO"}, "Association removed.")
+
def uninstall_desktop_linux(self, destdir="/tmp"):
"""Removes linux file assocations and launcher icon"""
for rel_path in (
diff --git a/src/blenderbim/blenderbim/core/cost.py b/src/blenderbim/blenderbim/core/cost.py
index d8f5d9166b..19bf23f02a 100644
--- a/src/blenderbim/blenderbim/core/cost.py
+++ b/src/blenderbim/blenderbim/core/cost.py
@@ -1,4 +1,4 @@
-def add_cost_schedule(ifc, name, predefined_type,object_type):
+def add_cost_schedule(ifc, name, predefined_type, object_type):
ifc.run("cost.add_cost_schedule", name=name, predefined_type=predefined_type, object_type=object_type)
@@ -112,70 +112,89 @@ def assign_cost_item_quantity(ifc, cost, cost_item, related_object_type, prop_na
ifc.run("cost.assign_cost_item_quantity", cost_item=cost_item, products=products, prop_name=prop_name)
cost.load_cost_item_quantity_assignments(cost_item, related_object_type=related_object_type)
+
def load_cost_item_quantities(cost):
cost.load_cost_item_quantities()
+
def load_cost_item_element_quantities(cost):
cost_item = cost.get_highlighted_cost_item()
cost.load_cost_item_quantity_assignments(cost_item, related_object_type="PRODUCT")
+
def load_cost_item_task_quantities(cost):
cost_item = cost.get_highlighted_cost_item()
cost.load_cost_item_quantity_assignments(cost_item, related_object_type="PROCESS")
+
def load_cost_item_resource_quantities(cost):
cost_item = cost.get_highlighted_cost_item()
cost.load_cost_item_quantity_assignments(cost_item, related_object_type="RESOURCE")
+
def assign_cost_value(ifc, cost_item, cost_rate):
ifc.run("cost.assign_cost_value", cost_item=cost_item, cost_rate=cost_rate)
+
def load_schedule_of_rates(cost, schedule_of_rates):
cost.load_schedule_of_rates_tree(schedule_of_rates)
+
def unassign_cost_item_quantity(ifc, cost, cost_item, products):
ifc.run("cost.unassign_cost_item_quantity", cost_item=cost_item, products=products)
cost.load_cost_item_quantities()
+
def enable_editing_cost_item_quantities(cost, cost_item):
cost.enable_editing_cost_item_quantities(cost_item)
+
def enable_editing_cost_item_values(cost, cost_item):
cost.enable_editing_cost_item_values(cost_item)
+
def add_cost_item_quantity(ifc, cost_item, ifc_class):
ifc.run("cost.add_cost_item_quantity", cost_item=cost_item, ifc_class=ifc_class)
+
def remove_cost_item_quantity(ifc, cost_item, physical_quantity):
ifc.run("cost.remove_cost_item_quantity", cost_item=cost_item, physical_quantity=physical_quantity)
-
+
+
def enable_editing_cost_item_quantity(cost, physical_quantity):
cost.load_cost_item_quantity_attributes(physical_quantity)
cost.enable_editing_cost_item_quantity(physical_quantity)
+
def disable_editing_cost_item_quantity(cost):
cost.disable_editing_cost_item_quantity()
+
def edit_cost_item_quantity(ifc, cost, physical_quantity):
attributes = cost.get_cost_item_quantity_attributes()
ifc.run("cost.edit_cost_item_quantity", physical_quantity=physical_quantity, attributes=attributes)
cost.disable_editing_cost_item_quantity()
cost.load_cost_item_quantities()
+
def add_cost_value(ifc, cost, parent, cost_type, cost_category):
value = ifc.run("cost.add_cost_value", parent=parent)
ifc.run(
"cost.edit_cost_value",
cost_value=value,
- attributes=cost.get_attributes_for_cost_value(cost_type, cost_category))
+ attributes=cost.get_attributes_for_cost_value(cost_type, cost_category),
+ )
+
def remove_cost_value(ifc, parent, cost_value):
ifc.run("cost.remove_cost_value", parent=parent, cost_value=cost_value)
+
def enable_editing_cost_item_value(cost, cost_value):
cost.load_cost_item_value_attributes(cost_value)
cost.enable_editing_cost_item_value(cost_value)
+
def disable_editing_cost_item_value(cost):
cost.disable_editing_cost_item_value()
@@ -195,7 +214,7 @@ def edit_cost_value(ifc, cost, cost_value):
attributes = cost.get_cost_value_attributes()
ifc.run("cost.edit_cost_value", cost_value=cost_value, attributes=attributes)
cost.disable_editing_cost_item_value()
- #cost.load_cost_item_values(cost.get_highlighted_cost_item())
+ # cost.load_cost_item_values(cost.get_highlighted_cost_item())
def copy_cost_item_values(ifc, cost, source, destination):
@@ -275,11 +294,12 @@ def change_parent_cost_item(ifc, cost, new_parent):
cost_item = cost.get_active_cost_item()
if cost_item and cost.is_root_cost_item(cost_item):
return "Cannot change root cost item"
- if cost_item :
+ if cost_item:
ifc.run("nest.change_nest", item=cost_item, new_parent=new_parent)
cost.disable_editing_cost_item_parent()
cost.load_cost_schedule_tree()
+
def copy_cost_item(ifc, cost):
cost_item = cost.get_highlighted_cost_item()
if cost_item:
@@ -287,9 +307,10 @@ def copy_cost_item(ifc, cost):
cost.disable_editing_cost_item_parent()
cost.load_cost_schedule_tree()
+
def add_currency(ifc, cost):
unit = ifc.run("unit.add_monetary_unit")
attributes = cost.get_currency_attributes()
ifc.run("unit.edit_monetary_unit", unit=unit, attributes=attributes)
ifc.run("unit.assign_unit", units=[unit])
- return unit
\ No newline at end of file
+ return unit
diff --git a/src/blenderbim/blenderbim/core/covering.py b/src/blenderbim/blenderbim/core/covering.py
new file mode 100644
index 0000000000..61aadfcd7a
--- /dev/null
+++ b/src/blenderbim/blenderbim/core/covering.py
@@ -0,0 +1,42 @@
+# BlenderBIM Add-on - OpenBIM Blender Add-on
+# Copyright (C) 2021 Dion Moult
+#
+# 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 .
+
+
+def add_instance_flooring_coverings_from_walls(ifc, spatial, collector, geometry):
+ z = spatial.get_active_obj_z()
+ union = spatial.get_union_shape_from_selected_objects()
+ for i, linear_ring in enumerate(union.interiors):
+ poly = spatial.get_buffered_poly_from_linear_ring(linear_ring)
+ bm = spatial.get_bmesh_from_polygon(poly, h=0)
+
+ name = "Covering" + str(i)
+ obj = spatial.get_named_obj_from_bmesh(name, bmesh = bm)
+
+ spatial.set_obj_origin_to_bboxcenter(obj)
+ spatial.traslate_obj_to_z_location(obj, z)
+ spatial.link_obj_to_active_collection(obj)
+
+ points = spatial.get_2d_vertices_from_obj(obj)
+
+ spatial.assign_type_to_obj(obj)
+ spatial.assign_container_to_obj(obj)
+
+ spatial.assign_swept_area_outer_curve_from_2d_vertices(obj, vertices = points)
+ body = spatial.get_body_representation(obj)
+ spatial.regen_obj_representation(ifc, geometry, obj, body)
+
diff --git a/src/blenderbim/blenderbim/core/debug.py b/src/blenderbim/blenderbim/core/debug.py
index 65e7ac0607..fd6356aa0d 100644
--- a/src/blenderbim/blenderbim/core/debug.py
+++ b/src/blenderbim/blenderbim/core/debug.py
@@ -23,3 +23,11 @@ def parse_express(debug, filename):
def purge_hdf5_cache(debug):
debug.purge_hdf5_cache()
+
+
+def purge_unused_elements(ifc, debug, ifc_class):
+ ifc_file = ifc.get()
+ unused_elements = [i for i in ifc_file.by_type(ifc_class) if ifc_file.get_total_inverses(i) == 0]
+ unused_elements_amount = len(unused_elements)
+ debug.remove_unused_elements(unused_elements)
+ return unused_elements_amount
diff --git a/src/blenderbim/blenderbim/core/geometry.py b/src/blenderbim/blenderbim/core/geometry.py
index c26c54d822..b055a65d6c 100644
--- a/src/blenderbim/blenderbim/core/geometry.py
+++ b/src/blenderbim/blenderbim/core/geometry.py
@@ -158,9 +158,12 @@ def remove_representation(ifc, geometry, obj=None, representation=None):
def purge_unused_representations(ifc, geometry):
+ purged_representations = 0
for representation in geometry.get_model_representations():
if ifc.get().get_total_inverses(representation) == 0:
ifc.run("geometry.remove_representation", representation=representation)
+ purged_representations += 1
+ return purged_representations
def select_connection(geometry, connection=None):
diff --git a/src/blenderbim/blenderbim/core/misc.py b/src/blenderbim/blenderbim/core/misc.py
index 358f96d5ec..5aff2100c0 100644
--- a/src/blenderbim/blenderbim/core/misc.py
+++ b/src/blenderbim/blenderbim/core/misc.py
@@ -28,11 +28,3 @@ def resize_to_storey(misc, obj=None, total_storeys=None):
misc.move_object_to_elevation(obj, misc.get_storey_elevation_in_si(storey))
misc.scale_object_to_height(obj, height)
misc.mark_object_as_edited(obj)
-
-
-def split_along_edge(misc, cutter=None, objs=None):
- new_objs = misc.split_objects_with_cutter(objs, cutter)
- for obj in new_objs:
- misc.run_root_copy_class(obj=obj)
- for obj in objs:
- misc.mark_object_as_edited(obj)
diff --git a/src/blenderbim/blenderbim/core/profile.py b/src/blenderbim/blenderbim/core/profile.py
index 7f2e53b061..58029f1dc4 100644
--- a/src/blenderbim/blenderbim/core/profile.py
+++ b/src/blenderbim/blenderbim/core/profile.py
@@ -18,7 +18,10 @@
def purge_unused_profiles(ifc, profile):
+ purged_profiles = 0
for element_profile in profile.get_model_profiles():
if ifc.get().get_total_inverses(element_profile) > 0:
continue
ifc.run("profile.remove_profile", profile=element_profile)
+ purged_profiles += 1
+ return purged_profiles
diff --git a/src/blenderbim/blenderbim/core/pset.py b/src/blenderbim/blenderbim/core/pset.py
index 36200040f7..023b7ba190 100644
--- a/src/blenderbim/blenderbim/core/pset.py
+++ b/src/blenderbim/blenderbim/core/pset.py
@@ -43,3 +43,4 @@ def add_pset(ifc, pset, blender, obj_name, obj_type):
ifc_pset = pset.get_element_pset(element, pset_name)
if not ifc_pset:
ifc.run("pset.add_pset", product=element, name=pset_name)
+ pset.enable_pset_editing(pset_id=0, pset_name=pset_name, pset_type="PSET",obj=obj_name, obj_type=obj_type)
\ No newline at end of file
diff --git a/src/blenderbim/blenderbim/core/resource.py b/src/blenderbim/blenderbim/core/resource.py
index c026139446..dd3875a04f 100644
--- a/src/blenderbim/blenderbim/core/resource.py
+++ b/src/blenderbim/blenderbim/core/resource.py
@@ -21,15 +21,11 @@
def load_resources(resource):
resource.load_resources()
- resource.load_resource_properties()
+
def add_resource(tool_ifc, resource_tool, ifc_class, parent_resource=None):
tool_ifc.run("resource.add_resource", ifc_class=ifc_class, parent_resource=parent_resource)
- load_resources(resource_tool)
-
-
-def load_resource_properties(resource_tool, resource=None):
- resource_tool.load_resource_properties()
+ resource_tool.load_resources()
def disable_editing_resource(resource_tool):
@@ -54,7 +50,7 @@ def edit_resource(ifc, resource_tool, resource):
def remove_resource(ifc, resource_tool, resource=None):
ifc.run("resource.remove_resource", resource=resource)
- load_resources(resource_tool)
+ resource_tool.load_resources()
def enable_editing_resource_time(ifc_tool, resource_tool, resource):
@@ -82,7 +78,7 @@ def calculate_resource_work(ifc, resource_tool, resource):
nested_resources = resource_tool.get_nested_resources(resource)
for nested_resource in nested_resources or []:
ifc.run("resource.calculate_resource_work", resource=nested_resource)
- load_resources(resource_tool)
+ resource_tool.load_resources()
def enable_editing_resource_costs(resource_tool, resource):
@@ -143,17 +139,17 @@ def edit_resource_quantity(resource_tool, ifc, physical_quantity=None):
def import_resources(resource_tool, file_path):
resource_tool.import_resources(file_path)
- load_resources(resource_tool)
+ resource_tool.load_resources()
def expand_resource(resource_tool, resource):
resource_tool.expand_resource(resource)
- load_resources(resource_tool)
+ resource_tool.load_resources()
def contract_resource(resource_tool, resource):
resource_tool.contract_resource(resource)
- load_resources(resource_tool)
+ resource_tool.load_resources()
def assign_resource(ifc, spatial, resource=None, products=None):
@@ -213,9 +209,11 @@ def remove_usage_constraint(ifc, resource_tool, resource, reference_path):
ifc.run("constraint.unassign_constraint", product=resource, constraint=constraint)
ifc.run("constraint.remove_constraint", constraint=constraint)
+
def go_to_resource(resource_tool, resource):
resource_tool.go_to_resource(resource)
+
def calculate_resource_usage(ifc, resource_tool, resource):
ifc.run("resource.calculate_resource_usage", resource=resource)
- load_resources(resource_tool)
\ No newline at end of file
+ resource_tool.load_resources()
diff --git a/src/blenderbim/blenderbim/core/sequence.py b/src/blenderbim/blenderbim/core/sequence.py
index 2321796783..2fa2debf5d 100644
--- a/src/blenderbim/blenderbim/core/sequence.py
+++ b/src/blenderbim/blenderbim/core/sequence.py
@@ -186,14 +186,14 @@ def enable_editing_task_time(ifc, sequence, task=None):
sequence.enable_editing_task_time(task)
-def edit_task_time(ifc, sequence, task_time=None):
+def edit_task_time(ifc, sequence, resource, task_time=None):
attributes = sequence.get_task_time_attributes()
# TODO: nasty loop goes on when calendar props are messed up
ifc.run("sequence.edit_task_time", task_time=task_time, attributes=attributes)
task = sequence.get_active_task()
sequence.load_task_properties(task=task)
sequence.disable_editing_task_time()
- sequence.load_resources()
+ resource.load_resource_properties()
def assign_predecessor(ifc, sequence, task=None):
@@ -248,8 +248,8 @@ def unassign_input_products(ifc, sequence, spatial, task=None, products=None):
sequence.load_task_inputs(inputs)
-def assign_resource(ifc, sequence, task=None):
- resource = sequence.get_selected_resource()
+def assign_resource(ifc, sequence, resource_tool, task=None):
+ resource = resource_tool.get_highlighted_resource()
sub_resource = ifc.run(
"resource.add_resource",
parent_resource=resource,
@@ -257,17 +257,15 @@ def assign_resource(ifc, sequence, task=None):
name="{}/{}".format(resource.Name or "Unnamed", task.Name or ""),
)
ifc.run("sequence.assign_process", relating_process=task, related_object=sub_resource)
- resources = sequence.get_task_resources(task)
- sequence.load_task_resources(resources)
- sequence.load_resources()
+ sequence.load_task_resources(task)
+ resource_tool.load_resources()
-def unassign_resource(ifc, sequence, task=None, resource=None):
+def unassign_resource(ifc, sequence, resource_tool, task=None, resource=None):
ifc.run("sequence.unassign_process", relating_process=task, related_object=resource)
ifc.run("resource.remove_resource", resource=resource)
- resources = sequence.get_task_resources(task)
- sequence.load_task_resources(resources)
- sequence.load_resources()
+ sequence.load_task_resources(task)
+ resource_tool.load_resources()
def remove_work_calendar(ifc, work_calendar=None):
@@ -515,6 +513,7 @@ def visualise_work_schedule_date_range(sequence, work_schedule=None):
product_frames = sequence.get_animation_product_frames(work_schedule, settings)
if not sequence.has_animation_colors():
sequence.load_default_animation_color_scheme()
+ load_animation_color_scheme(sequence, scheme=sequence.get_animation_color_scheme())
sequence.animate_objects(settings, product_frames, "date_range")
sequence.add_text_animation_handler(settings)
add_task_bars(sequence)
diff --git a/src/blenderbim/blenderbim/core/spatial.py b/src/blenderbim/blenderbim/core/spatial.py
index fb75dd2671..7f2beb3aba 100644
--- a/src/blenderbim/blenderbim/core/spatial.py
+++ b/src/blenderbim/blenderbim/core/spatial.py
@@ -17,6 +17,7 @@
# along with BlenderBIM Add-on. If not, see .
import blenderbim.core
+import bpy
def reference_structure(ifc, spatial, structure=None, element=None):
@@ -120,3 +121,63 @@ def select_decomposed_elements(spatial):
container = spatial.get_active_container()
if container:
spatial.select_products(spatial.get_decomposed_elements(container))
+
+#HERE STARTS SPATIAL TOOL
+
+def generate_spaces_from_walls(ifc, spatial, collector):
+ container, active_obj = spatial.get_container_and_active_obj()
+
+ if not active_obj:
+ self.report({"ERROR"}, "No active object. Please select a wall")
+ return
+
+ element = 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.")
+
+ 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
+
+ union = spatial.get_union_shape_from_selected_objects(selected_objects)
+
+ for i, linear_ring in enumerate(union.interiors):
+ poly = spatial.get_buffered_poly_from_linear_ring(linear_ring)
+
+ bm = spatial.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
+
+ spatial.set_obj_origin_to_bboxcenter(obj)
+
+ if z != 0:
+ obj.location = obj.location + Vector((0, 0, z))
+
+ bpy.context.view_layer.active_layer_collection.collection.objects.link(obj)
+ bpy.ops.bim.assign_class(obj=obj.name, ifc_class="IfcSpace")
+ container_obj = ifc.get_object(container)
+ blenderbim.core.spatial.assign_container(
+ ifc, collector, spatial, structure_obj=container_obj, element_obj=obj
+ )
+
+def toggle_space_visibility(ifc, spatial):
+ model = ifc.get()
+ spaces = model.by_type("IfcSpace")
+ if not spaces:
+ return
+ spatial.toggle_spaces_visibility_wired_and_textured(spaces)
+
diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py
index f0f168a7ba..0557e0144d 100644
--- a/src/blenderbim/blenderbim/core/tool.py
+++ b/src/blenderbim/blenderbim/core/tool.py
@@ -726,7 +726,6 @@ class Sequence:
def get_recurrence_pattern_attributes(cls, recurrence_pattern): pass
def get_recurrence_pattern_times(cls): pass
def get_rel_sequence_attributes(cls): pass
- def get_selected_resource(cls): pass
def get_start_date(cls): pass
def get_task_attribute_value(cls, attribute_name): pass
def get_task_attributes(cls): pass
@@ -758,7 +757,7 @@ class Sequence:
def load_task_inputs(cls, inputs): pass
def load_task_outputs(cls, outputs): pass
def load_task_properties(cls, task): pass
- def load_task_resources(cls,resources): pass
+ def load_task_resources(cls, task): pass
def load_task_time_attributes(cls, task_time): pass
def load_task_tree(cls, work_schedule): pass
def load_work_calendar_attributes(cls, work_calendar): pass
@@ -810,6 +809,28 @@ class Spatial:
def set_active_object(cls, obj): pass
def set_relative_object_matrix(cls, target_obj, relative_to_obj, matrix): pass
def show_scene_objects(cls): pass
+ #HERE STARTS SPATIAL TOOL
+# def get_container_and_active_obj(cls): pass
+ def get_union_shape_from_selected_objects(cls, selected_objects): pass
+ def get_boundary_elements(cls, selected_objects): pass
+ def get_polygons(cls, boundary_elements): pass
+ def get_obj_base_points(cls, obj): pass
+ def get_converted_tolerance(cls, tolerance): pass
+ def get_purged_inner_holes_poly(cls, union_geom, min_area): pass
+ def get_poly_valid_interior_list(cls, poly, min_area, interiors_list): pass
+ def get_buffered_poly_from_linear_ring(cls, linear_ring): pass
+ def get_bmesh_from_polygon(cls, poly, h): pass
+ def get_named_obj_from_bmesh(cls, name, bmesh): pass
+ def set_obj_origin_to_bboxcenter(cls, obj): pass
+ def get_active_obj_z(cls, obj): pass
+ def traslate_obj_to_z_location(cls, obj): pass
+ def link_obj_to_active_collection(cls, obj): pass
+ def get_2d_vertices_from_obj(cls, obj): pass
+ def assign_swept_area_outer_curve_from_2d_vertices(cls, obj, vertices): pass
+ def get_body_representation(cls, obj): pass
+ def assign_type_to_obj(cls, obj): pass
+ def regen_obj_representation(cls, ifc, geometry, obj, body): pass
+ def toggle_spaces_visibility_wired_and_textured(cls, spaces): pass
@interface
diff --git a/src/blenderbim/blenderbim/core/type.py b/src/blenderbim/blenderbim/core/type.py
index f4660895f6..ec36ad5ffe 100644
--- a/src/blenderbim/blenderbim/core/type.py
+++ b/src/blenderbim/blenderbim/core/type.py
@@ -32,10 +32,13 @@ def assign_type(ifc, type_tool, element=None, type=None):
def purge_unused_types(ifc, type):
+ purged_types = 0
for element_type in type.get_model_types():
if not type.get_type_occurrences(element_type):
obj = ifc.get_object(element_type)
ifc.run("root.remove_product", product=element_type)
+ purged_types += 1
if obj:
ifc.unlink(obj=obj)
type.remove_object(obj)
+ return purged_types
diff --git a/src/blenderbim/blenderbim/libs/desktop/windows_bbim_association.ps1 b/src/blenderbim/blenderbim/libs/desktop/windows_bbim_association.ps1
new file mode 100644
index 0000000000..11926a170a
--- /dev/null
+++ b/src/blenderbim/blenderbim/libs/desktop/windows_bbim_association.ps1
@@ -0,0 +1,16 @@
+param(
+ [string]$BlenderPath = "BLENDER_EXE"
+)
+
+Start-Process cmd -ArgumentList `
+ "/k ", `
+ "ASSOC .IFC=", `
+ "&", `
+ "FTYPE BLENDERBIM=""$BlenderPath"" --python-expr ""import bpy; bpy.ops.bim.load_project(filepath=r'%1')""",
+ "&", `
+ "echo. & echo. & echo To create an association between .IFC files and BlenderBIM", `
+ "&", `
+ "echo type the command below & echo.", `
+ "&", `
+ "echo ASSOC .IFC=BLENDERBIM & echo." `
+-Verb RunAs
\ No newline at end of file
diff --git a/src/blenderbim/blenderbim/tool/blender.py b/src/blenderbim/blenderbim/tool/blender.py
index 0ce77b6d1a..d544a67787 100644
--- a/src/blenderbim/blenderbim/tool/blender.py
+++ b/src/blenderbim/blenderbim/tool/blender.py
@@ -167,7 +167,9 @@ class Blender:
when in real life you can have a couple of those but should work for the most cases.
"""
area = next(area for area in bpy.context.screen.areas if area.type == "VIEW_3D")
- context_override = {"area": area}
+ region = next(region for region in area.regions if region.type == "WINDOW")
+ space = next(space for space in area.spaces if space.type == "VIEW_3D")
+ context_override = {"area": area, "region": region, "space_data": space}
return context_override
@classmethod
@@ -387,6 +389,11 @@ class Blender:
getattr(data_to, data_block_type).append(name)
return {"data_block": getattr(data_to, data_block_type)[0], "msg": ""}
+ @classmethod
+ def remove_data_block(cls, data_block):
+ collection_name = repr(data_block).split(".", 2)[-1].split("[", 1)[0]
+ getattr(bpy.data, collection_name).remove(data_block)
+
## BMESH UTILS ##
@classmethod
def apply_bmesh(cls, mesh, bm, obj=None):
@@ -477,6 +484,13 @@ class Blender:
if obj:
return obj
+ @classmethod
+ def lock_transform(cls, obj, lock_state=True):
+ for prop in ("lock_location", "lock_rotation", "lock_scale"):
+ attr = getattr(obj, prop)
+ for axis_idx in range(3):
+ attr[axis_idx] = lock_state
+
class Modifier:
@classmethod
def is_eligible_for_railing_modifier(cls, obj):
@@ -553,10 +567,7 @@ class Blender:
modifier_data = list(cls.get_modifiers_data(parent_element))[item]
children = cls.get_children_objects(modifier_data)
for child_obj in children:
- for prop in ("lock_location", "lock_rotation", "lock_scale"):
- attr = getattr(child_obj, prop)
- for axis_idx in range(3):
- attr[axis_idx] = lock_state
+ Blender.lock_transform(child_obj, lock_state)
@classmethod
def remove_constraints(cls, parent_element):
diff --git a/src/blenderbim/blenderbim/tool/cad.py b/src/blenderbim/blenderbim/tool/cad.py
index 3443e6cca7..41a66db0c2 100644
--- a/src/blenderbim/blenderbim/tool/cad.py
+++ b/src/blenderbim/blenderbim/tool/cad.py
@@ -35,6 +35,7 @@ import math
import bmesh
import mathutils.geometry
from mathutils import Vector, Matrix, geometry
+import itertools
VTX_PRECISION = 1.0e-5
@@ -74,6 +75,8 @@ class Cad:
"""
> takes 2 edges, each as a tuple of two vectors
< returns the potentially signed angle as degrees or radians
+
+ NOTE: `signed` expects both edges to be 2D (just as `Vector.angle_signed`)
"""
if signed:
a = (edge1[1] - edge1[0]).angle_signed(edge2[1] - edge2[0])
@@ -97,8 +100,8 @@ class Cad:
return (x + tolerance) > value > (x - tolerance)
@classmethod
- def are_vectors_equal(cls, v1: Vector, v2: Vector):
- return cls.is_x((v2 - v1).length, 0)
+ def are_vectors_equal(cls, v1: Vector, v2: Vector, tolerance: float = None):
+ return cls.is_x((v2 - v1).length, 0, tolerance)
@classmethod
def intersect_edges(cls, edge1, edge2):
@@ -189,6 +192,18 @@ class Cad:
distance_test = (v1 - pt).length >= (v2 - pt).length
return v1 if distance_test else v2
+ @classmethod
+ def closest_and_furthest_vectors(cls, pt, e):
+ """
+ > pt: vector
+ > e: 2 vector tuple
+ < returns the two vectors closest to and furthest from pt.
+ """
+ if isinstance(e, tuple) and all([isinstance(co, Vector) for co in e]):
+ closest = cls.closest_vector(pt, e)
+ furthest = e[1] if closest == e[0] else e[0]
+ return closest, furthest
+
@classmethod
def coords_tuple_from_edge_idx(cls, bm, idx):
"""bm is a bmesh representation"""
@@ -248,23 +263,33 @@ class Cad:
return cls.are_edges_parallel((edge2[0], edge1[0]), edge2)
@classmethod
- def closest_points(cls, edge1, edge2) -> bool:
+ def closest_points(cls, edge1, edge2):
+ """
+ closest end points between `edge1` and `edge2`
+
+ ensures returned vectors are the exact objects
+ that were passed to the method with `edge1` and `edge2`
+
+ < returns two tuples - two closest points and two other points
+
+ first point of each tuple belongs to `edge1` and second to `edge2`
"""
- closest end points between `edge1` and `edge2` assuming `edge1` and `edge2` are collinear.
+ distance_squared = None
+ closest_points = None
+ for p1 in edge1:
+ for p2 in edge2:
+ cur_line = p2 - p1
+ cur_distance_squared = cur_line.dot(cur_line)
+ if distance_squared is None or cur_distance_squared < distance_squared:
+ closest_points = (p1, p2)
+ distance_squared = cur_distance_squared
- < returns two points, first one belongs to `edge1` and second to `edge2`
-
- """
- direction = (edge1[1] - edge1[0]).normalized()
-
- # Project points onto the line to get scalar values along the direction
- points_values = [(p, p.dot(direction)) for p in (edge1 + edge2)]
- sorted_points = sorted(points_values, key=lambda el: el[1])
-
- edge1_point = next((p for p, v in sorted_points[1:3] if p in edge1), None)
- edge2_point = next((p for p, v in sorted_points[1:3] if p in edge2), None)
- return edge1_point, edge2_point
+ other_points = (
+ edge1[0] if closest_points[0] == edge1[1] else edge1[1],
+ edge2[0] if closest_points[1] == edge2[1] else edge2[1],
+ )
+ return closest_points, other_points
@classmethod
def find_intersecting_edges(cls, bm, pt, idx1, idx2):
@@ -489,3 +514,19 @@ class Cad:
def is_counter_clockwise_order(cls, A, B, C):
"""whether A-B-C located in counter-clockwise order in 2d space"""
return (C.y - A.y) * (B.x - A.x) > (B.y - A.y) * (C.x - A.x)
+
+ @classmethod
+ def sign(cls, value):
+ """
+ returns:
+ 0 if cls.is_x(value, 0)) \n
+ 1 if value > 0 \n
+ -1 if value < 0
+ """
+ if cls.is_x(value, 0):
+ return 0
+ return 1 if value > 0 else -1
+
+ @classmethod
+ def get_basis_vector(cls, object, axis_i):
+ return object.matrix_world.col[axis_i].normalized().to_3d()
diff --git a/src/blenderbim/blenderbim/tool/cost.py b/src/blenderbim/blenderbim/tool/cost.py
index 698625bc33..f51d157862 100644
--- a/src/blenderbim/blenderbim/tool/cost.py
+++ b/src/blenderbim/blenderbim/tool/cost.py
@@ -234,7 +234,6 @@ class Cost(blenderbim.core.tool.Cost):
@classmethod
def get_products(cls, related_object_type=None):
- props = bpy.context.scene.BIMCostProperties
if related_object_type == "PRODUCT":
products = tool.Spatial.get_selected_products()
elif related_object_type == "PROCESS":
diff --git a/src/blenderbim/blenderbim/tool/covering.py b/src/blenderbim/blenderbim/tool/covering.py
new file mode 100644
index 0000000000..cfbae2f807
--- /dev/null
+++ b/src/blenderbim/blenderbim/tool/covering.py
@@ -0,0 +1,49 @@
+# BlenderBIM Add-on - OpenBIM Blender Add-on
+# Copyright (C) 2021 Dion Moult
+#
+# 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 .
+
+import bpy
+import bmesh
+import shapely
+import ifcopenshell
+import blenderbim.core.tool
+import blenderbim.core.root
+import blenderbim.core.spatial
+import blenderbim.core.geometry
+import blenderbim.tool as tool
+import json
+from math import pi
+from mathutils import Vector, Matrix
+from shapely import Polygon, MultiPolygon
+
+class Covering(blenderbim.core.tool.Covering):
+ @classmethod
+# def toggle_spaces_visibility_wired_and_textured(cls, spaces):
+# 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
+#
+# 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
diff --git a/src/blenderbim/blenderbim/tool/debug.py b/src/blenderbim/blenderbim/tool/debug.py
index 7f0888e2ef..3bf44856a1 100644
--- a/src/blenderbim/blenderbim/tool/debug.py
+++ b/src/blenderbim/blenderbim/tool/debug.py
@@ -20,6 +20,7 @@ import os
import bpy
import ifcopenshell.express
import blenderbim.core.tool
+import blenderbim.tool as tool
from blenderbim.bim.ifc import IfcStore
@@ -48,3 +49,42 @@ class Debug(blenderbim.core.tool.Debug):
obj = bpy.data.objects.new(name, mesh)
bpy.context.scene.collection.objects.link(obj)
return obj
+
+ @classmethod
+ def remove_unused_elements(cls, elements):
+ ifc_file = tool.Ifc.get()
+ for element in elements:
+ ifcopenshell.util.element.remove_deep2(ifc_file, element)
+
+ @classmethod
+ def print_unused_elements_stats(cls, requested_ifc_class="", ignore_classes=tuple()):
+ ifc_file = tool.Ifc.get()
+
+ # get list of ifc classes used in model
+ classes = set()
+ requested_ifc_classes = set()
+ for el in ifc_file:
+ if any(el.is_a(i) for i in ignore_classes):
+ continue
+ classes.add(el.is_a())
+ if requested_ifc_class and el.is_a(requested_ifc_class):
+ requested_ifc_classes.add(el.is_a())
+
+ # count unused elements for each class
+ unused = dict()
+ for c in classes:
+ uses = [i for i in ifc_file.by_type(c) if ifc_file.get_total_inverses(i) == 0]
+ if not uses:
+ continue
+ unused[c] = len(uses)
+
+ # print classes and their unsued elements in ascending order
+ if unused:
+ print("Unused elements by classes:")
+ for ifc_class in sorted(unused.keys(), key=lambda x: unused[x]):
+ class_string = ifc_class
+ if ifc_class in requested_ifc_classes:
+ class_string = "---> " + class_string
+ print(f"{class_string: <50} {unused[ifc_class]: >5}")
+
+ return sum(unused.values())
diff --git a/src/blenderbim/blenderbim/tool/drawing.py b/src/blenderbim/blenderbim/tool/drawing.py
index 8c2751c4f6..6d461cefc5 100644
--- a/src/blenderbim/blenderbim/tool/drawing.py
+++ b/src/blenderbim/blenderbim/tool/drawing.py
@@ -26,9 +26,9 @@ import bmesh
import shutil
import logging
import shapely
+import platform
from shapely.ops import unary_union
import mathutils
-import webbrowser
import subprocess
import numpy as np
import blenderbim.core.tool
@@ -249,17 +249,7 @@ class Drawing(blenderbim.core.tool.Drawing):
obj_data = obj.data
bpy.data.objects.remove(obj)
if obj_data and obj_data.users == 0: # in case we have drawing element types
- cls.remove_object_data(obj_data)
-
- @classmethod
- def remove_object_data(cls, data):
- """also removes all related objects"""
- if isinstance(data, bpy.types.Camera):
- bpy.data.cameras.remove(data)
- elif isinstance(data, bpy.types.Mesh):
- bpy.data.meshes.remove(data)
- elif isinstance(data, bpy.types.Curve):
- bpy.data.curves.remove(data)
+ tool.Blender.remove_data_block(obj_data)
@classmethod
def delete_object(cls, obj):
@@ -445,7 +435,7 @@ class Drawing(blenderbim.core.tool.Drawing):
@classmethod
def get_drawing_target_view(cls, drawing):
- return ifcopenshell.util.element.get_psets(drawing)["EPset_Drawing"].get("TargetView", "MODEL_VIEW")
+ return ifcopenshell.util.element.get_psets(drawing).get("EPset_Drawing", {}).get("TargetView", "MODEL_VIEW")
@classmethod
def get_group_elements(cls, group):
@@ -844,7 +834,12 @@ class Drawing(blenderbim.core.tool.Drawing):
command[0] = shutil.which(command[0]) or command[0]
subprocess.Popen([replacements.get(c, c) for c in command])
else:
- webbrowser.open("file://" + path)
+ if platform.system() == "Darwin":
+ subprocess.call(("open", path))
+ elif platform.system() == "Windows":
+ os.startfile(path)
+ else:
+ subprocess.call(("xdg-open", path))
@classmethod
def open_spreadsheet(cls, uri):
@@ -895,7 +890,7 @@ class Drawing(blenderbim.core.tool.Drawing):
literals = cls.get_text_literal(obj, return_list=True)
cls.import_text_attributes(obj)
for i, literal in enumerate(literals):
- product = cls.get_assigned_product(tool.Ifc.get_entity(obj))
+ product = cls.get_assigned_product(tool.Ifc.get_entity(obj)) or tool.Ifc.get_entity(obj)
props.literals[i].value = cls.replace_text_literal_variables(literal.Literal, product)
@classmethod
@@ -1391,8 +1386,9 @@ class Drawing(blenderbim.core.tool.Drawing):
original_command = command
for variable in re.findall("{{.*?}}", command):
value = ifcopenshell.util.selector.get_element_value(product, variable[2:-2])
- command = command.replace(variable, repr(value))
- text = text.replace(original_command, str(eval(command[2:-2])))
+ value = '"' + str(value).replace('"', '\\"') + '"'
+ command = command.replace(variable, value)
+ text = text.replace(original_command, ifcopenshell.util.selector.format(command[2:-2]))
for variable in re.findall("{{.*?}}", text):
value = ifcopenshell.util.selector.get_element_value(product, variable[2:-2])
@@ -1509,7 +1505,7 @@ class Drawing(blenderbim.core.tool.Drawing):
elements = cls.get_elements_in_camera_view(tool.Ifc.get_object(drawing), bpy.data.objects)
include = pset.get("Include", None)
if include:
- elements = ifcopenshell.util.selector.filter_elements(ifc_file, include, elements=elements)
+ elements = ifcopenshell.util.selector.filter_elements(ifc_file, include)
else:
if tool.Ifc.get_schema() == "IFC2X3":
base_elements = set(ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcSpatialStructureElement"))
diff --git a/src/blenderbim/blenderbim/tool/geometry.py b/src/blenderbim/blenderbim/tool/geometry.py
index 0544c9d330..8e624d61f0 100644
--- a/src/blenderbim/blenderbim/tool/geometry.py
+++ b/src/blenderbim/blenderbim/tool/geometry.py
@@ -46,7 +46,7 @@ class Geometry(blenderbim.core.tool.Geometry):
@classmethod
def clear_cache(cls, element):
cache = IfcStore.get_cache()
- if cache:
+ if cache and hasattr(element, "GlobalId"):
cache.remove(element.GlobalId)
@classmethod
@@ -115,6 +115,12 @@ class Geometry(blenderbim.core.tool.Geometry):
for port in ifcopenshell.util.system.get_ports(element):
blenderbim.core.system.remove_port(tool.Ifc, tool.System, port=port)
ifcopenshell.api.run("root.remove_product", tool.Ifc.get(), product=element)
+
+ if isinstance(obj.data, bpy.types.Mesh) and not tool.Ifc.get_entity_by_id(
+ obj.data.BIMMeshProperties.ifc_definition_id
+ ):
+ tool.Blender.remove_data_block(obj.data)
+
if is_spatial:
blenderbim.core.spatial.load_container_manager(tool.Spatial)
try:
diff --git a/src/blenderbim/blenderbim/tool/ifc.py b/src/blenderbim/blenderbim/tool/ifc.py
index 247e3a5c66..a655b5ce1a 100644
--- a/src/blenderbim/blenderbim/tool/ifc.py
+++ b/src/blenderbim/blenderbim/tool/ifc.py
@@ -86,6 +86,15 @@ class Ifc(blenderbim.core.tool.Ifc):
except:
pass
+ @classmethod
+ def get_entity_by_id(cls, entity_id):
+ """useful to check whether entity_id is still exists in IFC"""
+ ifc_file = tool.Ifc.get()
+ try:
+ return ifc_file.by_id(entity_id)
+ except RuntimeError:
+ return None
+
@classmethod
def get_object(cls, element):
return IfcStore.get_element(element.id())
@@ -120,7 +129,6 @@ class Ifc(blenderbim.core.tool.Ifc):
IfcStore.guid_map[global_id] = obj
blenderbim.bim.handler.subscribe_to(obj, "name", blenderbim.bim.handler.name_callback)
- 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
)
diff --git a/src/blenderbim/blenderbim/tool/model.py b/src/blenderbim/blenderbim/tool/model.py
index 904f7019ed..47ae276c3a 100644
--- a/src/blenderbim/blenderbim/tool/model.py
+++ b/src/blenderbim/blenderbim/tool/model.py
@@ -28,9 +28,10 @@ import blenderbim.core.geometry as geometry
from mathutils import Matrix, Vector
from blenderbim.bim import import_ifc
from blenderbim.bim.module.geometry.helper import Helper
+from blenderbim.bim.module.model.data import AuthoringData, RailingData, RoofData, WindowData, DoorData
import collections
-from blenderbim.bim.module.model.data import AuthoringData
import json
+import numpy as np
class Model(blenderbim.core.tool.Model):
@@ -540,11 +541,56 @@ class Model(blenderbim.core.tool.Model):
return axes
@classmethod
- def regenerate_array(cls, parent, data, keep_objs=False):
- tool.Blender.Modifier.Array.remove_constraints(tool.Ifc.get_entity(parent))
+ def handle_array_on_copied_element(cls, element, array_data=None):
+ """if no `array_data` is provided then an array will be removed from the element"""
+
+ if array_data is None:
+ array_pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array")
+ if not array_pset:
+ return
+
+ array_pset_data = array_pset["Data"]
+ array_pset = tool.Ifc.get().by_id(array_pset["id"])
+ ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=array_pset)
+
+ # remove constraints
+ obj = tool.Ifc.get_object(element)
+ if not array_pset_data: # skip array parents
+ constraint = next((c for c in obj.constraints if c.type == "CHILD_OF"), None)
+ if constraint:
+ matrix = obj.matrix_world.copy()
+ obj.constraints.remove(constraint)
+ # keep the matrix before the constraint
+ # otherwise object will jump to some previous position
+ obj.matrix_world = matrix
+ tool.Blender.lock_transform(obj, False)
+
+ else:
+ obj = tool.Ifc.get_object(element)
+ array_pset = tool.Pset.get_element_pset(element, "BBIM_Array")
+ default_data = '[{"children": []}]'
+ ifcopenshell.api.run(
+ "pset.edit_pset",
+ tool.Ifc.get(),
+ pset=array_pset,
+ properties={"Parent": element.GlobalId, "Data": default_data},
+ )
+
+ tool.Model.regenerate_array(obj, array_data)
+
+ json_data = json.dumps(array_data)
+ ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=array_pset, properties={"Data": json_data})
+
+ for i in range(len(array_data)):
+ tool.Blender.Modifier.Array.set_children_lock_state(element, i, True)
+ tool.Blender.Modifier.Array.constrain_children_to_parent(element)
+
+ @classmethod
+ def regenerate_array(cls, parent_obj, data, keep_objs=False):
+ tool.Blender.Modifier.Array.remove_constraints(tool.Ifc.get_entity(parent_obj))
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
- obj_stack = [parent]
+ obj_stack = [parent_obj]
for array in data:
if array["sync_children"]:
@@ -770,8 +816,12 @@ class Model(blenderbim.core.tool.Model):
tool.Ifc.run("geometry.edit_object_placement", product=element, matrix=matrix, is_si=True)
@classmethod
- def get_element_matrix(cls, element):
- placement = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
+ def get_element_matrix(cls, element, keep_local=False):
+ placement = element.ObjectPlacement
+ if keep_local:
+ placement = ifcopenshell.util.placement.get_axis2placement(placement.RelativePlacement)
+ else:
+ placement = ifcopenshell.util.placement.get_local_placement(placement)
return Matrix(placement)
@classmethod
@@ -805,3 +855,19 @@ class Model(blenderbim.core.tool.Model):
is_global=True,
should_sync_changes_first=False,
)
+
+ @classmethod
+ def is_parametric_roof_active(cls):
+ return (RoofData.is_loaded or not RoofData.load()) and RoofData.data["pset_data"]
+
+ @classmethod
+ def is_parametric_railing_active(cls):
+ return (RailingData.is_loaded or not RailingData.load()) and RailingData.data["pset_data"]
+
+ @classmethod
+ def is_parametric_window_active(cls):
+ return (WindowData.is_loaded or not WindowData.load()) and WindowData.data["pset_data"]
+
+ @classmethod
+ def is_parametric_door_active(cls):
+ return (DoorData.is_loaded or not DoorData.load()) and DoorData.data["pset_data"]
diff --git a/src/blenderbim/blenderbim/tool/pset.py b/src/blenderbim/blenderbim/tool/pset.py
index 62f52bdecc..1c115f7e1e 100644
--- a/src/blenderbim/blenderbim/tool/pset.py
+++ b/src/blenderbim/blenderbim/tool/pset.py
@@ -66,3 +66,10 @@ class Pset(blenderbim.core.tool.Pset):
if value is not None:
return False
return True
+
+ @classmethod
+ def enable_pset_editing(cls, pset_id=None, pset_name=None, pset_type=None, obj=None, obj_type=None):
+ #TODO REFACTOR ONCE toll/CORE functions are available
+ bpy.ops.bim.enable_pset_editing(
+ pset_id=0, pset_name=tool.Pset.get_pset_name(obj, obj_type), pset_type="PSET", obj=obj, obj_type=obj_type
+ )
\ No newline at end of file
diff --git a/src/blenderbim/blenderbim/tool/resource.py b/src/blenderbim/blenderbim/tool/resource.py
index 3c5698505d..99293fcbc9 100644
--- a/src/blenderbim/blenderbim/tool/resource.py
+++ b/src/blenderbim/blenderbim/tool/resource.py
@@ -61,6 +61,7 @@ class Resource(blenderbim.core.tool.Resource):
continue
create_new_resource_li(resource, 0)
cls.load_productivity_data()
+ cls.load_resource_properties()
props.is_resource_update_enabled = True
props.is_editing = True
@@ -425,7 +426,7 @@ class Resource(blenderbim.core.tool.Resource):
contracted_resources.remove(ancestor)
bpy.context.scene.BIMResourceProperties.contracted_resources = json.dumps(contracted_resources)
cls.load_resources()
- cls.load_resource_properties()
+
resource_props = bpy.context.scene.BIMResourceTreeProperties
expanded_resources = [item.ifc_definition_id for item in resource_props.resources]
diff --git a/src/blenderbim/blenderbim/tool/sequence.py b/src/blenderbim/blenderbim/tool/sequence.py
index 6fea1e3639..a62322723a 100644
--- a/src/blenderbim/blenderbim/tool/sequence.py
+++ b/src/blenderbim/blenderbim/tool/sequence.py
@@ -191,12 +191,14 @@ class Sequence(blenderbim.core.tool.Sequence):
props = bpy.context.scene.BIMWorkScheduleProperties
task_props = bpy.context.scene.BIMTaskTreeProperties
+ tasks_with_visual_bar = cls.get_task_bar_list()
props.is_task_update_enabled = False
for item in task_props.tasks:
task = tool.Ifc.get().by_id(item.ifc_definition_id)
item.name = task.Name or "Unnamed"
item.identification = task.Identification or "XXX"
+ item.has_bar_visual = item.ifc_definition_id in tasks_with_visual_bar
if props.highlighted_task_id:
item.is_predecessor = props.highlighted_task_id in [
rel.RelatedProcess.id() for rel in task.IsPredecessorTo
@@ -205,10 +207,8 @@ class Sequence(blenderbim.core.tool.Sequence):
rel.RelatingProcess.id() for rel in task.IsSuccessorFrom
]
calendar = ifcopenshell.util.sequence.derive_calendar(task)
- if task.HasAssignments:
- for rel in task.HasAssignments:
- if rel.is_a("IfcRelAssignsToControl") and rel.RelatingControl.is_a("IfcWorkCalendar"):
- item.calendar = calendar.Name or "Unnamed" if calendar else ""
+ if ifcopenshell.util.sequence.get_calendar(task):
+ item.calendar = calendar.Name or "Unnamed" if calendar else ""
else:
item.calendar = ""
item.derived_calendar = calendar.Name or "Unnamed" if calendar else ""
@@ -254,14 +254,6 @@ class Sequence(blenderbim.core.tool.Sequence):
return None
return tool.Ifc.get().by_id(bpy.context.scene.BIMWorkScheduleProperties.active_work_schedule_id)
- @classmethod
- def get_selected_resource(cls):
- if bpy.context.scene.BIMResourceTreeProperties.resources:
- selected_resource_id = bpy.context.scene.BIMResourceTreeProperties.resources[
- bpy.context.scene.BIMResourceProperties.active_resource_index
- ].ifc_definition_id
- return tool.Ifc.get().by_id(selected_resource_id)
-
@classmethod
def expand_task(cls, task):
props = bpy.context.scene.BIMWorkScheduleProperties
@@ -409,19 +401,17 @@ class Sequence(blenderbim.core.tool.Sequence):
return blenderbim.bim.helper.export_attributes(props.task_time_attributes, callback)
@classmethod
- def load_task_resources(cls, resources):
+ def load_task_resources(cls, task):
props = bpy.context.scene.BIMWorkScheduleProperties
+ rprops = bpy.context.scene.BIMResourceProperties
props.task_resources.clear()
- for resource in resources or []:
+ rprops.is_resource_update_enabled = False
+ for resource in cls.get_task_resources(task) or []:
new = props.task_resources.add()
new.ifc_definition_id = resource.id()
new.name = resource.Name or "Unnamed"
new.schedule_usage = resource.Usage.ScheduleUsage or 0 if resource.Usage else 0
-
- @classmethod
- def load_resources(cls):
- blenderbim.core.resource.load_resources(tool.Resource)
- cls.refresh_task_resources
+ rprops.is_resource_update_enabled = True
@classmethod
def get_task_inputs(cls, task):
@@ -447,6 +437,8 @@ class Sequence(blenderbim.core.tool.Sequence):
@classmethod
def get_task_resources(cls, task):
+ if not task:
+ return
is_deep = bpy.context.scene.BIMWorkScheduleProperties.show_nested_resources
return ifcopenshell.util.sequence.get_task_resources(task, is_deep)
@@ -806,10 +798,9 @@ class Sequence(blenderbim.core.tool.Sequence):
@classmethod
def get_animation_bar_tasks(cls):
return [
- tool.Ifc.get().by_id(item.ifc_definition_id)
- for item in bpy.context.scene.BIMTaskTreeProperties.tasks
- if item.has_bar_visual
- ] or []
+ tool.Ifc.get().by_id(task_id)
+ for task_id in cls.get_task_bar_list()
+ ]
@classmethod
def create_bars(cls, tasks):
@@ -1080,9 +1071,9 @@ class Sequence(blenderbim.core.tool.Sequence):
for predefined_type in data["PredefinedType"]:
if group in ["CREATION", "OPERATION", "MOVEMENT_TO"]:
predefined_type_item = props.task_output_colors.add()
- elif group in ["MOVEMENT_FROM", "DESTRUCTION"]:
+ elif group in ["MOVEMENT_FROM"]:
predefined_type_item = props.task_input_colors.add()
- elif group == "USERDEFINED":
+ elif group in ["USERDEFINED", "DESTRUCTION"]:
predefined_type_item = props.task_input_colors.add()
predefined_type_item2 = props.task_output_colors.add()
predefined_type_item2.name = predefined_type
@@ -1299,6 +1290,12 @@ class Sequence(blenderbim.core.tool.Sequence):
obj.hide_viewport = False
obj.hide_render = False
+ @classmethod
+ def hide_object(cls, obj):
+ if obj.visible_get():
+ obj.hide_viewport = True
+ obj.hide_render = True
+
@classmethod
def clear_objects_animation(cls, include_blender_objects=True):
for obj in bpy.data.objects:
@@ -1309,19 +1306,20 @@ class Sequence(blenderbim.core.tool.Sequence):
cls.display_object(obj)
@classmethod
- def animate_objects(cls, settings, frames, clear_previous=True, animation_type=""):
+ def animate_objects(cls, settings, frames, animation_type=""):
for obj in bpy.data.objects:
if not obj.BIMObjectProperties.ifc_definition_id:
continue
- if clear_previous:
- cls.clear_object_animation(obj)
+ if tool.Ifc.get().by_id(obj.BIMObjectProperties.ifc_definition_id).is_a("IfcSpace"):
+ cls.hide_object(obj)
+ continue
cls.earliest_frame = None
product_frames = frames.get(obj.BIMObjectProperties.ifc_definition_id, [])
for product_frame in product_frames:
if product_frame["relationship"] == "input":
cls.animate_input(obj, settings["start_frame"], product_frame, animation_type)
elif product_frame["relationship"] == "output":
- cls.animate_output(obj, settings["start_frame"], product_frame)
+ cls.animate_output(obj, settings["start_frame"], product_frame, animation_type)
area = next(area for area in bpy.context.screen.areas if area.type == "VIEW_3D")
area.spaces[0].shading.color_type = "OBJECT"
bpy.context.scene.frame_start = settings["start_frame"]
@@ -1331,14 +1329,19 @@ class Sequence(blenderbim.core.tool.Sequence):
def animate_input(cls, obj, start_frame, product_frame, animation_type):
props = bpy.context.scene.BIMAnimationProperties
color = props.task_input_colors[product_frame["type"]].color
- cls.animate_destruction(obj, start_frame, product_frame, color, animation_type)
+ if product_frame["type"] in ["LOGISTIC", "MOVE", "DISPOSAL"]:
+ cls.animate_destruction(obj, start_frame, product_frame, color, animation_type)
+ else:
+ cls.animate_consumption(obj, start_frame, product_frame, color, animation_type)
@classmethod
- def animate_output(cls, obj, start_frame, product_frame):
+ def animate_output(cls, obj, start_frame, product_frame, animation_type):
props = bpy.context.scene.BIMAnimationProperties
color = props.task_output_colors[product_frame["type"]].color
if product_frame["type"] in ["CONSTRUCTION", "INSTALLATION", "NOTDEFINED"]:
cls.animate_creation(obj, start_frame, product_frame, color)
+ elif product_frame["type"] in ["DEMOLITION", "DISMANTLE", "DISPOSAL", "REMOVAL"]:
+ cls.animate_destruction(obj, start_frame, product_frame, color, animation_type)
elif product_frame["type"] in ["ATTENDANCE", "MAINTENANCE", "OPERATION", "RENOVATION"]:
cls.animate_operation(obj, start_frame, product_frame, color)
elif product_frame["type"] in ["LOGISTIC", "MOVE"]:
@@ -1664,20 +1667,41 @@ class Sequence(blenderbim.core.tool.Sequence):
return
inputs = cls.get_task_inputs(task)
outputs = cls.get_task_outputs(task)
- resources = cls.get_task_resources(task)
cls.load_task_inputs(inputs)
cls.load_task_outputs(outputs)
- cls.load_task_resources(resources)
+ cls.load_task_resources(task)
@classmethod
def refresh_task_resources(cls):
task = cls.get_highlighted_task()
if not task:
return
- cls.load_task_resources(cls.get_task_resources(task))
+ cls.load_task_resources(task)
@classmethod
def has_duration(cls, task):
if task.TaskTime and task.TaskTime.ScheduleDuration:
return True
return False
+
+ @classmethod
+ def get_task_bar_list(cls):
+ return json.loads(bpy.context.scene.BIMWorkScheduleProperties.task_bars)
+
+ @classmethod
+ def add_task_bar(cls, task_id):
+ task_bars = cls.get_task_bar_list()
+ task_bars.append(task_id)
+ bpy.context.scene.BIMWorkScheduleProperties.task_bars = json.dumps(task_bars)
+
+ @classmethod
+ def remove_task_bar(cls, task_id):
+ task_bars = cls.get_task_bar_list()
+ if task_id in task_bars:
+ task_bars.remove(task_id)
+ bpy.context.scene.BIMWorkScheduleProperties.task_bars = json.dumps(task_bars)
+
+ @classmethod
+ def get_animation_color_scheme(cls):
+ if len(bpy.context.scene.BIMAnimationProperties.saved_color_schemes) > 0:
+ return tool.Ifc.get().by_id(int(bpy.context.scene.BIMAnimationProperties.saved_color_schemes))
diff --git a/src/blenderbim/blenderbim/tool/spatial.py b/src/blenderbim/blenderbim/tool/spatial.py
index 3e89449511..f838703059 100644
--- a/src/blenderbim/blenderbim/tool/spatial.py
+++ b/src/blenderbim/blenderbim/tool/spatial.py
@@ -17,13 +17,18 @@
# along with BlenderBIM Add-on. If not, see .
import bpy
+import bmesh
+import shapely
import ifcopenshell
import blenderbim.core.tool
import blenderbim.core.root
import blenderbim.core.spatial
+import blenderbim.core.geometry
import blenderbim.tool as tool
import json
-
+from math import pi
+from mathutils import Vector, Matrix
+from shapely import Polygon, MultiPolygon
class Spatial(blenderbim.core.tool.Spatial):
@classmethod
@@ -198,6 +203,7 @@ class Spatial(blenderbim.core.tool.Spatial):
@classmethod
def load_container_manager(cls):
cls.props = bpy.context.scene.BIMSpatialManagerProperties
+ previous_container_index = cls.props.active_container_index
cls.props.containers.clear()
cls.contracted_containers = json.loads(cls.props.contracted_containers)
cls.props.is_container_update_enabled = False
@@ -208,16 +214,17 @@ class Spatial(blenderbim.core.tool.Spatial):
cls.create_new_storey_li(object, 0)
cls.props.is_container_update_enabled = True
# triggers spatial manager props setup
- cls.props.active_container_index = 0
+ cls.props.active_container_index = min(previous_container_index, len(cls.props.containers) - 1)
@classmethod
def create_new_storey_li(cls, element, level_index):
+ si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
new = cls.props.containers.add()
new.name = element.Name or "Unnamed"
new.long_name = element.LongName or ""
new.has_decomposition = bool(element.IsDecomposedBy)
new.ifc_definition_id = element.id()
- new.elevation = ifcopenshell.util.placement.get_storey_elevation(element)
+ new.elevation = ifcopenshell.util.placement.get_storey_elevation(element) * si_conversion
new.is_expanded = element.id() not in cls.contracted_containers
new.level_index = level_index
@@ -260,3 +267,252 @@ class Spatial(blenderbim.core.tool.Spatial):
contracted_containers = json.loads(props.contracted_containers)
contracted_containers.remove(container.id())
props.contracted_containers = json.dumps(contracted_containers)
+
+#HERE STARTS SPATIAL TOOL
+
+ @classmethod
+ def get_union_shape_from_selected_objects(cls):
+ selected_objects = bpy.context.selected_objects
+ boundary_elements = cls.get_boundary_elements(selected_objects)
+ polys = cls.get_polygons(boundary_elements)
+ converted_tolerance = cls.get_converted_tolerance(tolerance=0.03)
+ union = shapely.ops.unary_union(polys).buffer(converted_tolerance, cap_style=2, join_style=2)
+ union = cls.get_purged_inner_holes_poly(union_geom=union, min_area=cls.get_converted_tolerance(tolerance=3))
+
+ return union
+
+ @classmethod
+ def get_boundary_elements(cls, 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
+
+ @classmethod
+ def get_polygons(cls, boundary_elements):
+ polys = []
+ for boundary_element in boundary_elements:
+ obj = tool.Ifc.get_object(boundary_element)
+ if not obj:
+ continue
+ points = []
+ base = cls.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
+
+ @classmethod
+ def get_obj_base_points(cls, 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]),
+ }
+
+ @classmethod
+ def get_converted_tolerance(cls, 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
+
+ @classmethod
+ def get_purged_inner_holes_poly(cls, union_geom, min_area):
+ interiors_list = []
+
+ if union_geom.geom_type == "MultiPolygon":
+ for poly in union_geom.geoms:
+ interiors_list = cls.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 = cls.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
+
+ @classmethod
+ def get_poly_valid_interior_list(cls, 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
+
+ @classmethod
+ def get_buffered_poly_from_linear_ring(cls, linear_ring):
+ poly = Polygon(linear_ring)
+ converted_tolerance = cls.get_converted_tolerance(tolerance=0.03)
+ poly = poly.buffer(converted_tolerance, single_sided=True, cap_style=2, join_style=2)
+ return poly
+
+ @classmethod
+ def get_bmesh_from_polygon(cls, poly, h):
+ mat = bpy.context.active_object.matrix_world
+ 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)
+
+ if h!=0:
+ 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
+
+ @classmethod
+ def get_named_obj_from_bmesh(cls, name, bmesh):
+ mesh = bpy.data.meshes.new(name=name)
+ bmesh.to_mesh(mesh)
+ bmesh.free()
+ obj = bpy.data.objects.new(name, mesh)
+ mat = bpy.context.active_object.matrix_world
+ obj.matrix_world = mat
+ return obj
+
+ @classmethod
+ def set_obj_origin_to_bboxcenter(cls, 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
+
+ @classmethod
+ def get_active_obj_z(cls):
+ x, y, z = bpy.context.active_object.matrix_world.translation.xyz
+ return z
+
+ @classmethod
+ def traslate_obj_to_z_location(cls, obj, z):
+ if z != 0:
+ obj.location = obj.location + Vector((0, 0, z))
+
+ @classmethod
+ def link_obj_to_active_collection(cls, obj):
+ bpy.context.view_layer.active_layer_collection.collection.objects.link(obj)
+
+ @classmethod
+ def get_2d_vertices_from_obj(cls, obj):
+ points = []
+ vectors = [v.co for v in obj.data.vertices.values()]
+ for vector in vectors:
+ point = (vector[0], vector[1])
+ points.append(point)
+
+ points.append((vectors[0][0], vectors[0][1]))
+ return points
+
+ @classmethod
+ def assign_swept_area_outer_curve_from_2d_vertices(cls, obj, vertices):
+ body = cls.get_body_representation(obj)
+ model = tool.Ifc.get()
+ extrusion = tool.Model.get_extrusion(body)
+ area = extrusion.SweptArea
+ old_area = area.OuterCurve
+
+ builder = ifcopenshell.util.shape_builder.ShapeBuilder(model)
+ outer_curve = builder.polyline(vertices, closed = True)
+
+ area.OuterCurve = outer_curve
+ ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), old_area)
+
+ @classmethod
+ def get_body_representation(cls, obj):
+ element = tool.Ifc.get_entity(obj)
+ model = tool.Ifc.get()
+ body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
+ return body
+
+ @classmethod
+ def assign_type_to_obj(cls, obj):
+ relating_type_id = bpy.context.scene.BIMModelProperties.relating_type_id
+ relating_type = tool.Ifc.get().by_id(int(relating_type_id))
+ ifc_class = relating_type.is_a()
+ instance_class = ifcopenshell.util.type.get_applicable_entities(ifc_class, tool.Ifc.get().schema)[0]
+ bpy.ops.bim.assign_class(obj=obj.name, ifc_class=instance_class)
+ element = tool.Ifc.get_entity(obj)
+ blenderbim.core.type.assign_type(tool.Ifc, tool.Type, element=element, type=relating_type)
+
+ @classmethod
+ def assign_container_to_obj(cls, obj):
+ active_obj = bpy.context.active_object
+ element = tool.Ifc.get_entity(active_obj)
+ container = ifcopenshell.util.element.get_container(element)
+ 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
+ )
+
+ @classmethod
+ def regen_obj_representation(cls, ifc, geometry, obj, body):
+ blenderbim.core.geometry.switch_representation(
+ ifc,
+ geometry,
+ obj=obj,
+ representation=body,
+ should_reload=True,
+ is_global=True,
+ should_sync_changes_first=False,
+ )
+
+ @classmethod
+ def toggle_spaces_visibility_wired_and_textured(cls, spaces):
+ 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
+
+ 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
diff --git a/src/blenderbim/blenderbim/tool/system.py b/src/blenderbim/blenderbim/tool/system.py
index b274ff2929..6a0cd8b080 100644
--- a/src/blenderbim/blenderbim/tool/system.py
+++ b/src/blenderbim/blenderbim/tool/system.py
@@ -22,7 +22,10 @@ import blenderbim.core.tool
import blenderbim.tool as tool
from blenderbim.bim import import_ifc
import re
+from math import pi, cos, sin
from mathutils import Matrix, Vector
+from blenderbim.bim.module.system.data import ObjectSystemData, SystemDecorationData
+from blenderbim.bim.module.drawing.decoration import profile_consequential
class System(blenderbim.core.tool.System):
@@ -42,7 +45,8 @@ class System(blenderbim.core.tool.System):
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
mep_element = tool.Ifc.get_entity(obj)
- length = obj.dimensions.z
+ bbox = tool.Blender.get_object_bounding_box(obj)
+ length = bbox["min_z"] if tool.Cad.is_x(bbox["max_z"], 0) else bbox["max_z"]
ports = []
if add_start_port:
ports.append(add_port(mep_element, obj.matrix_world @ Matrix()))
@@ -104,7 +108,10 @@ class System(blenderbim.core.tool.System):
@classmethod
def get_port_predefined_type(cls, mep_element):
split_camel_case = lambda x: re.findall("[A-Z][^A-Z]*", x)
- class_name = "".join(split_camel_case(mep_element.is_a())[1:-1]).upper()
+ mep_class = mep_element.is_a()
+ if mep_class.endswith("Type"):
+ mep_class = mep_class[:-4]
+ class_name = "".join(split_camel_case(mep_class)[1:-1]).upper()
if class_name == "CONVEYOR":
return "NOTDEFINED"
return class_name
@@ -181,3 +188,151 @@ class System(blenderbim.core.tool.System):
@classmethod
def set_active_system(cls, system):
bpy.context.scene.BIMSystemProperties.active_system_id = system.id()
+
+ @classmethod
+ def get_decoration_data(cls):
+ all_vertices = []
+ preview_edges = []
+ special_vertices = []
+ selected_edges = []
+ selected_vertices = []
+
+ view3d_space = tool.Blender.get_viewport_context()["space_data"].region_3d
+ viewport_matrix = view3d_space.view_matrix.inverted()
+ viewport_y_axis = viewport_matrix.col[1].to_3d().normalized()
+ camera_pos = viewport_matrix.translation
+ dir_to_camera = lambda x: (camera_pos - x).normalized()
+
+ def most_aligned_vector(a, vectors):
+ return max(vectors, key=lambda v: abs(a.dot(v)))
+
+ start_vert_i = 0
+
+ if not ObjectSystemData.is_loaded:
+ ObjectSystemData.load()
+
+ if not SystemDecorationData.is_loaded:
+ SystemDecorationData.load()
+
+ object_system_data = ObjectSystemData.data
+ selected_elements = object_system_data["connected_elements"]
+
+ # TODO: get only objects visible in viewport
+ objects = set(bpy.data.objects) - set(bpy.data.collections["Types"].objects)
+ for obj in objects:
+ start_vert_i = len(all_vertices)
+ if obj.hide_get():
+ continue
+
+ if not isinstance(obj.data, bpy.types.Mesh):
+ continue
+
+ element = tool.Ifc.get_entity(obj)
+ if not element:
+ continue
+
+ if not cls.is_mep_element(element):
+ continue
+
+ selected_element = element in selected_elements
+ verts_pos = []
+
+ port_data = SystemDecorationData.get_element_ports_data(element)
+ verts_pos.extend([obj.matrix_world @ data["position"] for data in port_data])
+
+ verts = range(start_vert_i, start_vert_i + len(port_data))
+ edges = [(i, i + 1) for i in range(start_vert_i, start_vert_i + len(port_data) - 1)]
+
+ def get_flow_direction(port_data):
+ # diagram - https://i.imgur.com/ioYL7bZ.png
+ flow_dirs = [p["flow_direction"] for p in port_data]
+ unique = set(flow_dirs)
+ if len(unique) == 1:
+ return 0
+ elif flow_dirs[0] == "SOURCE":
+ return -1
+ elif flow_dirs[0] == "SINK":
+ return 1
+ elif flow_dirs[1] == "SOURCE":
+ return 1
+ elif flow_dirs[1] == "SINK":
+ return -1
+ return 0
+
+ if len(port_data) == 2 and selected_element and (flow_direction := get_flow_direction(port_data)):
+ edge_verts = verts_pos.copy()
+ edge_verts = edge_verts[::flow_direction]
+
+ # create direction lines
+ direction_lines_offset = 0.4
+ direction_lines_width = 0.05
+ base_vert = edge_verts[0]
+ edge = edge_verts[1] - edge_verts[0]
+ edge_length = edge.length
+ edge_dir = edge.normalized()
+ # edge_ortho = most_aligned_vector(
+ # viewport_y_axis, (
+ # obj.matrix_world.col[0].to_3d().normalized(),
+ # obj.matrix_world.col[1].to_3d().normalized(),
+ # ))
+
+ # for now it's hardcoded to local Y axis to avoid using viewport data
+ # for performance reasons
+ edge_ortho = obj.matrix_world.col[1].to_3d().normalized()
+ second_ortho = edge_dir.cross(edge_ortho)
+ edge_ortho = second_ortho.cross(edge_dir)
+
+ # direction lines should be around the edge center
+ n_direction_lines, start_offset = divmod(edge_length, direction_lines_offset)
+ n_direction_lines = int(n_direction_lines) + 1
+ start_offset /= 2
+ start_offset = edge_dir * start_offset + base_vert
+ cur_vert_index = start_vert_i + len(port_data)
+
+ for i in range(n_direction_lines):
+ cur_offset = start_offset + edge_dir * i * direction_lines_offset
+ arrow_base = cur_offset - edge_dir * direction_lines_width
+ verts_pos.append(arrow_base + edge_ortho * direction_lines_width)
+ verts_pos.append(cur_offset)
+ verts_pos.append(arrow_base - edge_ortho * direction_lines_width)
+ edges.append((cur_vert_index, cur_vert_index + 1))
+ edges.append((cur_vert_index + 1, cur_vert_index + 2))
+ cur_vert_index += 3
+
+ all_vertices.extend(verts_pos)
+
+ if selected_element:
+ selected_vertices.extend(verts)
+ selected_edges.extend(edges)
+ else:
+ special_vertices.extend(verts)
+ preview_edges.extend(edges)
+
+ decoration_data = {
+ "all_vertices": all_vertices,
+ "preview_edges": preview_edges,
+ "special_vertices": [all_vertices[i] for i in special_vertices],
+ "selected_edges": selected_edges,
+ "selected_vertices": [all_vertices[i] for i in selected_vertices],
+ }
+ return decoration_data
+
+ @classmethod
+ def get_connected_elements(cls, element, elements=None):
+ if elements is None:
+ elements = set((element,))
+
+ connected_elements = ifcopenshell.util.system.get_connected_from(element)
+ connected_elements += ifcopenshell.util.system.get_connected_to(element)
+
+ for element in connected_elements:
+ if element in elements:
+ continue
+ elements.add(element)
+ cls.get_connected_elements(element, elements)
+
+ return elements
+
+ @classmethod
+ def is_mep_element(cls, element):
+ return element.is_a("IfcFlowSegment") or element.is_a("IfcFlowFitting")
diff --git a/src/blenderbim/blenderbim/tool/unit.py b/src/blenderbim/blenderbim/tool/unit.py
index 8d39f9fab1..baec38b579 100644
--- a/src/blenderbim/blenderbim/tool/unit.py
+++ b/src/blenderbim/blenderbim/tool/unit.py
@@ -163,3 +163,13 @@ class Unit(blenderbim.core.tool.Unit):
unit = cls.get_project_currency_unit()
if unit:
return unit.Currency
+
+ @classmethod
+ def blender_format_unit(cls, value):
+ return bpy.utils.units.to_string(
+ bpy.context.scene.unit_settings.system,
+ "LENGTH",
+ value,
+ precision=4,
+ split_unit=bpy.context.scene.unit_settings.system == "IMPERIAL",
+ )
diff --git a/src/blenderbim/blenderbim_icons.blend b/src/blenderbim/blenderbim_icons.blend
index df16936ee6..387e6815f1 100644
Binary files a/src/blenderbim/blenderbim_icons.blend and b/src/blenderbim/blenderbim_icons.blend differ
diff --git a/src/blenderbim/docs/conf.py b/src/blenderbim/docs/conf.py
index e3c6526151..27073e246d 100644
--- a/src/blenderbim/docs/conf.py
+++ b/src/blenderbim/docs/conf.py
@@ -48,7 +48,10 @@ release = "0.0.220504"
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
-extensions = ["sphinx.ext.autodoc"]
+extensions = ["sphinx.ext.autodoc", "sphinx.ext.autosectionlabel"]
+
+# Auto add document prefixes to help guarantee uniqueness of automatic section references.
+autosectionlabel_prefix_document = True
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
diff --git a/src/blenderbim/docs/devs/writing_docs.rst b/src/blenderbim/docs/devs/writing_docs.rst
index 0d14e89bee..d16f7805a1 100644
--- a/src/blenderbim/docs/devs/writing_docs.rst
+++ b/src/blenderbim/docs/devs/writing_docs.rst
@@ -13,6 +13,14 @@ All documentation is written in ReStructured Text and is available in the
`BlenderBIM Add-on docs directory
`_.
+You can link to `external websites
+`_.
+You can also link to sections on the same page, like `Writing technical
+documentation`_. You can link to other pages, like :doc:`Hello
+World` or sections within other pages, like
+:ref:`devs/installation:unstable installation`. We have ``autosectionlabel``
+enabled so it is not necessary to manually create labels.
+
The following colours and annotation styles should be used for annotating
images. All stroke widths are 3px with a corner radius of 3px. Horizontal
underlines are 5px with a corner radius of 2px. The dark green is ``39b54a`` and
@@ -43,6 +51,16 @@ download.
`Visit critical link `__
+You can use bulleted lists:
+
+- Like.
+- This.
+
+Or ordered lists:
+
+1. Like.
+2. This.
+
.. note::
Instead of writing "Note that XYZ ..." you should use notes sparingly to
diff --git a/src/blenderbim/docs/users/installation.rst b/src/blenderbim/docs/users/installation.rst
index 713f494a4c..c50b4d3267 100644
--- a/src/blenderbim/docs/users/installation.rst
+++ b/src/blenderbim/docs/users/installation.rst
@@ -133,9 +133,10 @@ FAQ
1. **I get an error similar to "ImportError: IfcOpenShell not built for 'linux/64bit/python3.7'"**
Check which BlenderBIM Add-on build you are using. The zip will have either
- ``py37``, ``py39``, or ``py310`` in the name. See the instructions in the
- **Unstable installation** section to check that you have installed the
- correct version.
+ ``py39`` or ``py310`` in the name. If you are using a Mac, also make sure
+ you are using the M1 version if you have a newer Mac. See the instructions
+ in the :ref:`devs/installation:unstable installation` section to check that
+ you have installed the correct version.
2. **I am on Ubuntu and get an error similar to "ImportError:
/lib/x86_64-linux-gnu/libm.so.6: version GLIBC_2.29 not found"**
diff --git a/src/blenderbim/pytest.ini b/src/blenderbim/pytest.ini
index 4b4d9ae52c..2f55059a87 100644
--- a/src/blenderbim/pytest.ini
+++ b/src/blenderbim/pytest.ini
@@ -7,6 +7,7 @@ markers =
classification
context
cost
+ covering
debug
demo
document
diff --git a/src/blenderbim/scripts/generate_demo_library.py b/src/blenderbim/scripts/generate_demo_library.py
index 7f5775a39f..3990bdaab9 100644
--- a/src/blenderbim/scripts/generate_demo_library.py
+++ b/src/blenderbim/scripts/generate_demo_library.py
@@ -155,9 +155,10 @@ class LibraryGenerator:
self.create_line_type("MEDIUM", "medium")
self.create_line_type("THICK", "thick")
self.create_line_type("STRONG", "strong")
+ self.create_text_type("SETOUT-TAG", "setout-tag", ["E ``round({{easting}}, 0.001)``", "N ``round({{northing}}, 0.001)``"])
self.create_text_type("DOOR-TAG", "door-tag", ["{{type.Name}}", "{{Name}}"])
self.create_text_type("WINDOW-TAG", "window-tag", ["{{Name}}"])
- self.create_text_type("SPACE-TAG", "space-tag", ["{{Name}}", "{{Description}}", "``round({{Qto_SpaceBaseQuantities.NetFloorArea}} or 0., 2)``"])
+ self.create_text_type("SPACE-TAG", "space-tag", ["{{Name}}", "{{Description}}", "``round({{Qto_SpaceBaseQuantities.NetFloorArea}}, 0.01)``"])
self.create_text_type("MATERIAL-TAG", "rectangle-tag", ["{{material.Name}}"])
self.create_text_type("TYPE-TAG", "capsule-tag", ["{{type.Name}}"])
self.create_text_type("NAME-TAG", "capsule-tag", ["{{Name}}"])
diff --git a/src/blenderbim/scripts/generate_furniture_library.py b/src/blenderbim/scripts/generate_furniture_library.py
index d18920e900..a6153a30a3 100644
--- a/src/blenderbim/scripts/generate_furniture_library.py
+++ b/src/blenderbim/scripts/generate_furniture_library.py
@@ -1134,8 +1134,7 @@ class LibraryGenerator:
back_wall_extruded = builder.extrude(
back_wall,
back_wall_depth + back_wall_border_mask_depth,
- position_z_axis=V(0, -1, 0),
- extrusion_vector=V(0, 0, -1),
+ **builder.extrude_kwargs("Y")
)
items_3d_to_center.append(back_wall_extruded)
diff --git a/src/blenderbim/test/bim/feature/aggregate.feature b/src/blenderbim/test/bim/feature/aggregate.feature
index 662d3b6d15..9d6c35d294 100644
--- a/src/blenderbim/test/bim/feature/aggregate.feature
+++ b/src/blenderbim/test/bim/feature/aggregate.feature
@@ -61,6 +61,7 @@ Scenario: Add aggregate
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
@@ -73,6 +74,7 @@ Scenario: Add aggregate - with the aggregate inheriting the existing spatial col
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is placed in the collection "IfcBuildingStorey/My Storey"
@@ -86,6 +88,7 @@ Scenario: Add aggregate - add a nested aggregate
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is placed in the collection "IfcBuildingStorey/My Storey"
diff --git a/src/blenderbim/test/bim/feature/attribute.feature b/src/blenderbim/test/bim/feature/attribute.feature
index 2e0047e9c9..0810ec6068 100644
--- a/src/blenderbim/test/bim/feature/attribute.feature
+++ b/src/blenderbim/test/bim/feature/attribute.feature
@@ -38,10 +38,12 @@ Scenario: Copy attribute to selected
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
diff --git a/src/blenderbim/test/bim/feature/cost.feature b/src/blenderbim/test/bim/feature/cost.feature
index aefe3e5ff9..e74e6b003d 100644
--- a/src/blenderbim/test/bim/feature/cost.feature
+++ b/src/blenderbim/test/bim/feature/cost.feature
@@ -357,6 +357,7 @@ Scenario: Assign cost item quantity - count based
And the variable "cost_item" is "{ifc}.by_type('IfcCostItem')[0].id()"
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
When I press "bim.assign_cost_item_quantity(cost_item={cost_item}, related_object_type='PRODUCT', prop_name='')"
@@ -371,6 +372,7 @@ Scenario: Assign cost item quantity - quantity based
And the variable "cost_item" is "{ifc}.by_type('IfcCostItem')[0].id()"
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
@@ -388,6 +390,7 @@ Scenario: Unassign cost item quantity - selection based
And the variable "cost_item" is "{ifc}.by_type('IfcCostItem')[0].id()"
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
@@ -404,6 +407,7 @@ Scenario: Unassign cost item quantity - explicit object
And the variable "cost_item" is "{ifc}.by_type('IfcCostItem')[0].id()"
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
@@ -421,6 +425,7 @@ Scenario: Select cost item products
And the variable "cost_item" is "{ifc}.by_type('IfcCostItem')[0].id()"
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
@@ -437,6 +442,7 @@ Scenario: Select Cost Schedule Products
And the variable "cost_item" is "{ifc}.by_type('IfcCostItem')[0].id()"
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
diff --git a/src/blenderbim/test/bim/feature/covering.feature b/src/blenderbim/test/bim/feature/covering.feature
new file mode 100644
index 0000000000..bb7bdaf45d
--- /dev/null
+++ b/src/blenderbim/test/bim/feature/covering.feature
@@ -0,0 +1,15 @@
+@covering
+Feature: Covering
+ Covers covering tool.
+
+Scenario: Execute generate flooring coverings from walls
+ Given an empty IFC project
+ And I load the demo construction library
+ And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType"
+ And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()"
+ And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
+ And I press "bim.add_constr_type_instance"
+ And the object "IfcWall/Wall" is selected
+ When I press "bim.add_instance_flooring_coverings_from_walls"
+ Then nothing happens
+
diff --git a/src/blenderbim/test/bim/feature/document.feature b/src/blenderbim/test/bim/feature/document.feature
index db14740fbe..2157a79b2f 100644
--- a/src/blenderbim/test/bim/feature/document.feature
+++ b/src/blenderbim/test/bim/feature/document.feature
@@ -89,6 +89,7 @@ Scenario: Assign document
And I add a cube
And the object "Cube" is selected
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
When I press "bim.assign_document(document={reference})"
@@ -105,6 +106,7 @@ Scenario: Unassign document
And I add a cube
And the object "Cube" is selected
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I press "bim.assign_document(document={reference})"
diff --git a/src/blenderbim/test/bim/feature/drawing.feature b/src/blenderbim/test/bim/feature/drawing.feature
index e9d9ec5a81..28c2b5d80c 100644
--- a/src/blenderbim/test/bim/feature/drawing.feature
+++ b/src/blenderbim/test/bim/feature/drawing.feature
@@ -5,6 +5,7 @@ Scenario: Duplicate drawing
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the variable "wall1" is "IfcStore.get_file().by_type('IfcWall')[-1].id()"
@@ -17,6 +18,7 @@ Scenario: Create drawing
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the variable "wall1" is "IfcStore.get_file().by_type('IfcWall')[-1].id()"
@@ -31,6 +33,7 @@ Scenario: Create drawing after deleting a duplicated object
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the variable "wall1" is "IfcStore.get_file().by_type('IfcWall')[-1].id()"
@@ -52,6 +55,7 @@ Scenario: Remove drawing
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the variable "wall1" is "IfcStore.get_file().by_type('IfcWall')[-1].id()"
@@ -65,6 +69,7 @@ Scenario: Remove drawing - via object deletion
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the variable "wall1" is "IfcStore.get_file().by_type('IfcWall')[-1].id()"
@@ -79,6 +84,7 @@ Scenario: Remove drawing - deleting active drawing
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the variable "wall1" is "IfcStore.get_file().by_type('IfcWall')[-1].id()"
diff --git a/src/blenderbim/test/bim/feature/geometry.feature b/src/blenderbim/test/bim/feature/geometry.feature
index fc9afdd286..206769fc64 100644
--- a/src/blenderbim/test/bim/feature/geometry.feature
+++ b/src/blenderbim/test/bim/feature/geometry.feature
@@ -5,6 +5,7 @@ Scenario: Edit object placement
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
@@ -15,6 +16,7 @@ Scenario: Add representation
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
@@ -48,6 +50,7 @@ Scenario: Add representation - add a representation with a scale factor applied
And the object "Cube" is selected
When the object "Cube" is scaled to "2"
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
Then the object "IfcWall/Cube" has no scale
@@ -60,6 +63,7 @@ Scenario: Add representation - add a representation with a scale factor removed
And the object "Cube" is selected
When the object "Cube" is scaled to "2"
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
Then the object "IfcWall/Cube" has no scale
@@ -69,6 +73,7 @@ Scenario: Switch representation
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
When the variable "representation" is "{ifc}.by_type('IfcShapeRepresentation')[0].id()"
@@ -79,6 +84,7 @@ Scenario: Switch representation - current edited representation is updated prior
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the variable "context" is "[c for c in {ifc}.by_type('IfcGeometricRepresentationSubContext') if c.ContextType == 'Plan' and c.ContextIdentifier=='Annotation'][0].id()"
@@ -96,6 +102,7 @@ Scenario: Switch representation - current edited representation is discarded if
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
When the object "IfcWall/Cube" is scaled to "2"
@@ -111,6 +118,7 @@ Scenario: Switch representation - existing Blender modifiers must be purged
And I add a cube
And the object "Cube" is selected
And I add an array modifier
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
When the variable "representation" is "{ifc}.by_type('IfcShapeRepresentation')[0].id()"
@@ -122,6 +130,7 @@ Scenario: Remove representation - remove an active representation
And I add a cube
And the object "Cube" is selected
And I add an array modifier
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
When the variable "representation" is "{ifc}.by_type('IfcShapeRepresentation')[0].id()"
@@ -133,6 +142,7 @@ Scenario: Remove representation - remove an unloaded representation
And I add a cube
And the object "Cube" is selected
And I add an array modifier
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
When the variable "representation" is "{ifc}.by_type('IfcShapeRepresentation')[1].id()"
@@ -182,6 +192,7 @@ Scenario: Update representation - updating a tessellation
And I add a cube
And the object "Cube" is selected
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I press "bim.update_representation(obj='IfcWall/Cube')"
@@ -192,6 +203,7 @@ Scenario: Update representation - updating a layered extrusion
And I add a cube
And the object "Cube" is selected
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I add an empty
@@ -219,6 +231,7 @@ Scenario: Update representation - updating a profiled extrusion
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I add an empty
@@ -242,6 +255,7 @@ Scenario: Get representation IFC parameters
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I press "bim.update_representation(ifc_representation_class='IfcExtrudedAreaSolid/IfcRectangleProfileDef')"
@@ -252,6 +266,7 @@ Scenario: Copy representation
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I add a cube
@@ -273,6 +288,7 @@ Scenario: Override delete - with active IFC data
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
@@ -297,6 +313,7 @@ Scenario: Override duplicate move - with active IFC data
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
@@ -315,6 +332,7 @@ Scenario: Override duplicate move - copying a coloured representation
And I add a cube
And the object "Cube" is selected
And I add a material
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
@@ -347,6 +365,7 @@ Scenario: Override duplicate move - copying a layered extrusion
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I add an empty
@@ -376,6 +395,7 @@ Scenario: Override duplicate move - copying a profiled extrusion
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I add an empty
@@ -412,6 +432,7 @@ Scenario: Override duplicate move linked - with active IFC data
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
@@ -440,6 +461,7 @@ Scenario: Override paste buffer - with active IFC data
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
diff --git a/src/blenderbim/test/bim/feature/library.feature b/src/blenderbim/test/bim/feature/library.feature
index 3cc340d993..1223eccbda 100644
--- a/src/blenderbim/test/bim/feature/library.feature
+++ b/src/blenderbim/test/bim/feature/library.feature
@@ -113,6 +113,7 @@ Scenario: Assign library reference
And the variable "reference" is "{ifc}.by_type('IfcLibraryReference')[-1].id()"
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
@@ -128,6 +129,7 @@ Scenario: Unassign library reference
And the variable "reference" is "{ifc}.by_type('IfcLibraryReference')[-1].id()"
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
diff --git a/src/blenderbim/test/bim/feature/material.feature b/src/blenderbim/test/bim/feature/material.feature
index 18d9b8b0bc..290e8aaf0c 100644
--- a/src/blenderbim/test/bim/feature/material.feature
+++ b/src/blenderbim/test/bim/feature/material.feature
@@ -72,6 +72,7 @@ Scenario: Assign material - single material
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I press "bim.add_material(obj='')"
@@ -83,6 +84,7 @@ Scenario: Unassign material - single material
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I press "bim.add_material(obj='')"
@@ -95,6 +97,7 @@ Scenario: Enable editing assigned material - single material
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I press "bim.add_material(obj='')"
@@ -107,6 +110,7 @@ Scenario: Disable editing assigned material - single material
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I press "bim.add_material(obj='')"
@@ -120,6 +124,7 @@ Scenario: Edit assigned material - single material
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I press "bim.add_material(obj='')"
@@ -160,6 +165,7 @@ Scenario: Unassign material - removing inherited material
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
@@ -186,6 +192,7 @@ Scenario: Enable editing assigned material - material layer set
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I add an empty
@@ -203,6 +210,7 @@ Scenario: Disable editing assigned material - material layer set
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I add an empty
@@ -221,6 +229,7 @@ Scenario: Edit assigned material - material layer set
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I add an empty
@@ -266,6 +275,7 @@ Scenario: Enable editing assigned material - material profile set
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I add an empty
@@ -283,6 +293,7 @@ Scenario: Disable editing assigned material - material profile set
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I add an empty
@@ -301,6 +312,7 @@ Scenario: Edit assigned material - material profile set
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I add an empty
@@ -320,6 +332,7 @@ Scenario: Assign material - material constituent set
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I press "bim.add_material(obj='')"
@@ -331,6 +344,7 @@ Scenario: Unassign material - material constituent set
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I press "bim.add_material(obj='')"
@@ -343,6 +357,7 @@ Scenario: Enable editing assigned material - material constituent set
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I press "bim.add_material(obj='')"
@@ -355,6 +370,7 @@ Scenario: Disable editing assigned material - material constituent set
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I press "bim.add_material(obj='')"
@@ -368,6 +384,7 @@ Scenario: Edit assigned material - material constituent set
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I press "bim.add_material(obj='')"
@@ -419,6 +436,7 @@ Scenario: Add material set layer
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I add an empty
@@ -438,6 +456,7 @@ Scenario: Remove material set layer
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
diff --git a/src/blenderbim/test/bim/feature/misc.feature b/src/blenderbim/test/bim/feature/misc.feature
index 282ec0d16b..e20ef274a8 100644
--- a/src/blenderbim/test/bim/feature/misc.feature
+++ b/src/blenderbim/test/bim/feature/misc.feature
@@ -28,6 +28,7 @@ Scenario: Resize to storey
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
@@ -40,6 +41,7 @@ Scenario: Split along edge
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I add a plane of size "4" at "0,0,0"
diff --git a/src/blenderbim/test/bim/feature/owner.feature b/src/blenderbim/test/bim/feature/owner.feature
index 7852e2d70e..b1b96ae6e4 100644
--- a/src/blenderbim/test/bim/feature/owner.feature
+++ b/src/blenderbim/test/bim/feature/owner.feature
@@ -284,6 +284,7 @@ Scenario: Assign actor
And I press "bim.add_actor"
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the variable "actor" is "{ifc}.by_type('IfcActor')[0].id()"
@@ -297,6 +298,7 @@ Scenario: Unassign actor
And I press "bim.add_actor"
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the variable "actor" is "{ifc}.by_type('IfcActor')[0].id()"
diff --git a/src/blenderbim/test/bim/feature/project.feature b/src/blenderbim/test/bim/feature/project.feature
index d61d895b43..c427e2e877 100644
--- a/src/blenderbim/test/bim/feature/project.feature
+++ b/src/blenderbim/test/bim/feature/project.feature
@@ -436,6 +436,7 @@ Scenario: Export IFC - with changed object scale synchronised
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
@@ -450,6 +451,7 @@ Scenario: Export IFC - with changed style colour synchronised
And I add a cube
And the object "Cube" is selected
And I add a material
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
@@ -464,6 +466,7 @@ Scenario: Export IFC - with changed style element synchronised
And I add a cube
And the object "Cube" is selected
And I add a material
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
diff --git a/src/blenderbim/test/bim/feature/pset.feature b/src/blenderbim/test/bim/feature/pset.feature
index b4c87ffb1f..033c64627b 100644
--- a/src/blenderbim/test/bim/feature/pset.feature
+++ b/src/blenderbim/test/bim/feature/pset.feature
@@ -5,6 +5,7 @@ Scenario: Add pset - object
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
@@ -16,11 +17,13 @@ Scenario: Add pset - multiple objects
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
@@ -33,6 +36,7 @@ Scenario: Enable pset editing - object
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
@@ -48,6 +52,7 @@ Scenario: Enable pset editing - material
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I press "bim.add_material(obj='')"
@@ -127,6 +132,7 @@ Scenario: Disable pset editing - object
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
@@ -139,6 +145,7 @@ Scenario: Disable pset editing - material
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I press "bim.add_material(obj='')"
@@ -204,6 +211,7 @@ Scenario: Edit pset - object
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
@@ -216,6 +224,7 @@ Scenario: Edit qto - object
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
@@ -229,6 +238,7 @@ Scenario: Edit pset - material
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I press "bim.add_material(obj='')"
@@ -294,10 +304,12 @@ Scenario: Copy property to selected - copy property
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
@@ -312,6 +324,7 @@ Scenario: Remove pset - object
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
@@ -327,11 +340,13 @@ Scenario: Remove pset - multiple objects
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
diff --git a/src/blenderbim/test/bim/feature/resource.feature b/src/blenderbim/test/bim/feature/resource.feature
index 3e01cfa926..ac7522e774 100644
--- a/src/blenderbim/test/bim/feature/resource.feature
+++ b/src/blenderbim/test/bim/feature/resource.feature
@@ -274,6 +274,7 @@ Scenario: Calculate Resource Work
And I press "bim.edit_task_time"
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
@@ -307,6 +308,7 @@ Scenario: Assign Resource
And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()"
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
@@ -322,6 +324,7 @@ Scenario: UnAssign Resource
And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()"
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
diff --git a/src/blenderbim/test/bim/feature/root.feature b/src/blenderbim/test/bim/feature/root.feature
index 28b9e71726..ea2a032a27 100644
--- a/src/blenderbim/test/bim/feature/root.feature
+++ b/src/blenderbim/test/bim/feature/root.feature
@@ -5,6 +5,7 @@ Scenario: Reassign class
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class(ifc_class='IfcWall', predefined_type='SOLIDWALL')"
And I press "object.duplicate_move"
@@ -20,6 +21,7 @@ Scenario: Unlink object
And I add a cube
And the object "Cube" is selected
And I add a material
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I press "bim.add_material"
@@ -32,6 +34,7 @@ Scenario: Copy class
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
When I press "bim.copy_class(obj='IfcWall/Cube')"
@@ -41,6 +44,7 @@ Scenario: Assign a class to a cube
Given an empty IFC project
And I add a cube
When the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
Then the object "IfcWall/Cube" is an "IfcWall"
@@ -87,6 +91,7 @@ Scenario: Assign a class to a cube in a collection
And I add a cube
When the object "Cube" is selected
And the object "Cube" is placed in the collection "IfcBuildingStorey/My Storey"
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
Then the object "IfcWall/Cube" is contained in "My Storey"
@@ -95,6 +100,7 @@ Scenario: Copy a wall
Given an empty IFC project
And I add a cube
When the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I duplicate the selected objects
diff --git a/src/blenderbim/test/bim/feature/search.feature b/src/blenderbim/test/bim/feature/search.feature
index 7e1614b01a..80ec5a2c10 100644
--- a/src/blenderbim/test/bim/feature/search.feature
+++ b/src/blenderbim/test/bim/feature/search.feature
@@ -5,6 +5,7 @@ Scenario: Select all walls
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I add a new item to "scene.IfcSelectorProperties.groups"
diff --git a/src/blenderbim/test/bim/feature/sequence.feature b/src/blenderbim/test/bim/feature/sequence.feature
index 4cd0e551cb..ecbaa64063 100644
--- a/src/blenderbim/test/bim/feature/sequence.feature
+++ b/src/blenderbim/test/bim/feature/sequence.feature
@@ -349,6 +349,8 @@ Scenario: Animate the construction of a wall
And I press "bim.edit_task_time"
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
@@ -385,6 +387,7 @@ Scenario: Animate the demolition of a wall
And I press "bim.edit_task_time"
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
@@ -424,6 +427,7 @@ Scenario: Animate the operation of a wall
And I press "bim.edit_task_time"
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
@@ -458,6 +462,7 @@ Scenario: Animate the movement of a wall
And I add a cube
And I rename the object "Cube" to "ToObject"
And the object "ToObject" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/ToObject" is selected
@@ -508,6 +513,7 @@ Scenario: Animate the consumption of a wall
And I press "bim.edit_task_time"
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
@@ -549,6 +555,7 @@ Scenario: Clear Previous Animation
And I press "bim.edit_task_time"
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
@@ -758,6 +765,7 @@ Scenario: Assign Product Output
And the variable "task" is "IfcStore.get_file().by_type('IfcTask')[0].id()"
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class(ifc_class='IfcWall', predefined_type='SOLIDWALL')"
And the object "IfcWall/Cube" is selected
@@ -773,10 +781,11 @@ Scenario: Assign Product Input
And the variable "task" is "IfcStore.get_file().by_type('IfcTask')[0].id()"
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class(ifc_class='IfcWall', predefined_type='SOLIDWALL')"
And the object "IfcWall/Cube" is selected
- And I press "bim.assign_process(task={task}, related_object_type='PRODUCT')"
+ And I press "bim.assign_process(task={task},related_object=0, related_object_type='PRODUCT')"
Then nothing happens
Scenario: Select Assigned Outputs
@@ -788,9 +797,9 @@ Scenario: Select Assigned Outputs
And the variable "task" is "IfcStore.get_file().by_type('IfcTask')[0].id()"
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
- And I press "bim.assign_class(ifc_class='IfcWall', predefined_type='SOLIDWALL')"
- And I press "object.select_all(action='DESELECT')"
+ And I press "bim.assign_class()"
And I press "bim.assign_product(task={task})"
When I press "bim.select_task_related_products(task={task})"
Then nothing happens
@@ -804,9 +813,10 @@ Scenario: Select Assigned Inputs
And the variable "task" is "IfcStore.get_file().by_type('IfcTask')[0].id()"
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
- And I press "bim.assign_class(ifc_class='IfcWall', predefined_type='SOLIDWALL')"
- And I press "object.select_all(action='DESELECT')"
+ And I press "bim.assign_class()"
+ And the object "IfcWall/Cube" is selected
And I press "bim.assign_process(task={task}, related_object_type='PRODUCT')"
When I press "bim.select_task_related_products(task={task})"
Then nothing happens
@@ -849,6 +859,7 @@ Scenario: Add Animation Camera
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I press "bim.add_animation_camera"
diff --git a/src/blenderbim/test/bim/feature/spatial.feature b/src/blenderbim/test/bim/feature/spatial.feature
index acdf6ee292..03e54ef479 100644
--- a/src/blenderbim/test/bim/feature/spatial.feature
+++ b/src/blenderbim/test/bim/feature/spatial.feature
@@ -1,6 +1,6 @@
@spatial
Feature: Spatial
- Covers spatial containment management.
+ Covers spatial containment management and spatial tool.
Scenario: Enable editing container
Given an empty IFC project
@@ -21,6 +21,7 @@ Scenario: Assign container
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
@@ -33,6 +34,7 @@ Scenario: Copy to container
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
@@ -45,6 +47,7 @@ Scenario: Reference structure
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
@@ -57,6 +60,7 @@ Scenario: Dereference structure
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
@@ -70,6 +74,7 @@ Scenario: Select container
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
@@ -83,6 +88,7 @@ Scenario: Select similar container
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
@@ -91,3 +97,40 @@ Scenario: Select similar container
And I press "bim.assign_container(structure={site})"
When I press "bim.select_similar_container"
Then nothing happens
+
+#HERE STARTS TESTS FOR SPATIAL TOOL
+
+Scenario: Execute generate space from cursor position
+ Given an empty IFC project
+ When I press "bim.generate_space"
+ Then nothing happens
+
+Scenario: Execute generate spaces from walls
+ Given an empty IFC project
+ And I load the demo construction library
+ And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType"
+ And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()"
+ And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
+ And I press "bim.add_constr_type_instance"
+ And the object "IfcWall/Wall" is selected
+ When I press "bim.generate_spaces_from_walls"
+ Then nothing happens
+
+Scenario: Execute generate flooring coverings from walls
+ Given an empty IFC project
+ And I load the demo construction library
+ And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType"
+ And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()"
+ And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
+ And I press "bim.add_constr_type_instance"
+ And the object "IfcWall/Wall" is selected
+ When I press "bim.generate_flooring_coverings_from_walls"
+ Then nothing happens
+
+Scenario: Execute toggle space visibility
+ Given an empty IFC project
+ And I add a cube
+ And the object "Cube" is selected
+ And I press "bim.assign_class(ifc_class='IfcSpace', predefined_type='SPACE')"
+ When I press "bim.toggle_space_visibility"
+ Then nothing happens
diff --git a/src/blenderbim/test/bim/feature/type.feature b/src/blenderbim/test/bim/feature/type.feature
index 1e02167b6b..2e0fd06206 100644
--- a/src/blenderbim/test/bim/feature/type.feature
+++ b/src/blenderbim/test/bim/feature/type.feature
@@ -24,6 +24,7 @@ Scenario: Enable editing type
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
When I press "bim.enable_editing_type"
@@ -33,6 +34,7 @@ Scenario: Disable editing type
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I press "bim.enable_editing_type"
@@ -43,6 +45,7 @@ Scenario: Assign type - assign to an empty type
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I add an empty
@@ -58,6 +61,7 @@ Scenario: Assign type - assign to a type with representation maps
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I add a cube
@@ -73,6 +77,7 @@ Scenario: Assign type - assign to a type with a material layer set, which automa
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I add an empty
@@ -94,6 +99,7 @@ Scenario: Assign type - assign to a different type with a material layer set
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I add an empty
@@ -132,6 +138,7 @@ Scenario: Assign type - assign to a type with a material profile set
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I add an empty
@@ -155,6 +162,7 @@ Scenario: Select type objects
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I add an empty
@@ -172,10 +180,12 @@ Scenario: Select similar type
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I add an empty
diff --git a/src/blenderbim/test/bim/feature/void.feature b/src/blenderbim/test/bim/feature/void.feature
index 44691a35d1..53231ec61e 100644
--- a/src/blenderbim/test/bim/feature/void.feature
+++ b/src/blenderbim/test/bim/feature/void.feature
@@ -6,6 +6,7 @@ Scenario: Add an opening
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I add a cube
@@ -19,6 +20,7 @@ Scenario: Add an opening using the BIM tool
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I press "bim.add_potential_opening"
@@ -32,6 +34,7 @@ Scenario: Show openings
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I press "bim.add_potential_opening"
@@ -47,6 +50,7 @@ Scenario: Hide openings
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I press "bim.add_potential_opening"
@@ -62,6 +66,7 @@ Scenario: Edit openings
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I press "bim.add_potential_opening"
@@ -79,6 +84,7 @@ Scenario: Add an opening to Element B with a void that already voids Element A
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
@@ -105,6 +111,7 @@ Scenario: Remove opening
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I press "bim.add_potential_opening"
@@ -123,6 +130,7 @@ Scenario: Remove opening - using deletion
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I press "bim.add_potential_opening"
@@ -140,6 +148,7 @@ Scenario: Remove opening - indirectly by deleting its building element
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I press "bim.add_potential_opening"
diff --git a/src/blenderbim/test/core/test_misc.py b/src/blenderbim/test/core/test_misc.py
index 5f65c62867..9e420c0c23 100644
--- a/src/blenderbim/test/core/test_misc.py
+++ b/src/blenderbim/test/core/test_misc.py
@@ -39,11 +39,3 @@ class TestResizeToStorey:
misc.get_object_storey("obj").should_be_called().will_return("storey")
misc.get_storey_height_in_si("storey", 1).should_be_called().will_return(None)
subject.resize_to_storey(misc, obj="obj", total_storeys=1)
-
-
-class TestSplitAlongEdge:
- def test_run(self, misc):
- misc.split_objects_with_cutter(["obj"], "cutter").should_be_called().will_return(["new_obj"])
- misc.run_root_copy_class(obj="new_obj").should_be_called()
- misc.mark_object_as_edited("obj").should_be_called()
- subject.split_along_edge(misc, cutter="cutter", objs=["obj"])
diff --git a/src/blenderbim/test/tool/test_cad.py b/src/blenderbim/test/tool/test_cad.py
index 1aaa098fde..f359c806a9 100644
--- a/src/blenderbim/test/tool/test_cad.py
+++ b/src/blenderbim/test/tool/test_cad.py
@@ -56,3 +56,34 @@ class TestAreEdgesCollinear(NewFile):
(V(0,1,0), V(1,0,1))
)
# fmt: on
+
+
+class TestClosestPoints(NewFile):
+ def test_run(self):
+ # non collinear
+ edge1 = (V(0, 0, 0), V(1, 0, 0))
+ edge2 = (V(2, 0, 1), V(2, 0, 2))
+ assert subject.closest_points(edge1, edge2)[0] == (edge1[1], edge2[0])
+
+ # check other points
+ assert subject.closest_points(edge1, edge2)[1] == (edge1[0], edge2[1])
+
+ # collinear
+ edge1 = (V(0, 0, 0), V(1, 0, 0))
+ edge2 = (V(3, 0, 0), V(2, 0, 0))
+ assert subject.closest_points(edge1, edge2)[0] == (edge1[1], edge2[1])
+
+ # parallel
+ edge1 = (V(0, 0, 0), V(1, 0, 0))
+ edge2 = (V(-5, 0, 0), V(-1, 0, 0))
+ assert subject.closest_points(edge1, edge2)[0] == (edge1[0], edge2[1])
+
+ # overlapping
+ edge1 = (V(0, 0, 0), V(3, 0, 0))
+ edge2 = (V(2, 0, 0), V(5, 0, 0))
+ assert subject.closest_points(edge1, edge2)[0] == (edge1[1], edge2[0])
+
+ # edge as a point
+ edge1 = (V(0, 0, 0), V(0, 0, 0))
+ edge2 = (V(1, 0, 1), V(2, 0, 2))
+ assert subject.closest_points(edge1, edge2)[0] == (edge1[0], edge2[0])
diff --git a/src/blenderbim/test/tool/test_model.py b/src/blenderbim/test/tool/test_model.py
index 68c91a142e..90546c0a16 100644
--- a/src/blenderbim/test/tool/test_model.py
+++ b/src/blenderbim/test/tool/test_model.py
@@ -20,6 +20,7 @@ import bpy
import ifcopenshell
import blenderbim.core.tool
import blenderbim.tool as tool
+import numpy as np
from test.bim.bootstrap import NewFile
from blenderbim.tool.model import Model as subject
@@ -50,3 +51,31 @@ class TestGenerateOccurrenceName(NewFile):
bpy.context.scene.BIMModelProperties.occurrence_name_style = "CUSTOM"
bpy.context.scene.BIMModelProperties.occurrence_name_function = '"Foobar"'
assert subject.generate_occurrence_name(element_type, "IfcWall") == "Foobar"
+
+class TestGetManualBooleans(NewFile):
+ def test_run(self):
+ assert isinstance(subject(), blenderbim.core.tool.Model)
+
+ def test_len_returned_boolean(self):
+ ifc = ifcopenshell.file()
+ tool.Ifc.set(ifc)
+ ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject")
+ length = ifcopenshell.api.run("unit.add_si_unit", ifc, unit_type="LENGTHUNIT")
+ ifcopenshell.api.run("unit.assign_unit", ifc, units=[length])
+ ifcopenshell.api.run("unit.assign_unit", ifc)
+ element = ifc.createIfcColumn()
+ hea100 = ifc.create_entity(
+ "IfcIShapeProfileDef", ProfileName="HEA100", ProfileType="AREA",
+ OverallWidth=100, OverallDepth=96, WebThickness=5, FlangeThickness=8, FilletRadius=12,
+ )
+ model3d = ifcopenshell.api.run("context.add_context", ifc, context_type="Model")
+ body = ifcopenshell.api.run("context.add_context", ifc,context_type="Model", context_identifier="Body", target_view="MODEL_VIEW", parent=model3d)
+ representation = ifcopenshell.api.run("geometry.add_profile_representation", ifc, context=body, profile=hea100, depth=5)
+ ifcopenshell.api.run("geometry.assign_representation", ifc, product=element, representation=representation)
+ matrix = np.eye(4)
+ matrix = ifcopenshell.util.placement.rotation(45,"X") @ matrix
+ matrix[:,3][0:3] = (0, 0, 3)
+ matrix = matrix.tolist()
+ ifcopenshell.api.run("geometry.add_boolean", ifc, representation = representation, type = "IfcHalfSpaceSolid", matrix = matrix)
+ assert len(subject.get_manual_booleans(element)) == 1
+
diff --git a/src/bsdd/README.md b/src/bsdd/README.md
index 215845a321..5d92dbe9d5 100644
--- a/src/bsdd/README.md
+++ b/src/bsdd/README.md
@@ -1,21 +1,3 @@
# bsdd
-An experimental work in progress library to interact with the buildingSMART Data Dictionary (bSDD) API.
-
-More reading:
-
- * [Swagger API docs](https://bs-dd-api-prototype.azurewebsites.net/swagger/index.html)
- * [bSDD Github Repository](https://github.com/buildingSMART/bSDD)
-
-# Demo
-
-Let's replicate the SketchUp example:
-
-```
-client = Client()
-pprint(client.Domain())
-pprint(client.SearchListOpen("http://identifier.buildingsmart.org/uri/nlsfb/nlsfb2005-2.2", RelatedIfcEntity="IfcWall"))
-data = client.Classification("http://identifier.buildingsmart.org/uri/nlsfb/nlsfb2005-2.2/class/21.21")
-pprint(data)
-apply_ifc_classification_properties(ifc_file, element, data["classificationProperties"])
-```
+A library to interact with the buildingSMART Data Dictionary (bSDD) API.
diff --git a/src/bsdd/bsdd.py b/src/bsdd/bsdd.py
index 0e0f704cc5..637e36912c 100644
--- a/src/bsdd/bsdd.py
+++ b/src/bsdd/bsdd.py
@@ -52,7 +52,7 @@ class Client:
headers = {}
if is_auth_required:
headers = {"Authorization": "Bearer " + self.get_access_token()}
- return requests.get(f"{self.baseurl}{endpoint}", headers=headers, params=params or None).json()
+ return requests.get(f"{self.baseurl}{endpoint}", timeout=10, headers=headers, params=params or None).json()
def post(self):
pass # TODO
@@ -121,6 +121,20 @@ class Client:
},
)
+ def ClassificationSearchOpen(self, SearchText, version="v1", DomainNamespaceUris=None, RelatedIfcEntities=None):
+ if DomainNamespaceUris is None:
+ DomainNamespaceUris = []
+ if RelatedIfcEntities is None:
+ RelatedIfcEntities = []
+ return self.get(
+ f"api/ClassificationSearchOpen/{version}",
+ {
+ "SearchText": SearchText,
+ "DomainNamespaceUris": DomainNamespaceUris,
+ "RelatedIfcEntities": RelatedIfcEntities,
+ },
+ )
+
def Country(self, version="v1"):
return self.get(f"api/Country/{version}")
diff --git a/src/ifc4d/ifc4d/csv2ifc.py b/src/ifc4d/ifc4d/csv2ifc.py
index 7ef6a8b6d8..e60d45dd4a 100644
--- a/src/ifc4d/ifc4d/csv2ifc.py
+++ b/src/ifc4d/ifc4d/csv2ifc.py
@@ -20,6 +20,7 @@ import csv
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.unit
+import datetime
class Csv2Ifc:
@@ -28,6 +29,14 @@ class Csv2Ifc:
self.file = None
self.resources = []
self.units = {}
+ self.resource_map = {
+ "CREW": "IfcCrewResource",
+ "LABOR": "IfcLaborResource",
+ "EQUIPMENT": "IfcConstructionEquipmentResource",
+ "SUBCONTRACTOR": "IfcSubContractResource",
+ "MATERIAL": "IfcConstructionMaterialResource",
+ "PRODUCT": "IfcConstructionProductResource",
+ }
def execute(self):
self.parse_csv()
@@ -41,43 +50,46 @@ class Csv2Ifc:
for row in reader:
if not row[0]:
continue
- if row[0] == "Hierarchy":
+ if row[0] == "HIERARCHY":
for i, col in enumerate(row):
if not col:
continue
self.headers[col] = i
continue
- cost_data = self.get_row_resource_data(row)
+ resource_data = self.get_row_resource_data(row)
hierarchy_key = int(row[0])
if hierarchy_key == 1:
- self.resources.append(cost_data)
+ self.resources.append(resource_data)
else:
- self.parents[hierarchy_key - 1]["children"].append(cost_data)
- self.parents[hierarchy_key] = cost_data
+ self.parents[hierarchy_key - 1]["children"].append(resource_data)
+ self.parents[hierarchy_key] = resource_data
def get_row_resource_data(self, row):
- name = row[self.headers["Name"]]
- identification = row[self.headers["Identification"]] if "Identification" in self.headers else None
+ name = row[self.headers["ACTIVITY/RESOURCE NAME"]]
+ resource_class = self.resource_map[row[self.headers["TYPE"]]]
+ base_cost_value = row[self.headers["COST"]]
+ productivity = {}
- type = row[self.headers["Type"]]
- base_cost_value = row[self.headers["BaseCostValue"]]
- base_cost_quantity = row[self.headers["BaseCostQuantity"]]
- base_cost_unit = row[self.headers["QuantityUnit"]]
+ if resource_class in ["IfcConstructionEquipmentResource", "IfcLaborResource"]:
+ output_ratio = row[self.headers["LABOR OUTPUT"]]
+ if not output_ratio:
+ output_ratio = row[self.headers["EQUIPMENT OUTPUT"]]
+ if output_ratio:
+ time_consumed = datetime.timedelta(minutes=float(output_ratio) * 60)
+ time_consumed = ifcopenshell.util.date.datetime2ifc(time_consumed, "IfcDuration")
- productivity = {
- "BaseQuantityConsumed": row[self.headers["BaseQuantityConsumed"]],
- "BaseQuantityProducedName": row[self.headers["BaseQuantityProducedName"]],
- "BaseQuantityProducedValue": row[self.headers["BaseQuantityProducedValue"]],
- }
+ productivity = {
+ "BaseQuantityConsumed": time_consumed,
+ "BaseQuantityProducedName": row[self.headers["QUANTITY NAME"]],
+ "BaseQuantityProducedValue": 1,
+ }
return {
- "Identification": str(identification).strip() if identification else None,
"Name": str(name).strip() if name else None,
- "Type": type,
+ "Description": row[self.headers["DESCRIPTION"]],
+ "class": resource_class,
"BaseCostValue": float(base_cost_value) if base_cost_value else None,
- "BaseCostQuantity": float(base_cost_quantity) if base_cost_quantity else None,
- "Unit": str(base_cost_unit).strip() if base_cost_unit else None,
- "Productivity": productivity if productivity["BaseQuantityProducedName"] else None,
+ "Productivity": productivity,
"children": [],
}
@@ -92,45 +104,25 @@ class Csv2Ifc:
def create_resource(self, resource, parent):
if parent is None:
- resource["ifc"] = ifcopenshell.api.run("resource.add_resource", self.file, ifc_class=resource["Type"])
+ resource["ifc"] = ifcopenshell.api.run("resource.add_resource", self.file, ifc_class=resource["class"])
else:
resource["ifc"] = ifcopenshell.api.run(
- "resource.add_resource", self.file, parent_resource=parent, ifc_class=resource["Type"]
+ "resource.add_resource", self.file, parent_resource=parent, ifc_class=resource["class"]
)
resource["ifc"].Name = resource["Name"]
- resource["ifc"].Identification = resource["Identification"]
- productivity = resource["Productivity"]
- if productivity:
+ if resource.get("Description", None):
+ resource["ifc"].Description = resource.get("Description")
+ if resource["Productivity"]:
pset = ifcopenshell.api.run("pset.add_pset", self.file, product=resource["ifc"], name="EPset_Productivity")
ifcopenshell.api.run(
"pset.edit_pset",
self.file,
pset=pset,
- properties=productivity,
+ properties=resource["Productivity"],
)
if resource["BaseCostValue"]:
cost_value = ifcopenshell.api.run("cost.add_cost_value", self.file, parent=resource["ifc"])
cost_value.AppliedValue = self.file.createIfcMonetaryMeasure(resource["BaseCostValue"])
- if resource["Unit"]:
- measure_class = ifcopenshell.util.unit.get_symbol_measure_class(resource["Unit"])
- print(measure_class)
- value_component = self.file.create_entity(measure_class, resource["BaseCostQuantity"])
- print(value_component)
- unit_component = None
- if measure_class == "IfcNumericMeasure":
- unit_component = self.create_unit(resource["Unit"])
- else:
- unit_type = ifcopenshell.util.unit.get_measure_unit_type(measure_class)
- print(unit_type)
- unit_assignment = ifcopenshell.util.unit.get_unit_assignment(self.file)
- if unit_assignment:
- units = [u for u in unit_assignment.Units if getattr(u, "UnitType", None) == unit_type]
- if units:
- unit_component = units[0]
- if not unit_component:
- unit_component = self.create_unit(resource["Unit"], unit_type)
- print(unit_component)
- cost_value.UnitBasis = self.file.createIfcMeasureWithUnit(value_component, unit_component)
self.create_resources(resource["children"], resource["ifc"])
def create_unit(self, symbol, unit_type):
diff --git a/src/ifc4d/ifc4d/msp2ifc.py b/src/ifc4d/ifc4d/msp2ifc.py
index 470bcc435b..159d8279e9 100644
--- a/src/ifc4d/ifc4d/msp2ifc.py
+++ b/src/ifc4d/ifc4d/msp2ifc.py
@@ -17,7 +17,7 @@
# along with Ifc4D. If not, see .
import datetime
-from datetime import timedelta
+from datetime import timedelta, date
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.date
@@ -107,34 +107,79 @@ class MSP2Ifc:
}
def parse_calendar_xml(self, project):
+ def parse_working_times(day):
+ working_times = []
+ if day.find("pr:WorkingTimes", self.ns):
+ for working_time in day.find("pr:WorkingTimes", self.ns).findall("pr:WorkingTime", self.ns):
+ if working_time.find("pr:FromTime", self.ns) is None:
+ continue
+ working_times.append(
+ {
+ "Start": datetime.time.fromisoformat(working_time.find("pr:FromTime", self.ns).text),
+ "Finish": datetime.time.fromisoformat(working_time.find("pr:ToTime", self.ns).text),
+ }
+ )
+ return working_times
+
+ def parse_exception(exception):
+ work_times = parse_working_times(exception)
+ time_period = exception.find("pr:TimePeriod", self.ns)
+ data = {
+ "Name": exception.find("pr:Name", self.ns).text
+ if exception.find("pr:Name", self.ns) is not None
+ else None,
+ "FromDate": datetime.datetime.fromisoformat(time_period.find("pr:FromDate", self.ns).text)
+ if time_period is not None
+ else None,
+ "ToDate": datetime.datetime.fromisoformat(time_period.find("pr:ToDate", self.ns).text)
+ if time_period is not None
+ else None,
+ "Occurrences": int(exception.find("pr:Occurrences", self.ns).text)
+ if exception.find("pr:Occurrences", self.ns) is not None
+ else None,
+ "Month": exception.find("pr:Month", self.ns).text
+ if exception.find("pr:Month", self.ns) is not None
+ else None,
+ "MonthDay": exception.find("pr:MonthDay", self.ns).text
+ if exception.find("pr:MonthDay", self.ns) is not None
+ else None,
+ "Type": exception.find("pr:Type", self.ns).text
+ if exception.find("pr:Type", self.ns) is not None
+ else None,
+ "WorkingTimes": work_times,
+ "ifc": None,
+ }
+ return data
+
for calendar in project.find("pr:Calendars", self.ns).findall("pr:Calendar", self.ns):
calendar_id = calendar.find("pr:UID", self.ns).text
week_days = []
+ exceptions = []
week_days_element = calendar.find("pr:WeekDays", self.ns)
week_day_elements = week_days_element.findall("pr:WeekDay", self.ns) if week_days_element else []
for week_day in week_day_elements:
- working_times = []
if week_day.find("pr:WorkingTimes", self.ns):
- for working_time in week_day.find("pr:WorkingTimes", self.ns).findall("pr:WorkingTime", self.ns):
- if working_time.find("pr:FromTime", self.ns) is None:
- continue
- working_times.append(
+ if week_day.find("pr:DayType", self.ns).text == "0":
+ data = parse_exception(week_day)
+ data["Type"] = "2"
+ exceptions.append(data)
+ else:
+ week_days.append(
{
- "Start": datetime.time.fromisoformat(working_time.find("pr:FromTime", self.ns).text),
- "Finish": datetime.time.fromisoformat(working_time.find("pr:ToTime", self.ns).text),
+ "DayType": week_day.find("pr:DayType", self.ns).text,
+ "WorkingTimes": parse_working_times(week_day),
+ "ifc": None,
}
)
- week_days.append(
- {
- "DayType": week_day.find("pr:DayType", self.ns).text,
- "WorkingTimes": working_times,
- "ifc": None,
- }
- )
- exceptions = {}
+ exceptions_element = calendar.find("pr:Exceptions", self.ns)
+ for exception in exceptions_element.findall("pr:Exception", self.ns) if exceptions_element else []:
+ data = parse_exception(exception)
+ exceptions.append(data)
+
self.calendars[calendar_id] = {
"Name": calendar.find("pr:Name", self.ns).text,
"StandardWorkWeek": week_days,
+ "HolidayOrExceptions": exceptions,
}
def create_ifc(self):
@@ -163,9 +208,15 @@ class MSP2Ifc:
)
def create_calendars(self):
+ def has_work_or_exceptions(calendar):
+ return calendar["StandardWorkWeek"] or calendar["HolidayOrExceptions"]
+
for calendar in self.calendars.values():
+ if not has_work_or_exceptions(calendar):
+ continue
calendar["ifc"] = ifcopenshell.api.run("sequence.add_work_calendar", self.file, name=calendar["Name"])
self.process_working_week(calendar["StandardWorkWeek"], calendar["ifc"])
+ self.process_exceptions(calendar["HolidayOrExceptions"], calendar["ifc"])
def create_task(self, task, work_schedule=None, parent_task=None):
task["ifc"] = ifcopenshell.api.run(
@@ -218,13 +269,14 @@ class MSP2Ifc:
def process_working_week(self, week, calendar):
day_map = {
- "1": 7, # Sunday
- "2": 1, # Monday
- "3": 2, # Tuesday
- "4": 3, # Wednesday
- "5": 4, # Thursday
- "6": 5, # Friday
- "7": 6, # Saturday
+ "1": 7, # Sunday
+ "2": 1, # Monday
+ "3": 2, # Tuesday
+ "4": 3, # Wednesday
+ "5": 4, # Thursday
+ "6": 5, # Friday
+ "7": 6, # Saturday
+ "0": 0, # Exception
}
for day in week:
if day["ifc"]:
@@ -297,14 +349,12 @@ class MSP2Ifc:
def parse_resources_xml(self, project):
resources_lst = project.find("pr:Resources", self.ns)
resources = resources_lst.findall("pr:Resource", self.ns)
- # print("Resource text", resources[4].find("pr:Name", self.ns).text)
for resource in resources:
name = resource.find("pr:Name", self.ns)
id = resource.find("pr:ID", self.ns).text
if name is not None:
name = name.text
else:
- # print("- No Name")
name = None
self.resources[id] = {
"Name": name,
@@ -314,4 +364,62 @@ class MSP2Ifc:
"ifc": None,
"rel": None,
}
- print("Resource found", self.resources)
+
+ def process_exceptions(self, exceptions, calendar):
+ for exception in exceptions or []:
+ self.process_exception(exception, calendar)
+
+ def process_exception(self, exception, calendar):
+ if exception["ifc"] or not exception["FromDate"]:
+ return
+ exception["ifc"] = ifcopenshell.api.run(
+ "sequence.add_work_time", self.file, work_calendar=calendar, time_type="ExceptionTimes"
+ )
+ ifcopenshell.api.run(
+ "sequence.edit_work_time",
+ self.file,
+ work_time=exception["ifc"],
+ attributes={
+ "Name": exception["Name"],
+ "Start": ifcopenshell.util.date.datetime2ifc(exception["FromDate"], "IfcDate"),
+ "Finish": ifcopenshell.util.date.datetime2ifc(exception["ToDate"], "IfcDate"),
+ },
+ )
+ # BIG assumptions due to missing types enumeration in docs https://learn.microsoft.com/en-us/office-project/xml-data-interchange/exception-element?view=project-client-2016
+ recurrence_type = None
+ if exception["Type"] == "1":
+ recurrence_type = "DAILY"
+ attributes = {
+ "Occurrences": int(exception["Occurrences"]) if exception["Occurrences"] else None,
+ }
+ elif exception["Type"] == "2":
+ recurrence_type = "YEARLY_BY_DAY_OF_MONTH"
+ month_component = [int(exception["Month"]) + 1] if exception["Month"] else None
+ day_component = [int(exception["MonthDay"])] if exception["MonthDay"] else None
+ if month_component is None and (exception["FromDate"].date().day == exception["ToDate"].date().day):
+ month_component = [exception["FromDate"].date().month]
+ day_component = [exception["FromDate"].date().day]
+ attributes = {
+ "MonthComponent": month_component,
+ "DayComponent": day_component,
+ "Occurrences": int(exception["Occurrences"]) if exception["Occurrences"] else None,
+ }
+ else:
+ return
+ recurrence = ifcopenshell.api.run(
+ "sequence.assign_recurrence_pattern",
+ self.file,
+ parent=exception["ifc"],
+ recurrence_type=recurrence_type,
+ )
+ ifcopenshell.api.run(
+ "sequence.edit_recurrence_pattern", self.file, recurrence_pattern=recurrence, attributes=attributes
+ )
+ for work_time in exception["WorkingTimes"] or []:
+ ifcopenshell.api.run(
+ "sequence.add_time_period",
+ self.file,
+ recurrence_pattern=recurrence,
+ start_time=work_time["Start"],
+ end_time=work_time["Finish"],
+ )
diff --git a/src/ifc4d/ifc4d/resource_spreadsheet.csv b/src/ifc4d/ifc4d/resource_spreadsheet.csv
new file mode 100644
index 0000000000..47fea5d101
--- /dev/null
+++ b/src/ifc4d/ifc4d/resource_spreadsheet.csv
@@ -0,0 +1,63 @@
+HIERARCHY,TYPE,ACTIVITY/RESOURCE NAME,DESCRIPTION,COST,USAGE,UNIT,QUANTITY NAME,LABOR OUTPUT,EQUIPMENT OUTPUT,Productivity Unit,
+1,CREW,CONCRETE WORKS,,,,,,,,,
+2,LABOR,BEAMS,,,,,,,,,
+3,LABOR,"Beams, 745 kg/m ",3 m span,,,Cubic Meters ,,27.3,8.61,Hr / Meter,
+4,LABOR,Foreman,,25,1,Cubic Meters ,Length,27.3,,Hr / Meter,
+4,LABOR, Mason,,25,4,Cubic Meters ,Length,109.2,,Hr / Meter,
+4,LABOR,Carpenter &Steelman,,25,10,Cubic Meters ,Length,273,,Hr / Meter,
+4,LABOR,Laborer,,25,0.25,Cubic Meters ,Length,6.825,,Hr / Meter,
+4,EQUIPMENT,Crane,,25,0.125,Cubic Meters ,Length,,1.07625,Hr / Meter,
+,,,,,,,,,,,
+3,LABOR,"Beams, 745 kg/m ",7.5 m span,,,Cubic Meters ,,22.02,6.94,Hr / Meter,
+4,LABOR,Foreman,,25,1,Cubic Meters ,Length,22.02,,Hr / Meter,
+4,LABOR, Mason,,25,4,Cubic Meters ,Length,88.08,,Hr / Meter,
+4,LABOR,Carpenter &Steelman,,25,10,Cubic Meters ,Length,220.2,,Hr / Meter,
+4,LABOR,Laborer,,25,0.25,Cubic Meters ,Length,5.505,,Hr / Meter,
+4,EQUIPMENT,Crane,,25,0.125,Cubic Meters ,Length,,0.8675,Hr / Meter,
+,,,,,,,,,,,
+2,LABOR,SLABS,,,,,,,,,
+3,LABOR,In-situ,200 mm thick (concreting & finish only),,,Square Meters ,,0.22,0.2,Hr / Square Meters,
+4,LABOR,Foreman,,25,0.5,Square Meters ,GrossArea,0.11,,Hr / Square Meters,
+4,LABOR,Laborer,,25,3,Square Meters ,GrossArea,0.66,,Hr / Square Meters,
+4,LABOR,Carpenter,,25,4,Square Meters ,GrossArea,0.88,,Hr / Square Meters,
+4,LABOR,Steelfixers,,25,2.5,Square Meters ,GrossArea,0.55,,Hr / Square Meters,
+4,EQUIPMENT,Crane,,25,0.4,Square Meters ,GrossArea,,0.08,Hr / Square Meters,
+,,,,,,,,,,,
+2,LABOR,FOUNDATIONS,,,,,,,,,
+3,LABOR,Drainage,,25,,Meters,Length,3.5,,Hr / Meter,
+,,,,,,,,,,,
+,,,,,,,,,,,
+2,LABOR,WATERPROOFING,,,,,,,,,
+3,LABOR,Drainage,,25,,Square Meters ,GrossArea,0.5,,Hr / Square Meters,
+,,,,,,,,,,,
+,,,,,,,,,,,
+2,LABOR,STAIRS,,,,,,,,,
+3,LABOR,STAIRS,"300 mm wide (Incld. forms, rebar, & finish)",,,Cubic Meters ,,102.16,36.89,Hr / Cubic Meters,
+4,LABOR,Foreman,,25,0.5,Cubic Meters ,GrossVolume,51.08,,Hr / Cubic Meters,
+4,LABOR,Carpenter,,25,6,Cubic Meters ,GrossVolume,612.96,,Hr / Cubic Meters,
+4,LABOR,Steelman,,25,2,Cubic Meters ,GrossVolume,204.32,,Hr / Cubic Meters,
+4,LABOR,Mason,,25,2,Cubic Meters ,GrossVolume,204.32,,Hr / Cubic Meters,
+4,LABOR,Laborer,,25,2.5,Cubic Meters ,GrossVolume,255.4,,Hr / Cubic Meters,
+4,EQUIPMENT,Crane,,25,0.375,Cubic Meters ,GrossVolume,,13.83375,Hr / Cubic Meters,
+3,LABOR,STAIRS LANDING,"Incld. forms, rebar, & finish",,,Cubic Meters ,,12.76,4.02,Hr / Cubic Meters,
+4,LABOR,Foreman,,25,1,Cubic Meters ,GrossVolume,12.76,,Hr / Cubic Meters,
+4,LABOR,Carpenter & SteelFixers,,25,10,Cubic Meters ,GrossVolume,127.6,,Hr / Cubic Meters,
+4,LABOR,Laborer,,25,4,Cubic Meters ,GrossVolume,51.04,,Hr / Cubic Meters,
+4,LABOR,Mason,,25,0.125,Cubic Meters ,GrossVolume,1.595,,Hr / Cubic Meters,
+4,EQUIPMENT,Crane,,25,0.125,Cubic Meters ,GrossVolume,,0.5025,Hr / Cubic Meters,
+,,,,,,,,,,,
+1,CREW,MASONRY,,,,,,,,,
+2,LABOR,CONCRETE BLOCKS,,,,,,,,,
+3,LABOR,Hollow Blocks 100mm,100mm thick,,,Square Meters ,,1.17,0.29,Hr / Cubic Meters,
+4,LABOR,Foreman,,25,0.25,Square Meters ,GrossSideArea,0.2925,,Hr / Cubic Meters,
+4,LABOR,Mason,,25,4,Square Meters ,GrossSideArea,4.68,,Hr / Cubic Meters,
+4,LABOR,Laborer,,25,2,Square Meters ,GrossSideArea,2.34,,Hr / Cubic Meters,
+4,EQUIPMENT,Crane,,25,0.125,Square Meters ,GrossSideArea,,0.03625,Hr / Cubic Meters,
+4,EQUIPMENT,Fork Lift,,25,0.125,Square Meters ,GrossSideArea,,0.03625,Hr / Cubic Meters,
+4,EQUIPMENT,Truck,,25,0.125,Square Meters ,GrossSideArea,,0.03625,Hr / Cubic Meters,
+3,LABOR,Hollow Blocks 200mm,100mm thick,,,Square Meters ,,1.32,0.33,Hr / Cubic Meters,
+4,LABOR,Foreman,,25,0.25,Square Meters ,GrossSideArea,0.33,,Hr / Cubic Meters,
+4,LABOR,Mason,,25,4,Square Meters ,GrossSideArea,5.28,,Hr / Cubic Meters,
+4,LABOR,Laborer,,25,2,Square Meters ,GrossSideArea,2.64,,Hr / Cubic Meters,
+4,EQUIPMENT,Crane,,25,0.125,Square Meters ,GrossSideArea,,0.5025,Hr / Cubic Meters,
+4,EQUIPMENT,Fork Lift,,25,0.125,Square Meters ,GrossSideArea,,0.5025,Hr / Cubic Meters,
diff --git a/src/ifc4d/ifc4d/resource_spreadsheet.ods b/src/ifc4d/ifc4d/resource_spreadsheet.ods
new file mode 100644
index 0000000000..d2deaf274f
Binary files /dev/null and b/src/ifc4d/ifc4d/resource_spreadsheet.ods differ
diff --git a/src/ifccobie/COPYING b/src/ifccobie/COPYING
deleted file mode 100644
index 810fce6e9b..0000000000
--- a/src/ifccobie/COPYING
+++ /dev/null
@@ -1,621 +0,0 @@
- GNU GENERAL PUBLIC LICENSE
- Version 3, 29 June 2007
-
- Copyright (C) 2007 Free Software Foundation, Inc.
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
- Preamble
-
- The GNU General Public License is a free, copyleft license for
-software and other kinds of works.
-
- The licenses for most software and other practical works are designed
-to take away your freedom to share and change the works. By contrast,
-the GNU General Public License is intended to guarantee your freedom to
-share and change all versions of a program--to make sure it remains free
-software for all its users. We, the Free Software Foundation, use the
-GNU General Public License for most of our software; it applies also to
-any other work released this way by its authors. You can apply it to
-your programs, too.
-
- When we speak of free software, we are referring to freedom, not
-price. Our General Public Licenses are designed to make sure that you
-have the freedom to distribute copies of free software (and charge for
-them if you wish), that you receive source code or can get it if you
-want it, that you can change the software or use pieces of it in new
-free programs, and that you know you can do these things.
-
- To protect your rights, we need to prevent others from denying you
-these rights or asking you to surrender the rights. Therefore, you have
-certain responsibilities if you distribute copies of the software, or if
-you modify it: responsibilities to respect the freedom of others.
-
- For example, if you distribute copies of such a program, whether
-gratis or for a fee, you must pass on to the recipients the same
-freedoms that you received. You must make sure that they, too, receive
-or can get the source code. And you must show them these terms so they
-know their rights.
-
- Developers that use the GNU GPL protect your rights with two steps:
-(1) assert copyright on the software, and (2) offer you this License
-giving you legal permission to copy, distribute and/or modify it.
-
- For the developers' and authors' protection, the GPL clearly explains
-that there is no warranty for this free software. For both users' and
-authors' sake, the GPL requires that modified versions be marked as
-changed, so that their problems will not be attributed erroneously to
-authors of previous versions.
-
- Some devices are designed to deny users access to install or run
-modified versions of the software inside them, although the manufacturer
-can do so. This is fundamentally incompatible with the aim of
-protecting users' freedom to change the software. The systematic
-pattern of such abuse occurs in the area of products for individuals to
-use, which is precisely where it is most unacceptable. Therefore, we
-have designed this version of the GPL to prohibit the practice for those
-products. If such problems arise substantially in other domains, we
-stand ready to extend this provision to those domains in future versions
-of the GPL, as needed to protect the freedom of users.
-
- Finally, every program is threatened constantly by software patents.
-States should not allow patents to restrict development and use of
-software on general-purpose computers, but in those that do, we wish to
-avoid the special danger that patents applied to a free program could
-make it effectively proprietary. To prevent this, the GPL assures that
-patents cannot be used to render the program non-free.
-
- The precise terms and conditions for copying, distribution and
-modification follow.
-
- TERMS AND CONDITIONS
-
- 0. Definitions.
-
- "This License" refers to version 3 of the GNU General Public License.
-
- "Copyright" also means copyright-like laws that apply to other kinds of
-works, such as semiconductor masks.
-
- "The Program" refers to any copyrightable work licensed under this
-License. Each licensee is addressed as "you". "Licensees" and
-"recipients" may be individuals or organizations.
-
- To "modify" a work means to copy from or adapt all or part of the work
-in a fashion requiring copyright permission, other than the making of an
-exact copy. The resulting work is called a "modified version" of the
-earlier work or a work "based on" the earlier work.
-
- A "covered work" means either the unmodified Program or a work based
-on the Program.
-
- To "propagate" a work means to do anything with it that, without
-permission, would make you directly or secondarily liable for
-infringement under applicable copyright law, except executing it on a
-computer or modifying a private copy. Propagation includes copying,
-distribution (with or without modification), making available to the
-public, and in some countries other activities as well.
-
- To "convey" a work means any kind of propagation that enables other
-parties to make or receive copies. Mere interaction with a user through
-a computer network, with no transfer of a copy, is not conveying.
-
- An interactive user interface displays "Appropriate Legal Notices"
-to the extent that it includes a convenient and prominently visible
-feature that (1) displays an appropriate copyright notice, and (2)
-tells the user that there is no warranty for the work (except to the
-extent that warranties are provided), that licensees may convey the
-work under this License, and how to view a copy of this License. If
-the interface presents a list of user commands or options, such as a
-menu, a prominent item in the list meets this criterion.
-
- 1. Source Code.
-
- The "source code" for a work means the preferred form of the work
-for making modifications to it. "Object code" means any non-source
-form of a work.
-
- A "Standard Interface" means an interface that either is an official
-standard defined by a recognized standards body, or, in the case of
-interfaces specified for a particular programming language, one that
-is widely used among developers working in that language.
-
- The "System Libraries" of an executable work include anything, other
-than the work as a whole, that (a) is included in the normal form of
-packaging a Major Component, but which is not part of that Major
-Component, and (b) serves only to enable use of the work with that
-Major Component, or to implement a Standard Interface for which an
-implementation is available to the public in source code form. A
-"Major Component", in this context, means a major essential component
-(kernel, window system, and so on) of the specific operating system
-(if any) on which the executable work runs, or a compiler used to
-produce the work, or an object code interpreter used to run it.
-
- The "Corresponding Source" for a work in object code form means all
-the source code needed to generate, install, and (for an executable
-work) run the object code and to modify the work, including scripts to
-control those activities. However, it does not include the work's
-System Libraries, or general-purpose tools or generally available free
-programs which are used unmodified in performing those activities but
-which are not part of the work. For example, Corresponding Source
-includes interface definition files associated with source files for
-the work, and the source code for shared libraries and dynamically
-linked subprograms that the work is specifically designed to require,
-such as by intimate data communication or control flow between those
-subprograms and other parts of the work.
-
- The Corresponding Source need not include anything that users
-can regenerate automatically from other parts of the Corresponding
-Source.
-
- The Corresponding Source for a work in source code form is that
-same work.
-
- 2. Basic Permissions.
-
- All rights granted under this License are granted for the term of
-copyright on the Program, and are irrevocable provided the stated
-conditions are met. This License explicitly affirms your unlimited
-permission to run the unmodified Program. The output from running a
-covered work is covered by this License only if the output, given its
-content, constitutes a covered work. This License acknowledges your
-rights of fair use or other equivalent, as provided by copyright law.
-
- You may make, run and propagate covered works that you do not
-convey, without conditions so long as your license otherwise remains
-in force. You may convey covered works to others for the sole purpose
-of having them make modifications exclusively for you, or provide you
-with facilities for running those works, provided that you comply with
-the terms of this License in conveying all material for which you do
-not control copyright. Those thus making or running the covered works
-for you must do so exclusively on your behalf, under your direction
-and control, on terms that prohibit them from making any copies of
-your copyrighted material outside their relationship with you.
-
- Conveying under any other circumstances is permitted solely under
-the conditions stated below. Sublicensing is not allowed; section 10
-makes it unnecessary.
-
- 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
-
- No covered work shall be deemed part of an effective technological
-measure under any applicable law fulfilling obligations under article
-11 of the WIPO copyright treaty adopted on 20 December 1996, or
-similar laws prohibiting or restricting circumvention of such
-measures.
-
- When you convey a covered work, you waive any legal power to forbid
-circumvention of technological measures to the extent such circumvention
-is effected by exercising rights under this License with respect to
-the covered work, and you disclaim any intention to limit operation or
-modification of the work as a means of enforcing, against the work's
-users, your or third parties' legal rights to forbid circumvention of
-technological measures.
-
- 4. Conveying Verbatim Copies.
-
- You may convey verbatim copies of the Program's source code as you
-receive it, in any medium, provided that you conspicuously and
-appropriately publish on each copy an appropriate copyright notice;
-keep intact all notices stating that this License and any
-non-permissive terms added in accord with section 7 apply to the code;
-keep intact all notices of the absence of any warranty; and give all
-recipients a copy of this License along with the Program.
-
- You may charge any price or no price for each copy that you convey,
-and you may offer support or warranty protection for a fee.
-
- 5. Conveying Modified Source Versions.
-
- You may convey a work based on the Program, or the modifications to
-produce it from the Program, in the form of source code under the
-terms of section 4, provided that you also meet all of these conditions:
-
- a) The work must carry prominent notices stating that you modified
- it, and giving a relevant date.
-
- b) The work must carry prominent notices stating that it is
- released under this License and any conditions added under section
- 7. This requirement modifies the requirement in section 4 to
- "keep intact all notices".
-
- c) You must license the entire work, as a whole, under this
- License to anyone who comes into possession of a copy. This
- License will therefore apply, along with any applicable section 7
- additional terms, to the whole of the work, and all its parts,
- regardless of how they are packaged. This License gives no
- permission to license the work in any other way, but it does not
- invalidate such permission if you have separately received it.
-
- d) If the work has interactive user interfaces, each must display
- Appropriate Legal Notices; however, if the Program has interactive
- interfaces that do not display Appropriate Legal Notices, your
- work need not make them do so.
-
- A compilation of a covered work with other separate and independent
-works, which are not by their nature extensions of the covered work,
-and which are not combined with it such as to form a larger program,
-in or on a volume of a storage or distribution medium, is called an
-"aggregate" if the compilation and its resulting copyright are not
-used to limit the access or legal rights of the compilation's users
-beyond what the individual works permit. Inclusion of a covered work
-in an aggregate does not cause this License to apply to the other
-parts of the aggregate.
-
- 6. Conveying Non-Source Forms.
-
- You may convey a covered work in object code form under the terms
-of sections 4 and 5, provided that you also convey the
-machine-readable Corresponding Source under the terms of this License,
-in one of these ways:
-
- a) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by the
- Corresponding Source fixed on a durable physical medium
- customarily used for software interchange.
-
- b) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by a
- written offer, valid for at least three years and valid for as
- long as you offer spare parts or customer support for that product
- model, to give anyone who possesses the object code either (1) a
- copy of the Corresponding Source for all the software in the
- product that is covered by this License, on a durable physical
- medium customarily used for software interchange, for a price no
- more than your reasonable cost of physically performing this
- conveying of source, or (2) access to copy the
- Corresponding Source from a network server at no charge.
-
- c) Convey individual copies of the object code with a copy of the
- written offer to provide the Corresponding Source. This
- alternative is allowed only occasionally and noncommercially, and
- only if you received the object code with such an offer, in accord
- with subsection 6b.
-
- d) Convey the object code by offering access from a designated
- place (gratis or for a charge), and offer equivalent access to the
- Corresponding Source in the same way through the same place at no
- further charge. You need not require recipients to copy the
- Corresponding Source along with the object code. If the place to
- copy the object code is a network server, the Corresponding Source
- may be on a different server (operated by you or a third party)
- that supports equivalent copying facilities, provided you maintain
- clear directions next to the object code saying where to find the
- Corresponding Source. Regardless of what server hosts the
- Corresponding Source, you remain obligated to ensure that it is
- available for as long as needed to satisfy these requirements.
-
- e) Convey the object code using peer-to-peer transmission, provided
- you inform other peers where the object code and Corresponding
- Source of the work are being offered to the general public at no
- charge under subsection 6d.
-
- A separable portion of the object code, whose source code is excluded
-from the Corresponding Source as a System Library, need not be
-included in conveying the object code work.
-
- A "User Product" is either (1) a "consumer product", which means any
-tangible personal property which is normally used for personal, family,
-or household purposes, or (2) anything designed or sold for incorporation
-into a dwelling. In determining whether a product is a consumer product,
-doubtful cases shall be resolved in favor of coverage. For a particular
-product received by a particular user, "normally used" refers to a
-typical or common use of that class of product, regardless of the status
-of the particular user or of the way in which the particular user
-actually uses, or expects or is expected to use, the product. A product
-is a consumer product regardless of whether the product has substantial
-commercial, industrial or non-consumer uses, unless such uses represent
-the only significant mode of use of the product.
-
- "Installation Information" for a User Product means any methods,
-procedures, authorization keys, or other information required to install
-and execute modified versions of a covered work in that User Product from
-a modified version of its Corresponding Source. The information must
-suffice to ensure that the continued functioning of the modified object
-code is in no case prevented or interfered with solely because
-modification has been made.
-
- If you convey an object code work under this section in, or with, or
-specifically for use in, a User Product, and the conveying occurs as
-part of a transaction in which the right of possession and use of the
-User Product is transferred to the recipient in perpetuity or for a
-fixed term (regardless of how the transaction is characterized), the
-Corresponding Source conveyed under this section must be accompanied
-by the Installation Information. But this requirement does not apply
-if neither you nor any third party retains the ability to install
-modified object code on the User Product (for example, the work has
-been installed in ROM).
-
- The requirement to provide Installation Information does not include a
-requirement to continue to provide support service, warranty, or updates
-for a work that has been modified or installed by the recipient, or for
-the User Product in which it has been modified or installed. Access to a
-network may be denied when the modification itself materially and
-adversely affects the operation of the network or violates the rules and
-protocols for communication across the network.
-
- Corresponding Source conveyed, and Installation Information provided,
-in accord with this section must be in a format that is publicly
-documented (and with an implementation available to the public in
-source code form), and must require no special password or key for
-unpacking, reading or copying.
-
- 7. Additional Terms.
-
- "Additional permissions" are terms that supplement the terms of this
-License by making exceptions from one or more of its conditions.
-Additional permissions that are applicable to the entire Program shall
-be treated as though they were included in this License, to the extent
-that they are valid under applicable law. If additional permissions
-apply only to part of the Program, that part may be used separately
-under those permissions, but the entire Program remains governed by
-this License without regard to the additional permissions.
-
- When you convey a copy of a covered work, you may at your option
-remove any additional permissions from that copy, or from any part of
-it. (Additional permissions may be written to require their own
-removal in certain cases when you modify the work.) You may place
-additional permissions on material, added by you to a covered work,
-for which you have or can give appropriate copyright permission.
-
- Notwithstanding any other provision of this License, for material you
-add to a covered work, you may (if authorized by the copyright holders of
-that material) supplement the terms of this License with terms:
-
- a) Disclaiming warranty or limiting liability differently from the
- terms of sections 15 and 16 of this License; or
-
- b) Requiring preservation of specified reasonable legal notices or
- author attributions in that material or in the Appropriate Legal
- Notices displayed by works containing it; or
-
- c) Prohibiting misrepresentation of the origin of that material, or
- requiring that modified versions of such material be marked in
- reasonable ways as different from the original version; or
-
- d) Limiting the use for publicity purposes of names of licensors or
- authors of the material; or
-
- e) Declining to grant rights under trademark law for use of some
- trade names, trademarks, or service marks; or
-
- f) Requiring indemnification of licensors and authors of that
- material by anyone who conveys the material (or modified versions of
- it) with contractual assumptions of liability to the recipient, for
- any liability that these contractual assumptions directly impose on
- those licensors and authors.
-
- All other non-permissive additional terms are considered "further
-restrictions" within the meaning of section 10. If the Program as you
-received it, or any part of it, contains a notice stating that it is
-governed by this License along with a term that is a further
-restriction, you may remove that term. If a license document contains
-a further restriction but permits relicensing or conveying under this
-License, you may add to a covered work material governed by the terms
-of that license document, provided that the further restriction does
-not survive such relicensing or conveying.
-
- If you add terms to a covered work in accord with this section, you
-must place, in the relevant source files, a statement of the
-additional terms that apply to those files, or a notice indicating
-where to find the applicable terms.
-
- Additional terms, permissive or non-permissive, may be stated in the
-form of a separately written license, or stated as exceptions;
-the above requirements apply either way.
-
- 8. Termination.
-
- You may not propagate or modify a covered work except as expressly
-provided under this License. Any attempt otherwise to propagate or
-modify it is void, and will automatically terminate your rights under
-this License (including any patent licenses granted under the third
-paragraph of section 11).
-
- However, if you cease all violation of this License, then your
-license from a particular copyright holder is reinstated (a)
-provisionally, unless and until the copyright holder explicitly and
-finally terminates your license, and (b) permanently, if the copyright
-holder fails to notify you of the violation by some reasonable means
-prior to 60 days after the cessation.
-
- Moreover, your license from a particular copyright holder is
-reinstated permanently if the copyright holder notifies you of the
-violation by some reasonable means, this is the first time you have
-received notice of violation of this License (for any work) from that
-copyright holder, and you cure the violation prior to 30 days after
-your receipt of the notice.
-
- Termination of your rights under this section does not terminate the
-licenses of parties who have received copies or rights from you under
-this License. If your rights have been terminated and not permanently
-reinstated, you do not qualify to receive new licenses for the same
-material under section 10.
-
- 9. Acceptance Not Required for Having Copies.
-
- You are not required to accept this License in order to receive or
-run a copy of the Program. Ancillary propagation of a covered work
-occurring solely as a consequence of using peer-to-peer transmission
-to receive a copy likewise does not require acceptance. However,
-nothing other than this License grants you permission to propagate or
-modify any covered work. These actions infringe copyright if you do
-not accept this License. Therefore, by modifying or propagating a
-covered work, you indicate your acceptance of this License to do so.
-
- 10. Automatic Licensing of Downstream Recipients.
-
- Each time you convey a covered work, the recipient automatically
-receives a license from the original licensors, to run, modify and
-propagate that work, subject to this License. You are not responsible
-for enforcing compliance by third parties with this License.
-
- An "entity transaction" is a transaction transferring control of an
-organization, or substantially all assets of one, or subdividing an
-organization, or merging organizations. If propagation of a covered
-work results from an entity transaction, each party to that
-transaction who receives a copy of the work also receives whatever
-licenses to the work the party's predecessor in interest had or could
-give under the previous paragraph, plus a right to possession of the
-Corresponding Source of the work from the predecessor in interest, if
-the predecessor has it or can get it with reasonable efforts.
-
- You may not impose any further restrictions on the exercise of the
-rights granted or affirmed under this License. For example, you may
-not impose a license fee, royalty, or other charge for exercise of
-rights granted under this License, and you may not initiate litigation
-(including a cross-claim or counterclaim in a lawsuit) alleging that
-any patent claim is infringed by making, using, selling, offering for
-sale, or importing the Program or any portion of it.
-
- 11. Patents.
-
- A "contributor" is a copyright holder who authorizes use under this
-License of the Program or a work on which the Program is based. The
-work thus licensed is called the contributor's "contributor version".
-
- A contributor's "essential patent claims" are all patent claims
-owned or controlled by the contributor, whether already acquired or
-hereafter acquired, that would be infringed by some manner, permitted
-by this License, of making, using, or selling its contributor version,
-but do not include claims that would be infringed only as a
-consequence of further modification of the contributor version. For
-purposes of this definition, "control" includes the right to grant
-patent sublicenses in a manner consistent with the requirements of
-this License.
-
- Each contributor grants you a non-exclusive, worldwide, royalty-free
-patent license under the contributor's essential patent claims, to
-make, use, sell, offer for sale, import and otherwise run, modify and
-propagate the contents of its contributor version.
-
- In the following three paragraphs, a "patent license" is any express
-agreement or commitment, however denominated, not to enforce a patent
-(such as an express permission to practice a patent or covenant not to
-sue for patent infringement). To "grant" such a patent license to a
-party means to make such an agreement or commitment not to enforce a
-patent against the party.
-
- If you convey a covered work, knowingly relying on a patent license,
-and the Corresponding Source of the work is not available for anyone
-to copy, free of charge and under the terms of this License, through a
-publicly available network server or other readily accessible means,
-then you must either (1) cause the Corresponding Source to be so
-available, or (2) arrange to deprive yourself of the benefit of the
-patent license for this particular work, or (3) arrange, in a manner
-consistent with the requirements of this License, to extend the patent
-license to downstream recipients. "Knowingly relying" means you have
-actual knowledge that, but for the patent license, your conveying the
-covered work in a country, or your recipient's use of the covered work
-in a country, would infringe one or more identifiable patents in that
-country that you have reason to believe are valid.
-
- If, pursuant to or in connection with a single transaction or
-arrangement, you convey, or propagate by procuring conveyance of, a
-covered work, and grant a patent license to some of the parties
-receiving the covered work authorizing them to use, propagate, modify
-or convey a specific copy of the covered work, then the patent license
-you grant is automatically extended to all recipients of the covered
-work and works based on it.
-
- A patent license is "discriminatory" if it does not include within
-the scope of its coverage, prohibits the exercise of, or is
-conditioned on the non-exercise of one or more of the rights that are
-specifically granted under this License. You may not convey a covered
-work if you are a party to an arrangement with a third party that is
-in the business of distributing software, under which you make payment
-to the third party based on the extent of your activity of conveying
-the work, and under which the third party grants, to any of the
-parties who would receive the covered work from you, a discriminatory
-patent license (a) in connection with copies of the covered work
-conveyed by you (or copies made from those copies), or (b) primarily
-for and in connection with specific products or compilations that
-contain the covered work, unless you entered into that arrangement,
-or that patent license was granted, prior to 28 March 2007.
-
- Nothing in this License shall be construed as excluding or limiting
-any implied license or other defenses to infringement that may
-otherwise be available to you under applicable patent law.
-
- 12. No Surrender of Others' Freedom.
-
- If conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License. If you cannot convey a
-covered work so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you may
-not convey it at all. For example, if you agree to terms that obligate you
-to collect a royalty for further conveying from those to whom you convey
-the Program, the only way you could satisfy both those terms and this
-License would be to refrain entirely from conveying the Program.
-
- 13. Use with the GNU Affero General Public License.
-
- Notwithstanding any other provision of this License, you have
-permission to link or combine any covered work with a work licensed
-under version 3 of the GNU Affero General Public License into a single
-combined work, and to convey the resulting work. The terms of this
-License will continue to apply to the part which is the covered work,
-but the special requirements of the GNU Affero General Public License,
-section 13, concerning interaction through a network will apply to the
-combination as such.
-
- 14. Revised Versions of this License.
-
- The Free Software Foundation may publish revised and/or new versions of
-the GNU General Public License from time to time. Such new versions will
-be similar in spirit to the present version, but may differ in detail to
-address new problems or concerns.
-
- Each version is given a distinguishing version number. If the
-Program specifies that a certain numbered version of the GNU General
-Public License "or any later version" applies to it, you have the
-option of following the terms and conditions either of that numbered
-version or of any later version published by the Free Software
-Foundation. If the Program does not specify a version number of the
-GNU General Public License, you may choose any version ever published
-by the Free Software Foundation.
-
- If the Program specifies that a proxy can decide which future
-versions of the GNU General Public License can be used, that proxy's
-public statement of acceptance of a version permanently authorizes you
-to choose that version for the Program.
-
- Later license versions may give you additional or different
-permissions. However, no additional obligations are imposed on any
-author or copyright holder as a result of your choosing to follow a
-later version.
-
- 15. Disclaimer of Warranty.
-
- THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
-APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
-HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
-OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
-THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
-PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
-IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
-ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
-
- 16. Limitation of Liability.
-
- IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
-THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
-GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
-USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
-DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
-PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
-EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
-SUCH DAMAGES.
-
- 17. Interpretation of Sections 15 and 16.
-
- If the disclaimer of warranty and limitation of liability provided
-above cannot be given local legal effect according to their terms,
-reviewing courts shall apply local law that most closely approximates
-an absolute waiver of all civil liability in connection with the
-Program, unless a warranty or assumption of liability accompanies a
-copy of the Program in return for a fee.
-
- END OF TERMS AND CONDITIONS
diff --git a/src/ifccobie/COPYING.LESSER b/src/ifccobie/COPYING.LESSER
deleted file mode 100644
index 0a041280bd..0000000000
--- a/src/ifccobie/COPYING.LESSER
+++ /dev/null
@@ -1,165 +0,0 @@
- GNU LESSER GENERAL PUBLIC LICENSE
- Version 3, 29 June 2007
-
- Copyright (C) 2007 Free Software Foundation, Inc.
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
-
- This version of the GNU Lesser General Public License incorporates
-the terms and conditions of version 3 of the GNU General Public
-License, supplemented by the additional permissions listed below.
-
- 0. Additional Definitions.
-
- As used herein, "this License" refers to version 3 of the GNU Lesser
-General Public License, and the "GNU GPL" refers to version 3 of the GNU
-General Public License.
-
- "The Library" refers to a covered work governed by this License,
-other than an Application or a Combined Work as defined below.
-
- An "Application" is any work that makes use of an interface provided
-by the Library, but which is not otherwise based on the Library.
-Defining a subclass of a class defined by the Library is deemed a mode
-of using an interface provided by the Library.
-
- A "Combined Work" is a work produced by combining or linking an
-Application with the Library. The particular version of the Library
-with which the Combined Work was made is also called the "Linked
-Version".
-
- The "Minimal Corresponding Source" for a Combined Work means the
-Corresponding Source for the Combined Work, excluding any source code
-for portions of the Combined Work that, considered in isolation, are
-based on the Application, and not on the Linked Version.
-
- The "Corresponding Application Code" for a Combined Work means the
-object code and/or source code for the Application, including any data
-and utility programs needed for reproducing the Combined Work from the
-Application, but excluding the System Libraries of the Combined Work.
-
- 1. Exception to Section 3 of the GNU GPL.
-
- You may convey a covered work under sections 3 and 4 of this License
-without being bound by section 3 of the GNU GPL.
-
- 2. Conveying Modified Versions.
-
- If you modify a copy of the Library, and, in your modifications, a
-facility refers to a function or data to be supplied by an Application
-that uses the facility (other than as an argument passed when the
-facility is invoked), then you may convey a copy of the modified
-version:
-
- a) under this License, provided that you make a good faith effort to
- ensure that, in the event an Application does not supply the
- function or data, the facility still operates, and performs
- whatever part of its purpose remains meaningful, or
-
- b) under the GNU GPL, with none of the additional permissions of
- this License applicable to that copy.
-
- 3. Object Code Incorporating Material from Library Header Files.
-
- The object code form of an Application may incorporate material from
-a header file that is part of the Library. You may convey such object
-code under terms of your choice, provided that, if the incorporated
-material is not limited to numerical parameters, data structure
-layouts and accessors, or small macros, inline functions and templates
-(ten or fewer lines in length), you do both of the following:
-
- a) Give prominent notice with each copy of the object code that the
- Library is used in it and that the Library and its use are
- covered by this License.
-
- b) Accompany the object code with a copy of the GNU GPL and this license
- document.
-
- 4. Combined Works.
-
- You may convey a Combined Work under terms of your choice that,
-taken together, effectively do not restrict modification of the
-portions of the Library contained in the Combined Work and reverse
-engineering for debugging such modifications, if you also do each of
-the following:
-
- a) Give prominent notice with each copy of the Combined Work that
- the Library is used in it and that the Library and its use are
- covered by this License.
-
- b) Accompany the Combined Work with a copy of the GNU GPL and this license
- document.
-
- c) For a Combined Work that displays copyright notices during
- execution, include the copyright notice for the Library among
- these notices, as well as a reference directing the user to the
- copies of the GNU GPL and this license document.
-
- d) Do one of the following:
-
- 0) Convey the Minimal Corresponding Source under the terms of this
- License, and the Corresponding Application Code in a form
- suitable for, and under terms that permit, the user to
- recombine or relink the Application with a modified version of
- the Linked Version to produce a modified Combined Work, in the
- manner specified by section 6 of the GNU GPL for conveying
- Corresponding Source.
-
- 1) Use a suitable shared library mechanism for linking with the
- Library. A suitable mechanism is one that (a) uses at run time
- a copy of the Library already present on the user's computer
- system, and (b) will operate properly with a modified version
- of the Library that is interface-compatible with the Linked
- Version.
-
- e) Provide Installation Information, but only if you would otherwise
- be required to provide such information under section 6 of the
- GNU GPL, and only to the extent that such information is
- necessary to install and execute a modified version of the
- Combined Work produced by recombining or relinking the
- Application with a modified version of the Linked Version. (If
- you use option 4d0, the Installation Information must accompany
- the Minimal Corresponding Source and Corresponding Application
- Code. If you use option 4d1, you must provide the Installation
- Information in the manner specified by section 6 of the GNU GPL
- for conveying Corresponding Source.)
-
- 5. Combined Libraries.
-
- You may place library facilities that are a work based on the
-Library side by side in a single library together with other library
-facilities that are not Applications and are not covered by this
-License, and convey such a combined library under terms of your
-choice, if you do both of the following:
-
- a) Accompany the combined library with a copy of the same work based
- on the Library, uncombined with any other library facilities,
- conveyed under the terms of this License.
-
- b) Give prominent notice with the combined library that part of it
- is a work based on the Library, and explaining where to find the
- accompanying uncombined form of the same work.
-
- 6. Revised Versions of the GNU Lesser General Public License.
-
- The Free Software Foundation may publish revised and/or new versions
-of the GNU Lesser General Public License from time to time. Such new
-versions will be similar in spirit to the present version, but may
-differ in detail to address new problems or concerns.
-
- Each version is given a distinguishing version number. If the
-Library as you received it specifies that a certain numbered version
-of the GNU Lesser General Public License "or any later version"
-applies to it, you have the option of following the terms and
-conditions either of that published version or of any later version
-published by the Free Software Foundation. If the Library as you
-received it does not specify a version number of the GNU Lesser
-General Public License, you may choose any version of the GNU Lesser
-General Public License ever published by the Free Software Foundation.
-
- If the Library as you received it specifies that a proxy can decide
-whether future versions of the GNU Lesser General Public License shall
-apply, that proxy's public statement of acceptance of any version is
-permanent authorization for you to choose that version for the
-Library.
diff --git a/src/ifccobie/cobie.py b/src/ifccobie/cobie.py
deleted file mode 100755
index c5cde601eb..0000000000
--- a/src/ifccobie/cobie.py
+++ /dev/null
@@ -1,1667 +0,0 @@
-#!/usr/bin/env python3
-
-# IfcCOBie - Extract COBie data from IFC to spreadsheets
-# Copyright (C) 2019, 2020, 2021 Dion Moult
-#
-# This file is part of IfcCOBie.
-#
-# IfcCOBie is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# IfcCOBie 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 Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with IfcCOBie. If not, see .
-
-# This can be packaged with `pyinstaller --onefile --clean --icon=icon.ico bimtester.py`
-
-import os
-import time
-import argparse
-import datetime
-import logging
-import ifcopenshell
-import ifcopenshell.util.selector
-import ifcopenshell.util.placement
-
-
-class IfcCobieParser:
- def __init__(self, logger, selector):
- self.selector = selector
- self.logger = logger
- self.file = None
- self.sheets = [
- "contacts",
- "facilities",
- "floors",
- "spaces",
- "zones",
- "types",
- "components",
- "systems",
- "assemblies",
- "connections",
- "spares",
- "resources",
- "jobs",
- "impacts",
- "documents",
- "attributes",
- "coordinates",
- "issues",
- ]
- for sheet in self.sheets:
- setattr(self, sheet, {})
- self.picklists = {
- "Category-Role": [],
- "Category-Facility": [],
- "FloorType": [],
- "Category-Space": [],
- "ZoneType": [],
- "Category-Product": [],
- "AssetType": [],
- "DurationUnit": ["day"], # See note about hardcoded day below
- "Category-Element": [],
- "SpareType": [],
- "ApprovalBy": [],
- "StageType": [],
- "objType": [],
- }
- self.default_date = (datetime.datetime(1970, 1, 1) + datetime.timedelta(seconds=-2177452801)).isoformat()
-
- def parse(self, file, type_query=".COBieType", component_query=".COBie", custom_data={}):
- self.custom_data = custom_data
- for sheet in self.sheets:
- if sheet not in self.custom_data:
- self.custom_data[sheet] = {}
-
- if isinstance(file, str):
- self.file = ifcopenshell.open(file)
- else:
- self.file = file
-
- self.type_assets = self.selector.parse(self.file, type_query)
- self.component_assets = self.selector.parse(self.file, component_query)
- self.get_contacts()
- self.get_facilities()
- self.get_floors()
- self.get_spaces()
- self.get_zones()
- self.get_types()
- self.get_components()
- self.get_systems()
- self.get_assemblies()
- self.get_connections()
- self.get_spares()
- self.get_resources()
- self.get_jobs()
- self.get_impacts()
- self.get_documents()
- self.get_attributes()
- self.get_coordinates()
- self.get_issues()
-
- def get_contacts(self):
- histories = self.file.by_type("IfcOwnerHistory")
- for history in histories:
- email = self.get_email_from_history(history)
- if not email:
- continue
- postal_address = self.get_postal_address_from_history(history)
- self.contacts[email] = {
- "CreatedBy": email,
- "CreatedOn": datetime.datetime.fromtimestamp(history.CreationDate).isoformat()
- if history.CreationDate
- else datetime.datetime.now().isoformat(),
- "Category": self.get_category_from_history(history),
- "Company": history.OwningUser.TheOrganization.Name or "n/a",
- "Phone": self.get_phone_from_history(history),
- "ExtSystem": self.get_ext_system_from_history(history),
- "ExtObject": self.get_ext_object_from_history(history),
- "ExtIdentifier": history.OwningUser.ThePerson.Id
- if self.file.schema == "IFC2X3"
- else history.OwningUser.ThePerson.Identification,
- "Department": self.get_department_from_history(history),
- "OrganizationCode": (history.OwningUser.TheOrganization.Id or "n/a")
- if self.file.schema == "IFC2X3"
- else (history.OwningUser.TheOrganization.Identification or "n/a"),
- "GivenName": self.get_name_from_person(history.OwningUser.ThePerson, "GivenName"),
- "FamilyName": self.get_name_from_person(history.OwningUser.ThePerson, "FamilyName"),
- "Street": self.get_lines_from_address(postal_address),
- "PostalBox": self.get_attribute_from_address(postal_address, "PostalBox"),
- "Town": self.get_attribute_from_address(postal_address, "Town"),
- "StateRegion": self.get_attribute_from_address(postal_address, "Region"),
- "PostalCode": self.get_attribute_from_address(postal_address, "PostalCode"),
- "Country": self.get_attribute_from_address(postal_address, "Country"),
- }
- for field, key in self.custom_data["contacts"].items():
- self.contacts[email][field] = self.get_element_value(history, key)
-
- def get_facilities(self):
- buildings = self.file.by_type("IfcBuilding")
- for building in buildings:
- building_name = self.get_object_name(building)
- units = self.get_units_from_building(building)
- self.facilities[building_name] = {
- "CreatedBy": self.get_email_from_history(building.OwnerHistory),
- "CreatedOn": self.get_created_on_from_history(building.OwnerHistory),
- "Category": self.get_category_from_object(building, "Category-Facility"),
- "ProjectName": self.get_project_name_from_building(building),
- "SiteName": self.get_site_name_from_building(building),
- "LinearUnits": self.get_unit_type_from_units(units, "LENGTHUNIT"),
- "AreaUnits": self.get_unit_type_from_units(units, "AREAUNIT"),
- "VolumeUnits": self.get_unit_type_from_units(units, "VOLUMEUNIT"),
- "CostUnit": self.get_monetary_unit_from_units(units),
- "AreaMeasurement": self.get_area_measurement_from_building(building),
- "ExternalSystem": self.get_ext_system_from_history(building.OwnerHistory),
- "ExternalProjectObject": self.get_ext_project_object(),
- "ExternalProjectIdentifier": self.get_project_globalid_from_building(building),
- "ExternalSiteObject": self.get_ext_site_object(),
- "ExternalSiteIdentifier": self.get_site_globalid_from_building(building),
- "ExternalFacilityObject": self.get_ext_object(building),
- "ExternalFacilityIdentifier": building.GlobalId,
- "Description": self.get_object_attribute(building, "Description", default="n/a"),
- "ProjectDescription": self.get_object_attribute(
- self.get_parent_spatial_element(building, "IfcProject"), "Description", default="n/a"
- ),
- "SiteDescription": self.get_object_attribute(
- self.get_parent_spatial_element(building, "IfcSite"), "Description", default="n/a"
- ),
- "Phase": self.get_object_attribute(
- self.get_parent_spatial_element(building, "IfcProject"), "Phase", default="n/a"
- ),
- }
- for field, key in self.custom_data["facilities"].items():
- self.facilities[building_name][field] = self.get_element_value(building, key)
-
- def get_floors(self):
- storeys = self.file.by_type("IfcBuildingStorey")
- for storey in storeys:
- storey_name = self.get_object_name(storey)
- self.floors[storey_name] = {
- "CreatedBy": self.get_email_from_history(storey.OwnerHistory),
- "CreatedOn": self.get_created_on_from_history(storey.OwnerHistory),
- "Category": self.get_category_from_object(storey, "FloorType"),
- "ExtSystem": self.get_ext_system_from_history(storey.OwnerHistory),
- "ExtObject": self.get_ext_object(storey),
- "ExtIdentifier": storey.GlobalId,
- "Description": self.get_object_attribute(storey, "Description", default="n/a"),
- "Elevation": self.get_object_attribute(storey, "Elevation", default="n/a"),
- "Height": self.get_height_from_storey(storey),
- }
- for field, key in self.custom_data["floors"].items():
- self.floors[storey_name][field] = self.get_element_value(storey, key)
-
- def get_spaces(self):
- spaces = self.file.by_type("IfcSpace")
- for space in spaces:
- space_name = self.get_object_name(space)
- self.spaces[space_name] = {
- "CreatedBy": self.get_email_from_history(space.OwnerHistory),
- "CreatedOn": self.get_created_on_from_history(space.OwnerHistory),
- "Category": self.get_category_from_object(space, "Category-Space"),
- "FloorName": self.get_object_attribute(
- self.get_parent_spatial_element(space, "IfcBuildingStorey"),
- "Name",
- is_primary_key=True,
- default="n/a",
- ),
- "Description": self.get_object_attribute(space, "Description", default="n/a"),
- "ExtSystem": self.get_ext_system_from_history(space.OwnerHistory),
- "ExtObject": self.get_ext_object(space),
- "ExtIdentifier": space.GlobalId,
- "RoomTag": self.get_pset_value_from_object(space, "COBie_Space", "RoomTag", "n/a"),
- "UsableHeight": self.get_usable_height_from_space(space),
- "GrossArea": self.get_gross_area_from_space(space),
- "NetArea": self.get_net_area_from_space(space),
- }
- for field, key in self.custom_data["spaces"].items():
- self.spaces[space_name][field] = self.get_element_value(space, key)
-
- def get_zones(self):
- zones = self.file.by_type("IfcZone")
- for zone in zones:
- zone_name = self.get_object_name(zone)
- self.zones[zone_name] = {
- "CreatedBy": self.get_email_from_history(zone.OwnerHistory),
- "CreatedOn": self.get_created_on_from_history(zone.OwnerHistory),
- "Category": self.get_category_from_object(zone, "ZoneType"),
- "SpaceNames": self.get_grouped_product_names_from_object(zone, "IfcSpace"),
- "ExtSystem": self.get_ext_system_from_history(zone.OwnerHistory),
- "ExtObject": self.get_ext_object(zone),
- "ExtIdentifier": zone.GlobalId,
- "Description": self.get_object_attribute(zone, "Description", default="n/a"),
- }
- for field, key in self.custom_data["zones"].items():
- self.zones[zone_name][field] = self.get_element_value(zone, key)
-
- def get_types(self):
- types = self.file.by_type("IfcTypeObject")
- for type in self.type_assets:
- # The responsibility matrix states to parse IfcMaterial and
- # IfcMaterialLayerSet too, but it doesn't make much sense, so I
- # don't parse it.
- type_name = self.get_object_name(type)
- self.types[type_name] = {
- "CreatedBy": self.get_email_from_history(type.OwnerHistory),
- "CreatedOn": self.get_created_on_from_history(type.OwnerHistory),
- "Category": self.get_category_from_object(type, "Category-Product"),
- # The responsibility matrix states two possible fallbacks. I
- # choose the 'n/a' option as opposed to repeating the name.
- "Description": self.get_object_attribute(type, "Description", default="n/a"),
- "AssetType": self.get_pset_value_from_object(type, "COBie_Asset", "AssetType", "n/a", "AssetType"),
- "Manufacturer": self.get_contact_pset_value_from_object(
- type, "Pset_ManufacturerTypeInformation", "Manufacturer"
- ),
- "ModelNumber": self.get_pset_value_from_object(
- type, "Pset_ManufacturerTypeInformation", "ModelLabel", "n/a"
- ),
- # The responsibility matrix talks about using the Pset_Warranty
- # values, but Pset_Warranty only applies to objects, not types,
- # and so they are ignored.
- "WarrantyGuarantorParts": self.get_contact_pset_value_from_object(
- type, "COBie_Warranty", "WarrantyGuarantorParts"
- ),
- "WarrantyDurationParts": self.get_pset_value_from_object(
- type, "COBie_Warranty", "WarrantyDurationParts", 0
- ),
- "WarrantyGuarantorLabor": self.get_contact_pset_value_from_object(
- type, "COBie_Warranty", "WarrantyGuarantorLabor"
- ),
- "WarrantyDurationLabor": self.get_pset_value_from_object(
- type, "COBie_Warranty", "WarrantyDurationLabor", 0
- ),
- # TODO: this may be derived from the duration values above, but
- # until it is clarified, it will be hardcoded as 'day'
- "WarrantyDurationUnit": "day",
- "ExtSystem": self.get_ext_system_from_history(type.OwnerHistory),
- "ExtObject": self.get_ext_object(type),
- "ExtIdentifier": type.GlobalId,
- "ReplacementCost": self.get_pset_value_from_object(
- type, "COBie_EconomicImpactValues", "ReplacementCost", "n/a"
- ),
- "ExpectedLife": self.get_expected_life_from_type(type),
- # See note about WarrantyDurationUnit above
- "DurationUnit": "day",
- "NominalLength": self.get_pset_value_from_object(type, "COBie_Specification", "NominalLength", 0),
- "NominalWidth": self.get_pset_value_from_object(type, "COBie_Specification", "NominalWidth", 0),
- "NominalHeight": self.get_pset_value_from_object(type, "COBie_Specification", "NominalHeight", 0),
- "ModelReference": self.get_pset_value_from_object(
- type, "Pset_ManufacturerTypeInformation", "ModelReference", "n/a"
- ),
- "Shape": self.get_pset_value_from_object(type, "COBie_Specification", "Shape", "n/a"),
- "Size": self.get_pset_value_from_object(type, "COBie_Specification", "Size", "n/a"),
- # The responsbility matrix allows the British spelling of
- # "colour". I, however, do not.
- "Color": self.get_pset_value_from_object(type, "COBie_Specification", "Color", "n/a"),
- "Finish": self.get_pset_value_from_object(type, "COBie_Specification", "Finish", "n/a"),
- "Grade": self.get_pset_value_from_object(type, "COBie_Specification", "Grade", "n/a"),
- "Material": self.get_pset_value_from_object(type, "COBie_Specification", "Material", "n/a"),
- "Constituents": self.get_pset_value_from_object(type, "COBie_Specification", "Constituents", "n/a"),
- "Features": self.get_pset_value_from_object(type, "COBie_Specification", "Features", "n/a"),
- "AccessibilityPerformance": self.get_pset_value_from_object(
- type, "COBie_Specification", "AccessibilityPerformance", "n/a"
- ),
- "CodePerformance": self.get_pset_value_from_object(
- type, "COBie_Specification", "CodePerformance", "n/a"
- ),
- "SustainabilityPerformance": self.get_pset_value_from_object(
- type, "COBie_Specification", "SustainabilityPerformance", "n/a"
- ),
- }
- for field, key in self.custom_data["types"].items():
- self.types[type_name][field] = self.get_element_value(type, key)
-
- def get_components(self):
- components = self.file.by_type("IfcElement")
- for component in components:
- if not self.is_object_a_component_asset(component):
- self.logger.warning("A component which is not an asset was found for %s", component)
- continue
- component_name = self.get_object_name(component)
- self.components[component_name] = {
- "CreatedBy": self.get_email_from_history(component.OwnerHistory),
- "CreatedOn": self.get_created_on_from_history(component.OwnerHistory),
- "TypeName": self.get_type_name_from_object(component),
- "Space": self.get_space_name_from_component(component),
- "Description": self.get_object_attribute(component, "Description", default="n/a"),
- "ExtSystem": self.get_ext_system_from_history(component.OwnerHistory),
- "ExtObject": self.get_ext_object(component),
- "ExtIdentifier": component.GlobalId,
- "SerialNumber": self.get_pset_value_from_object(
- component, "Pset_ManufacturerOccurence", "SerialNumber", "n/a"
- ),
- "InstallationDate": self.get_pset_value_from_object(
- component, "COBie_Component", "InstallationDate", self.default_date
- ),
- "WarrantyStartDate": self.get_pset_value_from_object(
- component, "COBie_Component", "WarrantyStartDate", self.default_date
- ),
- "TagNumber": self.get_pset_value_from_object(component, "COBie_Component", "TagNumber", "n/a"),
- "BarCode": self.get_pset_value_from_object(component, "Pset_ManufacturerOccurence", "BarCode", "n/a"),
- "AssetIdentifier": self.get_pset_value_from_object(
- component, "COBie_Component", "AssetIdentifier", "n/a"
- ),
- }
- for field, key in self.custom_data["components"].items():
- self.components[component_name][field] = self.get_element_value(component, key)
-
- def get_systems(self):
- systems = self.file.by_type("IfcSystem")
- for system in systems:
- system_name = self.get_object_name(system)
- self.systems[system_name] = {
- "CreatedBy": self.get_email_from_history(system.OwnerHistory),
- "CreatedOn": self.get_created_on_from_history(system.OwnerHistory),
- "Category": self.get_category_from_object(system, "Category-Element"),
- "ComponentNames": self.get_grouped_product_names_from_object(system, "IfcProduct"),
- "ExtSystem": self.get_ext_system_from_history(system.OwnerHistory),
- "ExtObject": self.get_ext_object(system),
- "ExtIdentifier": system.GlobalId,
- "Description": self.get_object_attribute(system, "Description", default="n/a"),
- }
- for field, key in self.custom_data["systems"].items():
- self.systems[system_name][field] = self.get_element_value(system, key)
-
- def get_assemblies(self):
- assemblies = self.file.by_type("IfcRelAggregates")
- for assembly in assemblies:
- assembly_name = self.get_object_name(assembly)
- if not self.is_object_a_component_asset(assembly.RelatingObject):
- continue
- self.assemblies[assembly_name] = {
- "CreatedBy": self.get_email_from_history(assembly.OwnerHistory),
- "CreatedOn": self.get_created_on_from_history(assembly.OwnerHistory),
- "SheetName": "Assembly",
- "ParentName": self.get_object_name(assembly.RelatingObject),
- "ChildNames": ",".join([o.Name if o.Name else "" for o in assembly.RelatedObjects]),
- "AssemblyType": "n/a", # I don't understand this field
- "ExtSystem": self.get_ext_system_from_history(assembly.OwnerHistory),
- "ExtObject": self.get_ext_object(assembly),
- "ExtIdentifier": assembly.GlobalId,
- "Description": self.get_object_attribute(assembly, "Description", default="n/a"),
- }
- for field, key in self.custom_data["assemblies"].items():
- self.assemblies[assembly_name][field] = self.get_element_value(assembly, key)
-
- def get_connections(self):
- connections = self.file.by_type("IfcRelConnects")
- for connection in connections:
- connection_name = self.get_object_name(connection)
- self.connections[connection_name] = {
- "CreatedBy": self.get_email_from_history(connection.OwnerHistory),
- "CreatedOn": self.get_created_on_from_history(connection.OwnerHistory),
- # There is ambiguity for what the ConnectionType mapping should be
- "ConnectionType": self.get_object_attribute(connection, "Description", default="n/a"),
- "SheetName": "Connections",
- "RowName1": self.get_row_name_from_connection(connection, "RelatingElement"),
- "RowName2": self.get_row_name_from_connection(connection, "RelatedElement"),
- "RealizingElement": self.get_port_name_from_connection(connection, "RealizingElement"),
- "PortName1": self.get_port_name_from_connection(connection, "RelatingPort"),
- "PortName2": self.get_port_name_from_connection(connection, "RelatedPort"),
- "ExtSystem": self.get_ext_system_from_history(connection.OwnerHistory),
- "ExtObject": self.get_ext_object(connection),
- "ExtIdentifier": connection.GlobalId,
- "Description": self.get_object_attribute(connection, "Description", default="n/a"),
- }
- for field, key in self.custom_data["connections"].items():
- self.connections[connection_name][field] = self.get_element_value(connection, key)
-
- def get_spares(self):
- spares = self.file.by_type("IfcConstructionProductResource")
- for spare in spares:
- spare_name = self.get_object_name(spare)
- self.spares[spare_name] = {
- "CreatedBy": self.get_email_from_history(spare.OwnerHistory),
- "CreatedOn": self.get_created_on_from_history(spare.OwnerHistory),
- "Category": self.get_category_from_object(spare, "SpareType"),
- "TypeName": self.get_type_name_from_object(spare),
- "Suppliers": self.get_contact_pset_value_from_object(spare, "COBie_Spare", "Suppliers"),
- "ExtSystem": self.get_ext_system_from_history(spare.OwnerHistory),
- "ExtObject": self.get_ext_object(spare),
- "ExtIdentifier": spare.GlobalId,
- "Description": self.get_object_attribute(spare, "Description", default="n/a"),
- "SetNumber": self.get_contact_pset_value_from_object(spare, "COBie_Spare", "SetNumber"),
- "PartNumber": self.get_contact_pset_value_from_object(spare, "COBie_Spare", "PartNumber"),
- }
- for field, key in self.custom_data["spares"].items():
- self.spares[spare_name][field] = self.get_element_value(spare, key)
-
- def get_resources(self):
- resources = self.file.by_type("IfcConstructionProductResource")
- for resource in resources:
- resource_name = self.get_object_name(resource)
- self.resources[resource_name] = {
- "CreatedBy": self.get_email_from_history(resource.OwnerHistory),
- "CreatedOn": self.get_created_on_from_history(resource.OwnerHistory),
- "Category": self.get_object_attribute(resource, "ObjectType", picklist="ResourceType", default="n/a"),
- "ExtSystem": self.get_ext_system_from_history(resource.OwnerHistory),
- "ExtObject": self.get_ext_object(resource),
- "ExtIdentifier": resource.GlobalId,
- "Description": self.get_object_attribute(resource, "Description", default="n/a"),
- }
- for field, key in self.custom_data["resources"].items():
- self.resources[resource_name][field] = self.get_element_value(resource, key)
-
- def get_jobs(self):
- jobs = self.file.by_type("IfcTask")
- for job in jobs:
- job_name = self.get_object_name(job)
- task_time = job.TaskTime
- self.jobs[job_name] = {
- "CreatedBy": self.get_email_from_history(job.OwnerHistory),
- "CreatedOn": self.get_created_on_from_history(job.OwnerHistory),
- "Category": self.get_object_attribute(job, "ObjectType", picklist="JobType", default="n/a"),
- "Status": self.get_object_attribute(job, "Status", picklist="JobStatusType", default="n/a"),
- "TypeName": self.get_type_name_from_object(job),
- "Description": self.get_object_attribute(job, "Description", default="n/a"),
- "Duration": self.get_object_attribute(task_time, "ScheduleDuration", default=0),
- "DurationUnit": "day",
- "Start": self.get_object_attribute(task_time, "ScheduleStart", default=0),
- "TaskStartUnit": "day",
- "Frequency": self.get_object_attribute(task_time.Recurrence, "Occurrences", default=0)
- if hasattr(task_time, "Recurrence")
- else 0,
- "FrequencyUnit": "day",
- "ExtSystem": self.get_ext_system_from_history(job.OwnerHistory),
- "ExtObject": self.get_ext_object(job),
- "ExtIdentifier": job.GlobalId,
- "TaskNumber": self.get_object_attribute(job, "Identification"),
- "Priors": self.get_priors_from_job(job),
- "ResourceNames": self.get_resource_names_from_job(job),
- }
- for field, key in self.custom_data["jobs"].items():
- self.jobs[job_name][field] = self.get_element_value(job, key)
-
- # Impacts is not explicitly defined as a mapping in the responsibliity
- # matrix. This is my best guess. This data should not be relied upon until
- # this is clarified.
- def get_impacts(self):
- impacts = self.file.by_type("IfcPropertySet")
- for impact in impacts:
- if impact.Name != "Pset_EnvironmentalImpactValues" or not impact.HasProperties:
- continue
- for property in impact.HasProperties:
- property_name = "{}-{}".format(property.id(), self.get_object_name(property))
- self.impacts[property_name] = {
- "CreatedBy": self.get_email_from_history(impact.OwnerHistory),
- "CreatedOn": self.get_created_on_from_history(impact.OwnerHistory),
- "ImpactType": None,
- "ImpactStage": None,
- "SheetName": "Impacts",
- "RowName": "n/a",
- "Value": self.get_property_value(property),
- "Unit": "{}{}".format(property.Unit.Prefix, property.Unit.Name)
- if hasattr(property, "Unit") and property.Unit
- else "n/a",
- "LeadInTime": self.get_property_value(property, name="LeadInTime"),
- "Duration": self.get_property_value(property, name="Duration"),
- "LeadOutTime": self.get_property_value(property, name="LeadOutTime"),
- "ExtSystem": self.get_ext_system_from_history(impact.OwnerHistory),
- "ExtObject": self.get_ext_object(impact),
- "ExtIdentifier": impact.GlobalId,
- "Description": self.get_object_attribute(impact, "Description", default="n/a"),
- }
- for field, key in self.custom_data["impacts"].items():
- self.impacts[impact_name][field] = self.get_element_value(impact, key)
-
- def get_documents(self):
- documents = self.file.by_type("IfcDocumentInformation")
- for document in documents:
- document_name = self.get_object_name(document)
- self.documents[document_name] = {
- "CreatedBy": self.get_email_from_history(document.OwnerHistory),
- "CreatedOn": self.get_created_on_from_history(document.OwnerHistory),
- "Category": "n/a", # I am not sure what this mapping is meant to be
- "ApprovalBy": self.get_object_attribute(
- document, "IntendedUse", picklist="ApprovalBy", default="Information Only"
- ),
- "Stage": self.get_object_attribute(document, "Scope", picklist="StageType", default="Required"),
- "SheetName": "Documents",
- "RowName": "n/a",
- "Directory": self.get_directory_from_document(document),
- "File": self.get_file_from_document(document),
- "ExtSystem": self.get_ext_system_from_history(document.OwnerHistory),
- "ExtObject": self.get_ext_object(document),
- "ExtIdentifier": document.GlobalId,
- "Description": self.get_object_attribute(document, "Description", default=document_name),
- "Reference": document_name,
- }
- for field, key in self.custom_data["documents"].items():
- self.documents[document_name][field] = self.get_element_value(document, key)
-
- # Attributes is not explicitly defined as a mapping in the responsibliity
- # matrix. This is my best guess. This data should not be relied upon until
- # this is clarified.
- def get_attributes(self):
- attributes = self.file.by_type("IfcPropertySet")
- for attribute in attributes:
- if not attribute.HasProperties:
- continue
- for property in attribute.HasProperties:
- property_name = "{}-{}".format(property.id(), self.get_object_name(property))
- self.attributes[property_name] = {
- "CreatedBy": self.get_email_from_history(attribute.OwnerHistory),
- "CreatedOn": self.get_created_on_from_history(attribute.OwnerHistory),
- "Category": "n/a", # I am not sure what this mapping is meant to be
- "SheetName": "Attributes",
- "RowName": "n/a", # I am not sure what this mapping is meant to be
- "Value": self.get_property_value(property),
- "Unit": "{}{}".format(
- property.Unit.Prefix if hasattr(property.Unit, "Prefix") else "",
- property.Unit.Name if hasattr(property.Unit, "Name") else "n/a",
- )
- if hasattr(property, "Unit") and property.Unit
- else "n/a",
- "ExtSystem": self.get_ext_system_from_history(attribute.OwnerHistory),
- "ExtObject": self.get_ext_object(attribute),
- "ExtIdentifier": attribute.GlobalId,
- "Description": self.get_object_attribute(attribute, "Description", default="n/a"),
- # I'm holding off implementing this until I understand a bit
- # more about attributes
- "AllowedValues": "n/a",
- }
-
- def get_coordinates(self):
- coordinates = (
- self.file.by_type("IfcBuildingStorey") + self.file.by_type("IfcSpace") + self.file.by_type("IfcProduct")
- )
- for coordinate in coordinates:
- coordinate_name = "{}/{}".format(coordinate.is_a(), self.get_object_name(coordinate))
- mat = ifcopenshell.util.placement.get_local_placement(coordinate.ObjectPlacement)
- x, y, z = mat[:,3][:3]
- self.coordinates[coordinate_name] = {
- "CreatedBy": self.get_email_from_history(coordinate.OwnerHistory),
- "CreatedOn": self.get_created_on_from_history(coordinate.OwnerHistory),
- "Category": "Location", # I am not sure what this mapping is meant to be
- "SheetName": "Coordinates",
- "RowName": "n/a",
- "CoordinateXAxis": x,
- "CoordinateYAxis": y,
- "CoordinateZAxis": z,
- "ExtSystem": self.get_ext_system_from_history(coordinate.OwnerHistory),
- "ExtObject": self.get_ext_object(coordinate),
- "ExtIdentifier": coordinate.GlobalId,
- # Holding off implementing this, see Bug #688:
- # https://github.com/IfcOpenShell/IfcOpenShell/issues/688
- "ClockwiseRotation": "n/a", # X axis
- "ElevationalRotation": "n/a", # Y axis
- "YawRotation": "n/a", # Z axis
- }
-
- # I don't fully understand this worksheet. Don't trust this data.
- def get_issues(self):
- issues = self.file.by_type("IfcApproval")
- for issue in issues:
- issue_name = self.get_object_name(issue)
- self.issues[issue_name] = {
- "CreatedBy": self.get_email_from_history(issue.OwnerHistory),
- "CreatedOn": self.get_created_on_from_history(issue.OwnerHistory),
- "Type": "n/a", # How do we get to the Pset_Risk from the IfcApproval?
- "Risk": "n/a", # How do we get to the Pset_Risk from the IfcApproval?
- "Chance": "n/a", # How do we get to the Pset_Risk from the IfcApproval?
- "Impact": "n/a", # How do we get to the Pset_Risk from the IfcApproval?
- "SheetName1": "n/a",
- "RowName1": "n/a",
- "SheetName2": "n/a",
- "RowName2": "n/a",
- "Description": self.get_object_attribute(issue, "Description", default="n/a"),
- "Owner": self.get_email_from_history(issue.RequestingApproval), # Is this correct?
- "Mitigation": "n/a",
- "ExtSystem": self.get_ext_system_from_history(issue.OwnerHistory),
- "ExtObject": self.get_ext_object(issue),
- "ExtIdentifier": issue.GlobalId,
- }
-
- def get_directory_from_document(self, document):
- if self.file.schema == "IFC2X3":
- if document.HasDocumentReferences:
- for reference in document.HasDocumentReferences:
- return self.get_object_attribute(reference, "Location", default="n/a")
- else:
- return self.get_object_attribute(document, "Location", default="n/a")
-
- def get_file_from_document(self, document):
- if self.file.schema == "IFC2X3":
- if document.HasDocumentReferences:
- for reference in document.HasDocumentReferences:
- return self.get_object_attribute(reference, "Name", default="n/a")
- else:
- self.get_object_attribute(document, "Identification", default="n/a")
-
- def get_resource_names_from_job(self, job):
- names = []
- if job.OperatesOn and job.OperatesOn.RelatedObjects:
- for object in job.OperatesOn.RelatedObjects:
- names.append(object.Name)
- return ",".join(names)
-
- def get_priors_from_job(self, job):
- # The responsibility matrix is vague as to whether it expects a task
- # name or a task identification. I chose task name.
- if (
- job.IsSuccessorFrom
- and job.IsSuccessorFrom.RelatingProcess
- and job.IsSuccessorFrom.RelatingProcess.is_a("IfcTask")
- ):
- return self.get_object_name(job.IsSuccessorFrom.RelatingProcess)
-
- def get_row_name_from_connection(self, connection, key):
- if not connection.is_a("IfcRelConnectsElements"):
- return None
- object = getattr(connection, key)
- if self.is_object_a_component_asset(object):
- return object.Name
- self.logger.error("The connected object relationship %s is not a component asset for %s", key, connection)
-
- def get_port_name_from_connection(self, connection, key):
- if not connection.is_a("IfcRelConnectsPorts"):
- return None
- object = getattr(connection, key)
- if object and self.is_object_a_component_asset(object):
- return object.Name
- self.logger.error("The connected object relationship %s is not a component asset for %s", key, connection)
-
- def is_object_a_component_asset(self, obj):
- return obj in self.component_assets
-
- def get_space_name_from_component(self, component):
- for relationship in component.ContainedInStructure:
- if relationship.RelatingStructure.is_a("IfcSpace") and relationship.RelatingStructure.Name:
- return relationship.RelatingStructure.Name
- self.logger.error("A related space name could not be determined for %s", component)
-
- def get_type_name_from_object(self, object):
- if self.file.schema == "IFC2X3":
- for relationship in object.IsDefinedBy:
- if relationship.is_a("IfcRelDefinesByType") and relationship.RelatingType.Name:
- return relationship.RelatingType.Name
- else:
- for relationship in object.IsTypedBy:
- if relationship.RelatingType.Name:
- return relationship.RelatingType.Name
- self.logger.error("A related type name could not be determined for %s", object)
-
- def get_expected_life_from_type(self, type):
- if self.file.schema == "IFC2X3":
- return self.get_pset_value_from_object(type, "COBie_ServiceLife", "ServiceLifeDuration", "n/a")
- return self.get_pset_value_from_object(type, "Pset_ServiceLife", "ServiceLifeDuration", "n/a")
-
- def get_contact_pset_value_from_object(self, object, pset_name, property_name):
- result = self.get_pset_value_from_object(object, pset_name, property_name)
- if not result:
- self.logger.error("No property %s in %s was found for %s", property_name, pset_name, object)
- if result not in self.contacts:
- self.logger.error("A coresponding %s contact in %s was not found for %s", property_name, pset_name, object)
- return result
-
- def get_pset_value_from_object(self, object, pset_name, property_name, default=None, picklist=None):
- pset = self.get_pset_from_object(object, pset_name)
- if not pset:
- if picklist:
- self.picklists[picklist].append(default)
- return default
- prop = self.get_property_from_pset(pset, property_name, default)
- if picklist:
- self.picklists[picklist].append(prop)
- return prop
-
- def get_grouped_product_names_from_object(self, object, type):
- names = []
- for relationship in object.IsGroupedBy:
- for related_object in relationship.RelatedObjects:
- if related_object.is_a(type):
- names.append(related_object.Name)
- if names:
- return ",".join(names)
- self.logger.error("No related %s were found for %s", type, object)
-
- def get_net_area_from_space(self, space):
- qto = self.get_qto_from_object(space, "Qto_SpaceBaseQuantities")
- if not qto:
- return "n/a"
- return self.get_property_from_qto(qto, "NetFloorArea", "AreaValue")
-
- def get_gross_area_from_space(self, space):
- qto = self.get_qto_from_object(space, "Qto_SpaceBaseQuantities")
- if not qto:
- return "n/a"
- return self.get_property_from_qto(qto, "GrossFloorArea", "AreaValue")
-
- def get_usable_height_from_space(self, space):
- qto = self.get_qto_from_object(space, "Qto_SpaceBaseQuantities")
- if not qto:
- return "n/a"
- return self.get_property_from_qto(qto, "FinishCeilingHeight", "LengthValue")
-
- def get_qto_from_object(self, object, name):
- for relationship in object.IsDefinedBy:
- if (
- relationship.is_a("IfcRelDefinesByProperties")
- and relationship.RelatingPropertyDefinition.is_a("IfcQuantitySet")
- and relationship.RelatingPropertyDefinition.Name == name
- ):
- return relationship.RelatingPropertyDefinition
- self.logger.warning("The qto %s was not found for %s", name, object)
-
- def get_property_from_qto(self, qto, name, attribute):
- for property in qto.Quantities:
- if property.Name == name:
- return getattr(property, attribute)
- self.logger.warning("The quantity value %s was not found for %s", name, qto)
- return "n/a"
-
- def get_property_from_pset(self, pset, name, default=None):
- for prop in pset.HasProperties:
- if prop.Name == name:
- return prop.NominalValue.wrappedValue
- self.logger.warning("The property %s was not found for %s", name, pset)
- return default
-
- def get_property_value(self, prop, name=None):
- if not prop.is_a("IfcPropertySingleValue"):
- return "n/a"
- if name is not None and prop.Name != name:
- return "n/a"
- value = self.get_object_attribute(prop, "NominalValue", default=None)
- if value:
- return value.wrappedValue
- return "n/a"
-
- def get_pset_from_object(self, object, name):
- if object.is_a("IfcTypeObject"):
- if object.HasPropertySets:
- for pset in object.HasPropertySets:
- if pset.is_a("IfcPropertySet") and pset.Name == name:
- return pset
- else:
- for relationship in object.IsDefinedBy:
- if (
- relationship.is_a("IfcRelDefinesByProperties")
- and relationship.RelatingPropertyDefinition.is_a("IfcPropertySet")
- and relationship.RelatingPropertyDefinition.Name == name
- ):
- return relationship.RelatingPropertyDefinition
- self.logger.warning("The pset %s was not found for %s", name, object)
-
- def get_height_from_storey(self, storey):
- for relationship in storey.IsDefinedBy:
- if not relationship.RelatingPropertyDefinition.is_a("IfcElementQuantity"):
- continue
- for quantity in relationship.RelatingPropertyDefinition.Quantities:
- if quantity.is_a("IfcQuantityLength") and quantity.LengthValue:
- return quantity.LengthValue
- self.logger.warning("A height length value was not found for %s", storey)
- return "n/a"
-
- def get_created_on_from_history(self, history):
- if history.CreationDate:
- return datetime.datetime.fromtimestamp(history.CreationDate).isoformat()
- self.logger.warning("A created on date was not found for %s", history)
- return self.default_date
-
- def get_object_attribute(self, object, attribute, is_primary_key=False, picklist=None, default=None):
- result = getattr(object, attribute)
- if result:
- if picklist:
- self.picklists[picklist].append(result)
- return result
- if is_primary_key:
- self.logger.error("The primary key attribute %s was not found for %s", attribute, object)
- else:
- self.logger.warning("The attribute %s was not found for %s", attribute, object)
- return default
-
- def get_ext_project_object(self):
- self.picklists["objType"].append("IfcProject")
- return "IfcProject"
-
- def get_ext_site_object(self):
- self.picklists["objType"].append("IfcSite")
- return "IfcSite"
-
- def get_ext_object(self, object):
- self.picklists["objType"].append(object.is_a())
- return object.is_a()
-
- def get_ext_system_from_history(self, history):
- return history.OwningApplication.ApplicationFullName
-
- def get_object_name(self, object):
- if not object.Name:
- self.logger.error("A primary key name was not found for %s", object)
- return "Object{}".format(object.id())
- return object.Name
-
- def get_area_measurement_from_building(self, building):
- for relationship in building.IsDefinedBy:
- if (
- relationship.RelatingPropertyDefinition.is_a("IfcElementQuantity")
- and relationship.RelatingPropertyDefinition.MethodOfMeasurement
- ):
- return relationship.RelatingPropertyDefinition.MethodOfMeasurement
- self.logger.warning("A method of measurement was not defined for %s", building)
-
- def get_unit_type_from_units(self, units, type):
- for unit in units:
- if unit.UnitType == type:
- if unit.is_a("IfcSIUnit") and unit.Prefix:
- return "{}{}".format(unit.Prefix, unit.Name)
- return unit.Name
- self.logger.error("A unit %s was not defined in this project for %s", type, units)
-
- def get_monetary_unit_from_units(self, units):
- for unit in units:
- if unit.is_a("IfcMonetaryUnit"):
- return unit.Currency
- self.logger.error("A monetary unit could not be found for %s", units)
-
- def get_project_globalid_from_building(self, building):
- return self.get_parent_spatial_element(building, "IfcProject").GlobalId
-
- def get_site_globalid_from_building(self, building):
- return self.get_parent_spatial_element(building, "IfcSite").GlobalId
-
- def get_project_name_from_building(self, building):
- project = self.get_parent_spatial_element(building, "IfcProject")
- if project.Name:
- return project.Name
- self.logger.error("The project name is empty for %s", project)
- return "n/a"
-
- def get_site_name_from_building(self, building):
- site = self.get_parent_spatial_element(building, "IfcSite")
- if site.Name:
- return site.Name
- self.logger.error("The site name is empty for %s", site)
- return "n/a"
-
- def get_units_from_building(self, building):
- return self.get_parent_spatial_element(building, "IfcProject").UnitsInContext.Units
-
- def get_parent_spatial_element(self, child, name):
- for relationship in child.Decomposes:
- if relationship.RelatingObject.is_a(name):
- return relationship.RelatingObject
- return self.get_parent_spatial_element(relationship.RelatingObject, name)
- return None
-
- def get_category_from_object(self, object, picklist):
- class_identification = None
- class_name = None
- for association in object.HasAssociations:
- if not association.is_a("IfcRelAssociatesClassification"):
- continue
- if not association.RelatingClassification.is_a("IfcClassificationReference"):
- continue
- if self.file.schema == "IFC2X3":
- class_identification = association.RelatingClassification.ItemReference
- else:
- class_identification = association.RelatingClassification.Identification
- class_name = association.RelatingClassification.Name
- break
- if not class_identification or class_name:
- self.logger.error("The classification has invalid identification and name for %s", object)
- result = "{}:{}".format(class_identification, class_name)
- self.picklists[picklist].append(result)
- return result
- # The responsibility matrix lists a fallback, but it is a very
- # cumbersome check, and so it is not implemented here.
-
- def get_name_from_person(self, person, attribute):
- name = getattr(person, attribute)
- if not name or not name.isalpha():
- self.logger.warning('The person\'s %s seems to be badly formatted ("%s") for %s', attribute, name, person)
- return name if name else "n/a"
-
- def get_lines_from_address(self, address):
- result = self.get_attribute_from_address(address, "AddressLines")
- if isinstance(result, tuple):
- return ", ".join(result)
- return result
-
- def get_attribute_from_address(self, address, attribute):
- result = getattr(address, attribute)
- if not result:
- self.logger.warning("The address %s seems to not exist for %s", attribute, address)
- return "n/a"
- return result
-
- def get_email_from_history(self, history):
- person = history.OwningUser.ThePerson
- organisation = history.OwningUser.TheOrganization
- email = self.get_email_from_person_or_organisation(person)
- if email:
- return email
-
- email = self.get_email_from_person_or_organisation(organisation)
- if email:
- return email
-
- given_name = person.GivenName if person.GivenName else "unknown"
- family_name = person.FamilyName if person.FamilyName else "unknown"
- organisation_name = organisation.Name if organisation.Name else "unknown"
-
- if given_name == "unknown" and family_name == "unknown" and organisation_name == "unknown":
- self.logger.error("No primary key could be determined from %s", history)
-
- return "{}{}@{}".format(given_name, family_name, organisation_name)
-
- def get_postal_address_from_history(self, history):
- for address in history.OwningUser.ThePerson.Addresses or []:
- if address.is_a("IfcPostalAddress"):
- return address
- for address in history.OwningUser.TheOrganization.Addresses or []:
- if address.is_a("IfcPostalAddress"):
- return address
- return self.file.createIfcPostalAddress()
-
- def get_category_from_history(self, history):
- roles = []
- both = history.OwningUser
- person = both.ThePerson
- organisation = both.TheOrganization
- both_roles = list(both.Roles) if both.Roles else []
- person_roles = list(person.Roles) if person.Roles else []
- organisation_roles = list(organisation.Roles) if organisation.Roles else []
- for role in both_roles + person_roles + organisation_roles:
- roles.append(self.get_role(role))
- result = ",".join(set(roles))
- if not result:
- self.logger.error("No roles could be found for %s", history)
- return
- self.picklists["Category-Role"].append(result)
- return result
-
- def get_phone_from_history(self, history):
- person = history.OwningUser.ThePerson
- organisation = history.OwningUser.TheOrganization
- phone = self.get_phone_from_person_or_organisation(person)
- if phone:
- return phone
- phone = self.get_phone_from_person_or_organisation(organisation)
- if phone:
- return phone
- return "n/a"
-
- def get_ext_object_from_history(self, history):
- result = history.OwningUser.is_a()
- self.picklists["objType"].append(result)
- return result
-
- def get_department_from_history(self, history):
- organisation = history.OwningUser.TheOrganization
- department = self.get_internal_location_from_organisation(organisation)
- if department:
- return department
- last_department = None
- for relationship in organisation.Relates:
- for related_organisation in relationship.RelatedOrganizations:
- department = self.get_internal_location_from_organisation(related_organisation)
- if department:
- last_department = department
- if last_department:
- return last_department
- return history.OwningUser.TheOrganization.Name or "n/a"
-
- def get_internal_location_from_organisation(self, organisation):
- for address in organisation.Addresses or []:
- if address.is_a("IfcPostalAddress"):
- return address.InternalLocation
- self.logger.warning("An internal location was not found for %s", organisation)
-
- def get_role(self, role):
- if role.Role == "USERDEFINED":
- return role.UserDefinedRole
- return role.Role
-
- def get_phone_from_person_or_organisation(self, person_or_org):
- for address in person_or_org.Addresses or []:
- if address.is_a("IfcTelecomAddress"):
- return address.TelephoneNumbers[0]
- self.logger.warning("A phone was not found for {}", person_or_org)
-
- def get_email_from_person_or_organisation(self, person_or_org):
- for address in person_or_org.Addresses or []:
- if address.is_a("IfcTelecomAddress"):
- return address.ElectronicMailAddresses[0]
- self.logger.warning("An email address was not found for {}", person_or_org)
-
- def get_element_value(self, element, key):
- value = self.selector.get_element_value(element, key)
- if hasattr(value, "wrappedValue"):
- return value.wrappedValue
- return value
-
-
-class CobieWriter:
- def __init__(self, parser, filename=None):
- self.filename = filename
- self.parser = parser
- self.sheets = []
- self.sheet_data = {}
- self.colours = {
- "r": "fdff8e", # Required
- "i": "fdcd94", # Internal reference
- "e": "cd95ff", # External reference
- "o": "cdffc8", # Optional
- "s": "c0c0c0", # Secondary information
- "p": "9ccaff", # Project specific
- "n": "000000", # Not used
- }
-
- def write(self):
- self.sheets = [
- "Contact",
- "Facility",
- "Floor",
- "Space",
- "Zone",
- "Type",
- "Component",
- "System",
- "Assembly",
- "Connection",
- "Spare",
- "Resource",
- "Job",
- "Impact",
- "Document",
- "Attribute",
- "Coordinate",
- "Issue",
- ]
- self.write_data(
- "Contact",
- self.parser.contacts,
- "Email",
- [
- "Email",
- "CreatedBy",
- "CreatedOn",
- "Category",
- "Company",
- "Phone",
- "ExtSystem",
- "ExtObject",
- "ExtIdentifier",
- "Department",
- "OrganizationCode",
- "GivenName",
- "FamilyName",
- "Street",
- "PostalBox",
- "Town",
- "StateRegion",
- "PostalCode",
- "Country",
- ],
- "ririrreeeoooooooooo",
- self.parser.custom_data["contacts"],
- )
- self.write_data(
- "Facility",
- self.parser.facilities,
- "Name",
- [
- "Name",
- "CreatedBy",
- "CreatedOn",
- "Category",
- "ProjectName",
- "SiteName",
- "LinearUnits",
- "AreaUnits",
- "VolumeUnits",
- "CostUnit",
- "AreaMeasurement",
- "ExternalSystem",
- "ExternalProjectObject",
- "ExternalProjectIdentifier",
- "ExternalSiteObject",
- "ExternalSiteIdentifier",
- "ExternalFacilityObject",
- "ExternalFacilityIdentifier",
- "Description",
- "ProjectDescription",
- "SiteDescription",
- "Phase",
- ],
- "ririrriiiireeeeeeeoooo",
- self.parser.custom_data["facilities"],
- )
- self.write_data(
- "Floor",
- self.parser.floors,
- "Name",
- [
- "Name",
- "CreatedBy",
- "CreatedOn",
- "Category",
- "ExtSystem",
- "ExtObject",
- "ExtIdentifier",
- "Description",
- "Elevation",
- "Height",
- ],
- "ririeeeooo",
- self.parser.custom_data["floors"],
- )
- self.write_data(
- "Space",
- self.parser.spaces,
- "Name",
- [
- "Name",
- "CreatedBy",
- "CreatedOn",
- "Category",
- "FloorName",
- "Description",
- "ExtSystem",
- "ExtObject",
- "ExtIdentifier",
- "RoomTag",
- "UsableHeight",
- "GrossArea",
- "NetArea",
- ],
- "ririireeeoooo",
- self.parser.custom_data["spaces"],
- )
- self.write_data(
- "Zone",
- self.parser.zones,
- "Name",
- [
- "Name",
- "CreatedBy",
- "CreatedOn",
- "Category",
- "SpaceNames",
- "ExtSystem",
- "ExtObject",
- "ExtIdentifier",
- "Description",
- ],
- "ririieeeo",
- self.parser.custom_data["zones"],
- )
- self.write_data(
- "Type",
- self.parser.types,
- "Name",
- [
- "Name",
- "CreatedBy",
- "CreatedOn",
- "Category",
- "Description",
- "AssetType",
- "Manufacturer",
- "ModelNumber",
- "WarrantyGuarantorParts",
- "WarrantyDurationParts",
- "WarrantyGuarantorLabor",
- "WarrantyDurationLabor",
- "WarrantyDurationUnit",
- "ExtSystem",
- "ExtObject",
- "ExtIdentifier",
- "ReplacementCost",
- "ExpectedLife",
- "DurationUnit",
- "NominalLength",
- "NominalWidth",
- "NominalHeight",
- "ModelReference",
- "Shape",
- "Size",
- "Color",
- "Finish",
- "Grade",
- "Material",
- "Constituents",
- "Features",
- "AccessibilityPerformance",
- "CodePerformance",
- "SustainabilityPerformance",
- ],
- "riririoooooooeeeooooooooooooooooooo",
- self.parser.custom_data["types"],
- )
- self.write_data(
- "Component",
- self.parser.components,
- "Name",
- [
- "Name",
- "CreatedBy",
- "CreatedOn",
- "TypeName",
- "Space",
- "Description",
- "ExtSystem",
- "ExtObject",
- "ExtIdentifier",
- "SerialNumber",
- "InstallationDate",
- "WarrantyStartDate",
- "TagNumber",
- "BarCode",
- "AssetIdentifier",
- ],
- "ririireeeoooooo",
- self.parser.custom_data["components"],
- )
- self.write_data(
- "System",
- self.parser.systems,
- "Name",
- [
- "Name",
- "CreatedBy",
- "CreatedOn",
- "Category",
- "ComponentNames",
- "ExtSystem",
- "ExtObject",
- "ExtIdentifier",
- "Description",
- ],
- "ririieeeo",
- self.parser.custom_data["systems"],
- )
- self.write_data(
- "Assembly",
- self.parser.assemblies,
- "Name",
- [
- "Name",
- "CreatedBy",
- "CreatedOn",
- "SheetName",
- "ParentName",
- "ChildNames",
- "AssemblyType",
- "ExtSystem",
- "ExtObject",
- "ExtIdentifier",
- "Description",
- ],
- "rirrrrreeeo",
- self.parser.custom_data["assemblies"],
- )
- self.write_data(
- "Connection",
- self.parser.connections,
- "Name",
- [
- "Name",
- "CreatedBy",
- "CreatedOn",
- "ConnectionType",
- "SheetName",
- "RowName1",
- "RowName2",
- "RealizingElement",
- "PortName1",
- "PortName2",
- "ExtSystem",
- "ExtObject",
- "ExtIdentifier",
- "Description",
- ],
- "ririiiiiiieeeo",
- self.parser.custom_data["connections"],
- )
- self.write_data(
- "Spare",
- self.parser.spares,
- "Name",
- [
- "Name",
- "CreatedBy",
- "CreatedOn",
- "Category",
- "TypeName",
- "Suppliers",
- "ExtSystem",
- "ExtObject",
- "ExtIdentifier",
- "Description",
- "SetNumber",
- "PartNumber",
- ],
- "ririiieeeooo",
- self.parser.custom_data["spares"],
- )
- self.write_data(
- "Resource",
- self.parser.resources,
- "Name",
- [
- "Name",
- "CreatedBy",
- "CreatedOn",
- "Category",
- "ExtSystem",
- "ExtObject",
- "ExtIdentifier",
- "Description",
- ],
- "ririeeeo",
- self.parser.custom_data["resources"],
- )
- self.write_data(
- "Job",
- self.parser.jobs,
- "Name",
- [
- "Name",
- "CreatedBy",
- "CreatedOn",
- "Category",
- "Status",
- "TypeName",
- "Description",
- "Duration",
- "DurationUnit",
- "Start",
- "TaskStartUnit",
- "Frequency",
- "FrequencyUnit",
- "ExtSystem",
- "ExtObject",
- "ExtIdentifier",
- "TaskNumber",
- "Priors",
- "ResourceNames",
- ],
- "ririiirriririeeeoii",
- self.parser.custom_data["jobs"],
- )
- self.write_data(
- "Impact",
- self.parser.impacts,
- "Name",
- [
- "Name",
- "CreatedBy",
- "CreatedOn",
- "ImpactType",
- "ImpactStage",
- "SheetName",
- "RowName",
- "Value",
- "Unit",
- "LeadInTime",
- "Duration",
- "LeadOutTime",
- "ExtSystem",
- "ExtObject",
- "ExtIdentifier",
- "Description",
- ],
- "ririiiirioooeeeo",
- self.parser.custom_data["impacts"],
- )
- self.write_data(
- "Document",
- self.parser.documents,
- "Name",
- [
- "Name",
- "CreatedBy",
- "CreatedOn",
- "Category",
- "ApprovalBy",
- "Stage",
- "SheetName",
- "RowName",
- "Directory",
- "File",
- "ExtSystem",
- "ExtObject",
- "ExtIdentifier",
- "Description",
- "Reference",
- ],
- "ririiiiirreeeoo",
- self.parser.custom_data["documents"],
- )
- self.write_data(
- "Attribute",
- self.parser.attributes,
- "Name",
- [
- "Name",
- "CreatedBy",
- "CreatedOn",
- "Category",
- "SheetName",
- "RowName",
- "Value",
- "Unit",
- "ExtSystem",
- "ExtObject",
- "ExtIdentifier",
- "Description",
- "AllowedValues",
- ],
- "ririiirreeeoo",
- )
- self.write_data(
- "Coordinate",
- self.parser.coordinates,
- "Name",
- [
- "Name",
- "CreatedBy",
- "CreatedOn",
- "Category",
- "SheetName",
- "RowName",
- "CoordinateXAxis",
- "CoordinateYAxis",
- "CoordinateZAxis",
- "ExtSystem",
- "ExtObject",
- "ExtIdentifier",
- "ClockwiseRotation",
- "ElevationalRotation",
- "YawRotation",
- ],
- "ririiooooeeeooo",
- )
- self.write_data(
- "Issue",
- self.parser.issues,
- "Name",
- [
- "Name",
- "CreatedBy",
- "CreatedOn",
- "Type",
- "Risk",
- "Chance",
- "Impact",
- "SheetName1",
- "RowName1",
- "SheetName2",
- "RowName2",
- "Description",
- "Owner",
- "Mitigation",
- "ExtSystem",
- "ExtObject",
- "ExtIdentifier",
- ],
- "ririooooooooooeee",
- )
-
- def write_data(self, sheet, data, primary_key, fieldnames, colours, custom_data={}):
- self.sheet_data[sheet] = {"headers": fieldnames + list(custom_data.keys()), "colours": colours, "rows": []}
- for name, row in data.items():
- row[primary_key] = name
- values = []
- for fieldname in fieldnames:
- values.append(row[fieldname])
- for fieldname in custom_data.keys():
- values.append(row[fieldname])
- self.sheet_data[sheet]["rows"].append(values)
-
-
-class CobieCsvWriter(CobieWriter):
- def write(self):
- import csv
-
- super().write()
- for sheet, data in self.sheet_data.items():
- with open(os.path.join(self.filename, "{}.csv".format(sheet)), "w", newline="", encoding="utf-8") as file:
- writer = csv.writer(file)
- writer.writerow(data["headers"])
- for row in data["rows"]:
- writer.writerow(row)
-
-
-class CobieXlsWriter(CobieWriter):
- def write(self):
- from xlsxwriter import Workbook
-
- super().write()
- self.workbook = Workbook(self.filename + ".xlsx")
-
- self.cell_formats = {}
- for key, value in self.colours.items():
- self.cell_formats[key] = self.workbook.add_format()
- self.cell_formats[key].set_bg_color(value)
-
- for sheet in self.sheets:
- self.write_worksheet(sheet)
- self.workbook.close()
-
- def write_worksheet(self, name):
- worksheet = self.workbook.add_worksheet(name)
- r = 0
- c = 0
- for header in self.sheet_data[name]["headers"]:
- cell = worksheet.write(r, c, header, self.cell_formats["s"])
- c += 1
- c = 0
- r += 1
- for row in self.sheet_data[name]["rows"]:
- c = 0
- for col in row:
- if c >= len(self.sheet_data[name]["colours"]):
- cell_format = "p"
- else:
- cell_format = self.sheet_data[name]["colours"][c]
- cell = worksheet.write(r, c, col, self.cell_formats[cell_format])
- c += 1
- r += 1
-
-
-class CobieOdsWriter(CobieWriter):
- def write(self):
- from odf.opendocument import OpenDocumentSpreadsheet
- from odf.style import Style, TableCellProperties
-
- super().write()
- self.doc = OpenDocumentSpreadsheet()
-
- self.cell_formats = {}
- for key, value in self.colours.items():
- style = Style(name=key, family="table-cell")
- style.addElement(TableCellProperties(backgroundcolor="#" + value))
- self.doc.automaticstyles.addElement(style)
- self.cell_formats[key] = style
-
- for sheet in self.sheets:
- self.write_table(sheet)
- self.doc.save(self.filename, True)
-
- def write_table(self, name):
- from odf.table import Table, TableRow, TableCell
- from odf.text import P
-
- table = Table(name=name)
- tr = TableRow()
- for header in self.sheet_data[name]["headers"]:
- tc = TableCell(valuetype="string", stylename="s")
- tc.addElement(P(text=header))
- tr.addElement(tc)
- table.addElement(tr)
- for row in self.sheet_data[name]["rows"]:
- tr = TableRow()
- c = 0
- for col in row:
- if c >= len(self.sheet_data[name]["colours"]):
- cell_format = "p"
- else:
- cell_format = self.sheet_data[name]["colours"][c]
- tc = TableCell(valuetype="string", stylename=cell_format)
- tc.addElement(P(text=col))
- tr.addElement(tc)
- c += 1
- table.addElement(tr)
- self.doc.spreadsheet.addElement(table)
-
-
-if __name__ == "__main__":
- parser = argparse.ArgumentParser(description="Converts COBie IFC MVD into its spreadsheet equivalent")
- parser.add_argument("input", type=str, help="Specify an IFC file to process")
- parser.add_argument("output", type=str, help="The output directory for CSV or filename for other formats")
- parser.add_argument("-l", "--log", type=str, help="Specify where errors should be logged", default="process.log")
- parser.add_argument(
- "-f", "--format", type=str, help="Choose which format to export in (csv/ods/xlsx)", default="csv"
- )
- parser.add_argument(
- "-c", "--components", type=str, help="A custom selector for components. Defaults to COBie", default=".COBie"
- )
- parser.add_argument(
- "-t", "--types", type=str, help="A custom selector for types. Defaults to COBieType", default=".COBieType"
- )
- parser.add_argument(
- "-d",
- "--data",
- type=str,
- help="JSON file containing custom data to be appended to the COBie spreadsheet template",
- default="",
- )
- args = vars(parser.parse_args())
-
- print("Processing IFC file ...")
-
- start = time.time()
- logging.basicConfig(filename=args["log"], filemode="a", level=logging.DEBUG)
- logger = logging.getLogger("IFCtoCOBie")
- logger.info("Starting conversion")
- selector = ifcopenshell.util.selector.Selector()
- parser = IfcCobieParser(logger, selector)
- if args["data"]:
- with open(bpy.context.scene.BIMProperties.cobie_json_file, "r") as f:
- custom_data = json.load(f)
- else:
- custom_data = {}
- parser.parse(args["input"], args["types"], args["components"], custom_data)
-
- print("Generating reports ...")
-
- if args["format"] == "xlsx":
- writer = CobieXlsWriter(parser, args["output"])
- elif args["format"] == "ods":
- writer = CobieOdsWriter(parser, args["output"])
- else:
- writer = CobieCsvWriter(parser, args["output"])
- writer.write()
-
- logger.info("Finished conversion in %ss", time.time() - start)
- print("# All reports are complete :-)")
diff --git a/src/ifccobie/get_maintainable_assets.py b/src/ifccobie/get_maintainable_assets.py
deleted file mode 100644
index 591a080088..0000000000
--- a/src/ifccobie/get_maintainable_assets.py
+++ /dev/null
@@ -1,79 +0,0 @@
-
-# IfcCOBie - Extract COBie data from IFC to spreadsheets
-# Copyright (C) 2019, 2020, 2021 Dion Moult
-#
-# This file is part of IfcCOBie.
-#
-# IfcCOBie is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# IfcCOBie 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 Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with IfcCOBie. If not, see .
-
-import json
-import ifcopenshell
-import ifcopenshell.util.selector
-
-with open("../blenderbim/blenderbim/bim/schema/entity_descriptions.json") as f:
- entity_descriptions = json.load(f)
-with open("../blenderbim/blenderbim/bim/schema/enum_descriptions.json") as f:
- enum_descriptions = json.load(f)
-
-print('{|class="wikitable"')
-print("! IFC Class")
-print("! Predefined Type")
-
-
-def print_entity(entity):
- print("|-")
- print("| " + entity.name())
- print("| ")
- if entity.name() in entity_descriptions:
- print(
- "{} ... [https://standards.buildingsmart.org/IFC/DEV/IFC4_3/RC1/HTML/link/{}.htm read more]".format(
- entity_descriptions[entity.name()], entity.name().lower()
- )
- )
- else:
- print(
- "No description provided ... [https://standards.buildingsmart.org/IFC/DEV/IFC4_3/RC1/HTML/link/{}.htm read more]".format(
- entity_descriptions[entity.name()], entity.name().lower()
- )
- )
- for attribute in entity.attributes():
- if attribute.name() == "PredefinedType":
- enum = attribute.type_of_attribute().declared_type()
- print("\nThe following predefined types are defined:\n")
- for item in enum.enumeration_items():
- # print('|-')
- # print('| ' + entity.name())
- # print('| ' + item)
- if enum.name() in enum_descriptions and item in enum_descriptions[enum.name()]:
- print(
- "* '''{}''' - {} ... [https://standards.buildingsmart.org/IFC/DEV/IFC4_3/RC1/HTML/link/{}.htm read more]".format(
- item, enum_descriptions[enum.name()][item], enum.name().lower()
- )
- )
- else:
- print(
- "* '''{}''' - No description provided ... [https://standards.buildingsmart.org/IFC/DEV/IFC4_3/RC1/HTML/link/{}.htm read more]".format(
- item, enum.name().lower()
- )
- )
-
- for subtype in entity.subtypes():
- print_entity(subtype)
-
-
-for asset in ifcopenshell.util.selector.cobie_component_assets:
- schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name("IFC4")
- print_entity(schema.declaration_by_name(asset))
-
-print("|}")
diff --git a/src/ifccobie/icon.ico b/src/ifccobie/icon.ico
deleted file mode 100644
index b9eee9acb9..0000000000
Binary files a/src/ifccobie/icon.ico and /dev/null differ
diff --git a/src/ifccsv/ifccsv.py b/src/ifccsv/ifccsv.py
index 1f72bf6c54..04d18727e2 100755
--- a/src/ifccsv/ifccsv.py
+++ b/src/ifccsv/ifccsv.py
@@ -70,6 +70,7 @@ class IfcCsv:
include_global_id=True,
delimiter=",",
null="-",
+ empty="",
bool_true="YES",
bool_false="NO",
sort=None,
@@ -96,6 +97,8 @@ class IfcCsv:
value = ifcopenshell.util.selector.get_element_value(element, attribute)
if value is None:
value = null
+ elif value == "":
+ value = empty
elif value is True:
value = bool_true
elif value is False:
@@ -237,18 +240,18 @@ class IfcCsv:
for index, format_query in formatting_indices.items():
if row[index] == null:
continue
- if not isinstance(row[index], str):
- row[index] = '"' + str(row[index]) + '"'
+ row[index] = '"' + str(row[index]).replace('"', '\\"') + '"'
row[index] = ifcopenshell.util.selector.format(format_query.replace("{{value}}", row[index]))
def sort_results(self, sort, attributes, include_global_id):
if not self.results:
return
if sort:
+
def natural_sort(value):
if isinstance(value, str):
convert = lambda text: int(text) if text.isdigit() else text.lower()
- return [convert(c) for c in re.split('([0-9]+)', value)]
+ return [convert(c) for c in re.split("([0-9]+)", value)]
return value
# Sort least important keys first, then more important keys.
@@ -368,17 +371,21 @@ class IfcCsv:
results.update([p.Name for p in element.Quantities])
return ["{}.{}".format(pset_qto_name, n) for n in results]
- def Import(self, ifc_file, table, attributes=None, delimiter=",", null="-", bool_true="YES", bool_false="NO"):
+ def Import(
+ self, ifc_file, table, attributes=None, delimiter=",", null="-", empty="", bool_true="YES", bool_false="NO"
+ ):
ext = table.split(".")[-1].lower()
if ext == "csv":
- self.import_csv(ifc_file, table, attributes, delimiter, null, bool_true, bool_false)
+ self.import_csv(ifc_file, table, attributes, delimiter, null, empty, bool_true, bool_false)
elif ext == "ods":
- self.import_ods(ifc_file, table, attributes, null, bool_true, bool_false)
+ self.import_ods(ifc_file, table, attributes, null, empty, bool_true, bool_false)
elif ext == "xlsx":
- self.import_xlsx(ifc_file, table, attributes, null, bool_true, bool_false)
+ self.import_xlsx(ifc_file, table, attributes, null, empty, bool_true, bool_false)
- def import_csv(self, ifc_file, table, attributes=None, delimiter=",", null="-", bool_true="YES", bool_false="NO"):
+ def import_csv(
+ self, ifc_file, table, attributes=None, delimiter=",", null="-", empty="", bool_true="YES", bool_false="NO"
+ ):
with open(table, newline="", encoding="utf-8") as f:
reader = csv.reader(f, delimiter=delimiter)
headers = []
@@ -390,17 +397,17 @@ class IfcCsv:
elif len(attributes) == len(headers) - 1:
attributes.insert(0, "") # The GlobalId column
continue
- self.process_row(ifc_file, row, headers, attributes, null, bool_true, bool_false)
+ self.process_row(ifc_file, row, headers, attributes, null, empty, bool_true, bool_false)
- def import_xlsx(self, ifc_file, table, attributes, null, bool_true, bool_false):
+ def import_xlsx(self, ifc_file, table, attributes, null, empty, bool_true, bool_false):
df = pd.read_excel(table)
- self.import_pd(ifc_file, df, attributes, null, bool_true, bool_false)
+ self.import_pd(ifc_file, df, attributes, null, empty, bool_true, bool_false)
- def import_ods(self, ifc_file, table, attributes, null, bool_true, bool_false):
+ def import_ods(self, ifc_file, table, attributes, null, empty, bool_true, bool_false):
df = pd.read_excel(table, engine="odf")
- self.import_pd(ifc_file, df, attributes, null, bool_true, bool_false)
+ self.import_pd(ifc_file, df, attributes, null, empty, bool_true, bool_false)
- def import_pd(self, ifc_file, df, attributes=None, null="-", bool_true="YES", bool_false="NO"):
+ def import_pd(self, ifc_file, df, attributes=None, null="-", empty="", bool_true="YES", bool_false="NO"):
headers = df.columns.tolist()
if not attributes:
@@ -409,9 +416,9 @@ class IfcCsv:
attributes.insert(0, "") # The GlobalId column
for _, row in df.iterrows():
- self.process_row(ifc_file, row.tolist(), headers, attributes, null, bool_true, bool_false)
+ self.process_row(ifc_file, row.tolist(), headers, attributes, null, empty, bool_true, bool_false)
- def process_row(self, ifc_file, row, headers, attributes, null, bool_true, bool_false):
+ def process_row(self, ifc_file, row, headers, attributes, null, empty, bool_true, bool_false):
try:
element = ifc_file.by_guid(row[0])
except:
@@ -422,6 +429,8 @@ class IfcCsv:
continue # Skip GlobalId
if value == null:
value = None
+ elif value == empty:
+ value = ""
elif value == bool_true:
value = True
elif value == bool_false:
@@ -437,7 +446,10 @@ if __name__ == "__main__":
parser.add_argument("-f", "--format", type=str, default="csv", help="The format, chosen from csv, ods, or xlsx")
parser.add_argument("-d", "--delimiter", type=str, default=",", help="The delimiter in CSV. Defaults to a comma.")
parser.add_argument(
- "-n", "--null", type=str, default="-", help="How to represent null values. Defaults to a hyphen."
+ "-n", "--null", type=str, default="N/A", help="How to represent null values. Defaults to N/A."
+ )
+ parser.add_argument(
+ "-e", "--empty", type=str, default="-", help="How to represent empty strings. Defaults to a hyphen."
)
parser.add_argument("--bool_true", type=str, default="YES", help="How to represent true values. Defaults to YES.")
parser.add_argument("--bool_false", type=str, default="NO", help="How to represent false values. Defaults to NO.")
@@ -448,9 +460,7 @@ if __name__ == "__main__":
nargs="+",
help="Specify attributes that are part of the extract, using the IfcQuery syntax such as 'class', 'Name' or 'Pset_Foo.Bar'",
)
- parser.add_argument(
- "-h", "--headers", nargs="+", help="Specify human readable headers that correlate to each attribute."
- )
+ parser.add_argument("--headers", nargs="+", help="Specify human readable headers that correlate to each attribute.")
parser.add_argument("--sort", nargs="+", help="Specify one or more attributes to sort by.")
parser.add_argument("--order", nargs="+", help="Choose the sort order from ASC or DESC for each sorted attribute.")
parser.add_argument("--export", action="store_true", help="Export from IFC to the desired format.")
@@ -473,6 +483,7 @@ if __name__ == "__main__":
format=args.format,
delimiter=args.delimiter,
null=args.null,
+ empty=args.empty,
bool_true=args.bool_true,
bool_false=args.bool_false,
sort=sort,
@@ -481,6 +492,11 @@ if __name__ == "__main__":
ifc_csv = IfcCsv()
ifc_file = ifcopenshell.open(args.ifc)
ifc_csv.Import(
- ifc_file, args.spreadsheet, attributes=args.attributes or [], delimiter=args.delimiter, null=args.null
+ ifc_file,
+ args.spreadsheet,
+ attributes=args.attributes or [],
+ delimiter=args.delimiter,
+ null=args.null,
+ empty=args.empty,
)
ifc_file.write(args.ifc)
diff --git a/src/ifcdiff/ifcdiff.py b/src/ifcdiff/ifcdiff.py
index 4c61782a0f..32286882d0 100755
--- a/src/ifcdiff/ifcdiff.py
+++ b/src/ifcdiff/ifcdiff.py
@@ -190,19 +190,20 @@ class IfcDiff:
shape = iterator.get()
element = ifc.by_id(shape.id)
geometry = shape.geometry
- shapes[element.GlobalId] = {
- "total_verts": len(geometry.verts),
- "sum_verts": sum(geometry.verts),
- "min_vert": min(geometry.verts),
- "max_vert": max(geometry.verts),
- "matrix": tuple(shape.transformation.matrix.data),
- "openings": sorted(
- [o.RelatedOpeningElement.GlobalId for o in getattr(element, "HasOpenings", []) or []]
- ),
- "projections": sorted(
- [o.RelatedFeatureElement.GlobalId for o in getattr(element, "HasProjections", []) or []]
- ),
- }
+ if geometry.verts:
+ shapes[element.GlobalId] = {
+ "total_verts": len(geometry.verts),
+ "sum_verts": sum(geometry.verts),
+ "min_vert": min(geometry.verts),
+ "max_vert": max(geometry.verts),
+ "matrix": tuple(shape.transformation.matrix.data),
+ "openings": sorted(
+ [o.RelatedOpeningElement.GlobalId for o in getattr(element, "HasOpenings", []) or []]
+ ),
+ "projections": sorted(
+ [o.RelatedFeatureElement.GlobalId for o in getattr(element, "HasProjections", []) or []]
+ ),
+ }
if not iterator.next():
break
return shapes
diff --git a/src/ifcfm/ifcfm/__init__.py b/src/ifcfm/ifcfm/__init__.py
new file mode 100644
index 0000000000..c5414a46ac
--- /dev/null
+++ b/src/ifcfm/ifcfm/__init__.py
@@ -0,0 +1,253 @@
+# IfcFM - IFC for facility management
+# Copyright (C) 2023 Dion Moult
+#
+# This file is part of IfcFM.
+#
+# IfcFM is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# IfcFM 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 Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with IfcFM. If not, see .
+
+import os
+import re
+import csv
+import importlib
+
+try:
+ from openpyxl import Workbook
+ from openpyxl.styles import PatternFill
+except:
+ pass # No XLSX support
+
+try:
+ from odf.opendocument import OpenDocumentSpreadsheet
+ from odf.style import Style, TableCellProperties
+ from odf.table import Table, TableRow, TableCell
+ from odf.text import P
+except:
+ pass # No ODF support
+
+
+try:
+ import pandas as pd
+except:
+ pass # No Pandas support
+
+
+class Parser:
+ def __init__(self, preset="basic"):
+ self.file = None
+ self.preset = preset
+ self.categories = {}
+ self.config = None
+ self.get_custom_element_data = {}
+ self.duplicate_keys = []
+
+ if isinstance(preset, str):
+ module = importlib.import_module(f"ifcfm.{preset}")
+ self.config = getattr(module, "config")
+ else:
+ self.config = preset
+
+ def parse(self, ifc_file):
+ for category_name, category_config in self.config["categories"].items():
+ self.categories.setdefault(category_name, {})
+ for element in category_config["get_category_elements"](ifc_file):
+ get_element_data = category_config["get_element_data"]
+
+ if isinstance(get_element_data, dict):
+ data = {}
+ for key, query in get_element_data.items():
+ data[key] = ifcopenshell.util.selector.get_element_value(element, query)
+ else:
+ data = get_element_data(ifc_file, element) or {}
+
+ get_custom_element_data = self.get_custom_element_data.get(category_name, lambda x, y: None)
+ if isinstance(get_custom_element_data, dict):
+ custom_data = {}
+ for key, query in get_custom_element_data.items():
+ custom_data[key] = ifcopenshell.util.selector.get_element_value(element, query)
+ else:
+ custom_data = get_custom_element_data(ifc_file, element) or {}
+
+ data.update(custom_data)
+
+ if data:
+ key = data["key"]
+ del data["key"]
+ if key in self.categories[category_name]:
+ self.duplicate_keys.append((self.categories[category_name][key], data))
+ self.categories[category_name][key] = data
+
+ def exclude_categories(self, names):
+ for name in names:
+ if name in self.config["categories"]:
+ del self.config["categories"][name]
+
+ def exclude_element_data(self, category, names):
+ headers = self.config["categories"][category]["headers"]
+ self.config["categories"][category]["headers"] = [h for h in headers if h not in names]
+
+
+class Writer:
+ def __init__(self, parser):
+ self.parser = parser
+ if isinstance(self.parser.preset, str):
+ module = importlib.import_module(f"ifcfm.{self.parser.preset}")
+ self.config = getattr(module, "config")
+ elif isinstance(self.parser.preset, dict):
+ self.config = self.parser.preset["config"]
+ else:
+ self.config = getattr(self.parser.preset, "config")
+
+ def write(self, null="N/A", empty="-", bool_true="YES", bool_false="NO"):
+ self.categories = {}
+ null = self.config.get("null", null)
+ empty = self.config.get("empty", empty)
+ bool_true = self.config.get("bool_true", bool_true)
+ bool_false = self.config.get("bool_false", bool_false)
+ for category, config in self.config["categories"].items():
+ data = self.parser.categories.get(category, None)
+ headers = config["headers"]
+
+ if not data:
+ self.categories[category] = {"headers": headers, "rows": []}
+ continue
+
+ if not headers:
+ headers = list(data[list(data.keys())[0]].keys())
+
+ rows = []
+ for row in data.values():
+ processed_row = []
+ for header in headers:
+ value = row[header]
+ if value is None:
+ value = null
+ elif value == "":
+ value = empty
+ elif value is True:
+ value = bool_true
+ elif value is False:
+ value = bool_false
+ processed_row.append(value)
+ rows.append(processed_row)
+
+ sort = self.config.get("categories", {}).get(category, {}).get("sort", None)
+ if sort:
+
+ def natural_sort(value):
+ if isinstance(value, str):
+ convert = lambda text: int(text) if text.isdigit() else text.lower()
+ return [convert(c) for c in re.split("([0-9]+)", value)]
+ return value
+
+ # Sort least important keys first, then more important keys.
+ # https://stackoverflow.com/questions/11476371/sort-by-multiple-keys-using-different-orderings
+ for sort_data in reversed(sort):
+ i = headers.index(sort_data["name"])
+ reverse = sort_data["order"] == "DESC"
+ rows = sorted(rows, key=lambda x: natural_sort(x[i]), reverse=reverse)
+ self.categories[category] = {"headers": headers, "rows": rows}
+
+ def write_csv(self, output, delimiter=","):
+ filename = None
+ if len(self.categories.keys()) == 1 and "." in os.path.basename(output):
+ filename = output
+ for category, data in self.categories.items():
+ category_filename = filename or os.path.join(output, f"{category}.csv")
+ with open(category_filename, "w", newline="", encoding="utf-8") as f:
+ writer = csv.writer(f, delimiter=delimiter)
+ writer.writerow(data["headers"])
+ for row in data["rows"]:
+ writer.writerow(row)
+
+ def write_ods(self, output):
+ doc = OpenDocumentSpreadsheet()
+
+ for key, value in self.config.get("colours", {}).items():
+ style = Style(name=key, family="table-cell")
+ style.addElement(TableCellProperties(backgroundcolor="#" + value))
+ doc.automaticstyles.addElement(style)
+
+ for category, data in self.categories.items():
+ colours = self.config.get("categories", {}).get(category, {}).get("colours", [])
+
+ table = Table(name=category)
+ tr = TableRow()
+ for header in data["headers"]:
+ tc = TableCell(valuetype="string", stylename="h")
+ tc.addElement(P(text=header))
+ tr.addElement(tc)
+ table.addElement(tr)
+ for row in data["rows"]:
+ tr = TableRow()
+ c = 0
+ for col in row:
+ if c >= len(colours):
+ cell_format = "n"
+ else:
+ cell_format = colours[c]
+ tc = TableCell(valuetype="string", stylename=cell_format)
+ tc.addElement(P(text=str(col)))
+ tr.addElement(tc)
+ c += 1
+ table.addElement(tr)
+ doc.spreadsheet.addElement(table)
+
+ if len(output) > 4 and output[-4:].lower() == ".ods":
+ output = output[0:-4]
+
+ doc.save(output, True)
+
+ def write_xlsx(self, output):
+ workbook = Workbook()
+
+ cell_formats = {}
+ for key, value in self.config.get("colours", {}).items():
+ fill = PatternFill(start_color=value, end_color=value, fill_type="solid")
+ cell_formats[key] = fill
+
+ for category, data in self.categories.items():
+ colours = self.config.get("categories", {}).get(category, {}).get("colours", [])
+
+ if category in workbook.sheetnames:
+ worksheet = workbook[category]
+ else:
+ worksheet = workbook.create_sheet(category)
+
+ r = 1 # Openpyxl uses 1-based indexing
+ c = 1
+ for header in data["headers"]:
+ cell = worksheet.cell(row=r, column=c, value=header)
+ cell.fill = cell_formats["h"]
+ c += 1
+
+ r += 1
+ for row in data["rows"]:
+ c = 1
+ for col in row:
+ if c > len(colours): # Adjusted the comparison
+ cell_format = "n"
+ else:
+ cell_format = colours[c - 1] # Adjusted the indexing
+ cell = worksheet.cell(row=r, column=c, value=col)
+ cell.fill = cell_formats[cell_format]
+ c += 1
+ r += 1
+
+ workbook.save(output)
+
+ def write_pd(self):
+ results = {}
+ for category, data in self.categories.items():
+ results[category] = pd.DataFrame(data["rows"], columns=data["headers"])
+ return results
diff --git a/src/ifcfm/ifcfm/__main__.py b/src/ifcfm/ifcfm/__main__.py
new file mode 100644
index 0000000000..6b0fac9cb7
--- /dev/null
+++ b/src/ifcfm/ifcfm/__main__.py
@@ -0,0 +1,37 @@
+import ifcfm
+import argparse
+import ifcopenshell
+
+parser = argparse.ArgumentParser(description="Extracts FM data from IFC to spreadsheets")
+parser.add_argument(
+ "-p",
+ "--preset",
+ type=str,
+ default="basic",
+ help="The FM standard to extract. Built-in preset standards include cobie24, cobie3, aohbsem, and basic.",
+)
+parser.add_argument("-i", "--ifc", type=str, required=True, help="The IFC file")
+parser.add_argument("-s", "--spreadsheet", type=str, default="output.ods", help="The spreadsheet file, or directory if the format is csv. Defaults to output.ods")
+parser.add_argument(
+ "-f", "--format", type=str, default="ods", help="The format, chosen from csv, ods, or xlsx. Defaults to ods."
+)
+parser.add_argument("-d", "--delimiter", type=str, default=",", help="The delimiter in CSV. Defaults to a comma.")
+parser.add_argument("-n", "--null", type=str, default="N/A", help="How to represent null values. Defaults to N/A.")
+parser.add_argument(
+ "-e", "--empty", type=str, default="-", help="How to represent empty strings. Defaults to a hyphen."
+)
+parser.add_argument("--bool_true", type=str, default="YES", help="How to represent true values. Defaults to YES.")
+parser.add_argument("--bool_false", type=str, default="NO", help="How to represent false values. Defaults to NO.")
+args = parser.parse_args()
+
+ifc_file = ifcopenshell.open(args.ifc)
+parser = ifcfm.Parser(preset=args.preset)
+parser.parse(ifc_file)
+writer = ifcfm.Writer(parser)
+writer.write(null=args.null, empty=args.empty, bool_true=args.bool_true, bool_false=args.bool_false)
+if args.format == "csv":
+ writer.write_csv(args.spreadsheet, delimiter=args.delimiter)
+elif args.format == "ods":
+ writer.write_ods(args.spreadsheet)
+elif args.format == "xlsx":
+ writer.write_xlsx(args.spreadsheet)
diff --git a/src/ifcfm/ifcfm/basic.py b/src/ifcfm/ifcfm/basic.py
new file mode 100644
index 0000000000..f4ccb81ffd
--- /dev/null
+++ b/src/ifcfm/ifcfm/basic.py
@@ -0,0 +1,368 @@
+# IfcFM - IFC for facility management
+# Copyright (C) 2023 Dion Moult
+#
+# This file is part of IfcFM.
+#
+# IfcFM is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# IfcFM 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 Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with IfcFM. If not, see .
+
+import ifcopenshell
+import ifcopenshell.util.fm
+import ifcopenshell.util.date
+import ifcopenshell.util.system
+import ifcopenshell.util.placement
+import ifcopenshell.util.classification
+
+
+def get_facilities(ifc_file):
+ return ifc_file.by_type("IfcBuilding")
+
+
+def get_storeys(ifc_file):
+ return ifc_file.by_type("IfcBuildingStorey")
+
+
+def get_spaces(ifc_file):
+ return ifc_file.by_type("IfcSpace")
+
+
+def get_zones(ifc_file):
+ zones = []
+ for zone in ifc_file.by_type("IfcZone"):
+ for rel in zone.IsGroupedBy:
+ zones.extend([(zone, space) for space in rel.RelatedObjects])
+ return zones
+
+
+def get_element_types(ifc_file):
+ return ifcopenshell.util.fm.get_fmhem_types(ifc_file)
+
+
+def get_elements(ifc_file):
+ elements = set()
+ for element_type in ifcopenshell.util.fm.get_fmhem_types(ifc_file):
+ elements.update(ifcopenshell.util.element.get_types(element_type))
+ return elements
+
+
+def get_systems(ifc_file):
+ return ifc_file.by_type("IfcSystem")
+
+
+def get_facility_data(ifc_file, element):
+ return {
+ "key": element.Name,
+ "Name": element.Name,
+ "ProjectName": ifc_file.by_type("IfcProject")[0].Name,
+ "SiteName": getattr(get_facility_parent(element, "IfcSite"), "Name", None),
+ "Category": get_classification(element),
+ "AuthorOrganizationName": get_owner_name(element),
+ "AuthorDate": get_owner_creation_date(element),
+ "ModelSoftware": get_owner_application(element),
+ "ModelProjectID": ifc_file.by_type("IfcProject")[0].GlobalId,
+ "ModelSiteID": getattr(get_facility_parent(element, "IfcSite"), "GlobalId", None),
+ "ModelBuildingID": element.GlobalId,
+ "LinearUnits": "millimeters",
+ "AreaUnits": "square meters",
+ "Phase": ifc_file.by_type("IfcProject")[0].Phase,
+ }
+
+
+def get_storey_data(ifc_file, element):
+ return {
+ "key": element.Name,
+ "Name": element.Name,
+ "Category": "Level",
+ "AuthorOrganizationName": get_owner_name(element),
+ "AuthorDate": get_owner_creation_date(element),
+ "ModelSoftware": get_owner_application(element),
+ "ModelObject": element.is_a(),
+ "ModelID": element.GlobalId,
+ "Elevation": ifcopenshell.util.placement.get_storey_elevation(element),
+ }
+
+
+def get_space_data(ifc_file, element):
+ psets = ifcopenshell.util.element.get_psets(element)
+ return {
+ "key": element.Name,
+ "Name": element.Name,
+ "Description": element.LongName,
+ "Category": get_classification(element),
+ "LevelName": getattr(get_facility_parent(element, "IfcBuildingStorey"), "Name", None),
+ "AuthorOrganizationName": get_owner_name(element),
+ "AuthorDate": get_owner_creation_date(element),
+ "ModelSoftware": get_owner_application(element),
+ "ModelID": element.GlobalId,
+ "AreaGross": get_property(psets, "Qto_SpaceBaseQuantities", "GrossFloorArea", decimals=2),
+ "AreaNet": get_property(psets, "Qto_SpaceBaseQuantities", "NetFloorArea", decimals=2),
+ }
+
+
+def get_zone_data(ifc_file, element):
+ zone, space = element
+ return {
+ "key": (zone.Name or "Unnamed") + (space.Name or "Unnamed"),
+ "Name": zone.Name,
+ "SpaceName": space.Name,
+ "AuthorOrganizationName": get_owner_name(zone),
+ "AuthorDate": get_owner_creation_date(zone),
+ "ModelSoftware": get_owner_application(zone),
+ "ModelID": zone.GlobalId,
+ }
+
+
+def get_element_type_data(ifc_file, element):
+ psets = ifcopenshell.util.element.get_psets(element)
+ return {
+ "key": element.Name,
+ "Name": element.Name,
+ "Description": element.Description,
+ "Category": get_classification(element),
+ "AuthorOrganizationName": get_owner_name(element),
+ "AuthorDate": get_owner_creation_date(element),
+ "ModelSoftware": get_owner_application(element),
+ "ModelObject": "{}[{}]".format(element.is_a(), ifcopenshell.util.element.get_predefined_type(element)),
+ "ModelID": element.GlobalId,
+ "ModelTag": element.Tag,
+ "Manufacturer": get_property(psets, "Pset_ManufacturerTypeInformation", "Manufacturer"),
+ "ModelReference": get_property(psets, "Pset_ManufacturerTypeInformation", "ModelReference"),
+ "ModelLabel": get_property(psets, "Pset_ManufacturerTypeInformation", "ModelLabel"),
+ "PointOfContact": get_property(psets, "Pset_Warranty", "PointOfContact"),
+ "WarrantyPeriod": get_property(psets, "Pset_Warranty", "WarrantyPeriod"),
+ }
+
+
+def get_element_data(ifc_file, element):
+ space = ifcopenshell.util.element.get_container(element)
+ space_name = space.Name if space.is_a("IfcSpace") else None
+ systems = ifcopenshell.util.system.get_element_systems(element)
+ system = systems[0].Name if systems else None
+ psets = ifcopenshell.util.element.get_psets(element)
+ return {
+ "key": element.Name,
+ "Name": element.Name,
+ "TypeName": ifcopenshell.util.element.get_type(element).Name,
+ "SpaceName": space_name,
+ "SystemName": system,
+ "AuthorOrganizationName": get_owner_name(element),
+ "AuthorDate": get_owner_creation_date(element),
+ "ModelSoftware": get_owner_application(element),
+ "ModelObject": "{}[{}]".format(element.is_a(), ifcopenshell.util.element.get_predefined_type(element)),
+ "ModelID": element.GlobalId,
+ "ModelTag": element.Tag,
+ "SerialNumber": get_property(psets, "Pset_ManufacturerOccurrence", "SerialNumber"),
+ "BarCode": get_property(psets, "Pset_ManufacturerOccurrence", "BarCode"),
+ "BatchReference": get_property(psets, "Pset_ManufacturerOccurrence", "BatchReference"),
+ "TagNumber": get_property(psets, "Pset_ConstructionOccurrence", "TagNumber"),
+ "AssetIdentifier": get_property(psets, "Pset_ConstructionOccurrence", "AssetIdentifier"),
+ "InstallationDate": get_property(psets, "Pset_ConstructionOccurrence", "InstallationDate"),
+ "WarrantyStartDate": get_property(psets, "Pset_Warranty", "WarrantyStartDate"),
+ }
+
+
+def get_system_data(ifc_file, element):
+ return {
+ "key": element.Name,
+ "Name": element.Name,
+ "Description": element.Description,
+ "Category": get_classification(element),
+ "AuthorOrganizationName": get_owner_name(element),
+ "AuthorDate": get_owner_creation_date(element),
+ "ModelSoftware": get_owner_application(element),
+ "ModelID": element.GlobalId,
+ }
+
+
+def get_owner_name(element):
+ if not getattr(element, "OwnerHistory", None):
+ return
+ return element.OwnerHistory.OwningUser.TheOrganization.Name
+
+
+def get_owner_creation_date(element):
+ if not getattr(element, "OwnerHistory", None):
+ return
+ return ifcopenshell.util.date.ifc2datetime(element.OwnerHistory.CreationDate).isoformat()
+
+
+def get_owner_application(element):
+ if not getattr(element, "OwnerHistory", None):
+ return
+ return element.OwnerHistory.OwningApplication.ApplicationFullName
+
+
+def get_facility_parent(element, ifc_class):
+ parent = ifcopenshell.util.element.get_aggregate(element)
+ while parent:
+ if parent.is_a(ifc_class):
+ return parent
+ if parent.is_a("IfcProject"):
+ return
+ parent = ifcopenshell.util.element.get_aggregate(parent)
+
+
+def get_classification(element):
+ references = list(ifcopenshell.util.classification.get_references(element))
+ if references:
+ if hasattr(references[0], "Identification"):
+ return "{}:{}".format(references[0].Identification, references[0].Name)
+ return "{}:{}".format(references[0].ItemReference, references[0].Name)
+
+
+def get_property(psets, pset_name, prop_name, decimals=None):
+ if pset_name in psets:
+ result = psets[pset_name].get(prop_name, None)
+ if decimals is None or result is None:
+ return result
+ return round(result, decimals)
+
+
+config = {
+ "colours": {
+ "h": "dddddd", # Header data
+ "p": "dc8774", # Primary identification data
+ "s": "b8dd73", # Secondary asset data
+ "r": "eda786", # Internal reference
+ "e": "96c7d0", # External / autogenerated data
+ "o": "ddb873", # Conditional / optional data
+ "n": "eeeeee", # Other data
+ "b": "000000", # Not in scope
+ },
+ "categories": {
+ "Facilities": {
+ "headers": [
+ "Name",
+ "ProjectName",
+ "SiteName",
+ "Category",
+ "AuthorOrganizationName",
+ "AuthorDate",
+ "ModelSoftware",
+ "ModelProjectID",
+ "ModelSiteID",
+ "ModelBuildingID",
+ "LinearUnits",
+ "AreaUnits",
+ "Phase",
+ ],
+ "colours": "ppppreeeeesss",
+ "sort": [{"name": "Name", "order": "ASC"}],
+ "get_category_elements": get_facilities,
+ "get_element_data": get_facility_data,
+ },
+ "Storeys": {
+ "headers": [
+ "Name",
+ "Category",
+ "AuthorOrganizationName",
+ "AuthorDate",
+ "ModelSoftware",
+ "ModelObject",
+ "ModelID",
+ "Elevation",
+ ],
+ "colours": "ppreeees",
+ "sort": [{"name": "Elevation", "order": "ASC"}, {"name": "Name", "order": "ASC"}],
+ "get_category_elements": get_storeys,
+ "get_element_data": get_storey_data,
+ },
+ "Spaces": {
+ "headers": [
+ "Name",
+ "Description",
+ "Category",
+ "LevelName",
+ "AuthorOrganizationName",
+ "AuthorDate",
+ "ModelSoftware",
+ "ModelID",
+ "AreaGross",
+ "AreaNet",
+ ],
+ "colours": "ppprreeess",
+ "sort": [{"name": "LevelName", "order": "ASC"}, {"name": "Name", "order": "ASC"}],
+ "get_category_elements": get_spaces,
+ "get_element_data": get_space_data,
+ },
+ "Zones": {
+ "headers": ["Name", "SpaceName", "AuthorOrganizationName", "AuthorDate", "ModelSoftware", "ModelID"],
+ "colours": "prreee",
+ "sort": [{"name": "Name", "order": "ASC"}],
+ "get_category_elements": get_zones,
+ "get_element_data": get_zone_data,
+ },
+ "ElementTypes": {
+ "headers": [
+ "Name",
+ "Description",
+ "Category",
+ "AuthorOrganizationName",
+ "AuthorDate",
+ "ModelSoftware",
+ "ModelObject",
+ "ModelID",
+ "ModelTag",
+ "Manufacturer",
+ "ModelReference",
+ "ModelLabel",
+ "PointOfContact",
+ "WarrantyPeriod",
+ ],
+ "colours": "pppreeeeesssss",
+ "sort": [{"name": "ModelObject", "order": "ASC"}, {"name": "Name", "order": "ASC"}],
+ "get_category_elements": get_element_types,
+ "get_element_data": get_element_type_data,
+ },
+ "Elements": {
+ "headers": [
+ "Name",
+ "TypeName",
+ "SpaceName",
+ "SystemName",
+ "AuthorOrganizationName",
+ "AuthorDate",
+ "ModelSoftware",
+ "ModelObject",
+ "ModelID",
+ "ModelTag",
+ "SerialNumber",
+ "BarCode",
+ "BatchReference",
+ "TagNumber",
+ "AssetIdentifier",
+ "InstallationDate",
+ "WarrantyStartDate",
+ ],
+ "colours": "prrrreeeeesssssss",
+ "sort": [{"name": "TypeName", "order": "ASC"}, {"name": "Name", "order": "ASC"}],
+ "get_category_elements": get_elements,
+ "get_element_data": get_element_data,
+ },
+ "Systems": {
+ "headers": [
+ "Name",
+ "Description",
+ "Category",
+ "AuthorOrganizationName",
+ "AuthorDate",
+ "ModelSoftware",
+ "ModelID",
+ ],
+ "colours": "pppreee",
+ "sort": [{"name": "Name", "order": "ASC"}],
+ "get_category_elements": get_systems,
+ "get_element_data": get_system_data,
+ },
+ },
+}
diff --git a/src/ifcfm/ifcfm/cobie24.py b/src/ifcfm/ifcfm/cobie24.py
new file mode 100644
index 0000000000..6f385f1278
--- /dev/null
+++ b/src/ifcfm/ifcfm/cobie24.py
@@ -0,0 +1,1509 @@
+# IfcFM - IFC for facility management
+# Copyright (C) 2023 Dion Moult
+#
+# This file is part of IfcFM.
+#
+# IfcFM is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# IfcFM 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 Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with IfcFM. If not, see .
+
+import ifcopenshell
+import ifcopenshell.util.fm
+import ifcopenshell.util.date
+import ifcopenshell.util.system
+import ifcopenshell.util.placement
+import ifcopenshell.util.classification
+
+
+# The original BIMServer plugin has a function called ifcToCOBie:
+# https://github.com/opensourceBIM/COBie-plugins/blob/master/COBieShared/src/org/bimserver/cobie/shared/serialization/COBieTabSerializer.java#L54
+# This calls various serialisers here:
+# https://github.com/opensourceBIM/COBie-plugins/tree/master/COBieShared/src/org/bimserver/cobie/shared/serialization/util
+# Some settings are also defined here:
+# https://github.com/opensourceBIM/COBie-plugins/blob/master/COBiePlugins/lib/IfcToCobieConfig.xml
+# Note that the following categories are not implemented in the BIMServer COBie-Plugins:
+# Impact, Coordinate, Issue, Picklist
+
+
+def get_contacts(ifc_file):
+ return ifc_file.by_type("IfcPersonAndOrganization")
+
+
+def get_facilities(ifc_file):
+ return ifc_file.by_type("IfcBuilding")
+
+
+def get_floors(ifc_file):
+ return [
+ e
+ for e in ifc_file.by_type("IfcBuildingStorey")
+ if ifcopenshell.util.element.get_aggregate(e).is_a("IfcBuilding")
+ ]
+
+
+def get_spaces(ifc_file):
+ return ifc_file.by_type("IfcSpace")
+
+
+def get_zones(ifc_file):
+ results = []
+ zones = ifc_file.by_type("IfcZone")
+ for zone in zones or []:
+ has_space = False
+ for rel in zone.IsGroupedBy:
+ items = [(zone, space) for space in rel.RelatedObjects if space.is_a("IfcSpace") and val(space.Name)]
+ if items:
+ results.extend(items)
+ has_space = True
+ if not has_space:
+ results.append((zone, None))
+ if zones:
+ return results
+
+ zone_spaces = {}
+ for space in ifc_file.by_type("IfcSpace"):
+ for _, props in ifcopenshell.util.element.get_psets(space).items():
+ for name, value in props.items():
+ if "ZoneName" in name:
+ zone_name = val(value)
+ space_name = val(space.Name)
+ category = name
+ zone_key = str(name) + "," + str(category)
+ zone_spaces.setdefault(zone_key, [])
+ if space_name not in zone_spaces[zone_key]:
+ zone_spaces[zone_key].append(space_name)
+ results.append(((zone_name, category), space_name))
+ return results
+
+
+def get_types(ifc_file):
+ return ifcopenshell.util.fm.get_cobie_types(ifc_file)
+
+
+def get_components(ifc_file):
+ elements = set()
+ for element_type in get_types(ifc_file):
+ elements.update(ifcopenshell.util.element.get_types(element_type))
+ return elements
+
+
+def get_systems(ifc_file):
+ results = []
+ components = get_components(ifc_file)
+ if ifc_file.schema == "IFC2X3":
+ systems = ifc_file.by_type("IfcSystem", include_subtypes=False)
+ else:
+ systems = ifc_file.by_type("IfcSystem", include_subtypes=False) + ifc_file.by_type("IfcDistributionSystem")
+ for system in systems:
+ for element in ifcopenshell.util.system.get_system_elements(system):
+ if element in components:
+ results.append((system, element))
+ return results
+
+
+def get_assemblies(ifc_file):
+ results = []
+ layer_sets = ifc_file.by_type("IfcMaterialLayerSet")
+ layer_sets = [] # This is temporarily overridden because it is unclear exactly how this is stored in Type.
+ for layer_set in layer_sets:
+ for layer in layer_set.MaterialLayers:
+ results.append((None, layer_set, layer.Material))
+ rels = ifc_file.by_type("IfcRelAggregates") + ifc_file.by_type("IfcRelNests")
+ types = get_types(ifc_file)
+ components = get_components(ifc_file)
+ for rel in rels:
+ if rel.RelatingObject.is_a("IfcSpace"):
+ for related_object in rel.RelatedObjects:
+ if related_object.is_a("IfcSpace"):
+ results.append((rel, rel.RelatingObject, related_object))
+ elif rel.RelatingObject.is_a("IfcZone"):
+ for related_object in rel.RelatedObjects:
+ if related_object.is_a("IfcZone"):
+ results.append((rel, rel.RelatingObject, related_object))
+ elif rel.RelatingObject in types:
+ for related_object in rel.RelatedObjects:
+ if related_object in types:
+ results.append((rel, rel.RelatingObject, related_object))
+ elif rel.RelatingObject in components:
+ for related_object in rel.RelatedObjects:
+ if related_object in components:
+ results.append((rel, rel.RelatingObject, related_object))
+ elif rel.RelatingObject.is_a() in ("IfcSystem", "IfcDistributionSystem"):
+ for related_object in rel.RelatedObjects:
+ if related_object.is_a() in ("IfcSystem", "IfcDistributionSystem"):
+ results.append((rel, rel.RelatingObject, related_object))
+ return results
+
+
+def get_connections(ifc_file):
+ return ifc_file.by_type("IfcRelConnectsPorts")
+
+
+def get_spares(ifc_file):
+ return ifc_file.by_type("IfcConstructionProductResource")
+
+
+def get_resources(ifc_file):
+ return ifc_file.by_type("IfcConstructionEquipmentResource")
+
+
+def get_jobs(ifc_file):
+ return ifc_file.by_type("IfcTask")
+
+
+def get_documents(ifc_file):
+ # The original COBie-Plugins assumes a single related object per rel. I think this was wrong.
+ results = []
+ for rel in ifc_file.by_type("IfcRelAssociatesDocument"):
+ doc = rel.RelatingDocument
+ if doc.is_a("IfcDocumentInformation"):
+ for related_object in rel.RelatedObjects:
+ results.append((rel, doc, related_object))
+ elif doc.is_a("IfcDocumentReference") and doc.ReferencedDocument:
+ for related_object in rel.RelatedObjects:
+ results.append((rel, doc.ReferencedDocument, related_object))
+ return results
+
+
+def get_attributes(ifc_file):
+ results = []
+ history = get_history(ifc_file)
+ created_by = get_email_from_history(history) if history else None
+ created_on = ifcopenshell.util.date.ifc2datetime(history.CreationDate).isoformat() if history else None
+ external_system = history.OwningApplication.ApplicationFullName if history else None
+ get_sheets = {
+ "Facility": get_facilities,
+ "Floor": get_floors,
+ "Space": get_spaces,
+ "Type": get_types,
+ "Component": get_components,
+ }
+
+ # COBie-Plugins includes what seems like a whole bunch of arbitrary "at the
+ # time it seemed to help" exclusion names. I don't like that strategy. If
+ # you've got garbage in your model, clean it out first.
+
+ # fmt: off
+ excluded_names = {
+ "Manufacturer",
+ "ModelNumber", "ArticleNumber", "ModelLabel",
+ "WarrantyGuarantorParts", "PointOfContact",
+ "WarrantyGuarantorLabor", "PointOfContact",
+ "WarrantyDescription", "WarrantyIdentifier",
+ "ReplacementCost", "Replacement Cost", "Replacement", "Cost",
+ "NominalLength", "OverallLength",
+ "NominalWidth", "Width",
+ "NominalHeight", "Height",
+ "ModelReference", "Reference",
+ "Shape",
+ "Size",
+ "Color", "Colour",
+ "Finish",
+ "Grade",
+ "Material",
+ "Constituents", "Parts",
+ "Features",
+ "AccessibilityPerformance", "Access",
+ "CodePerformance", "Regulation",
+ "SustainabilityPerformance", "Environmental",
+ "SerialNumber", "InstallationDate", "WarrantyStartDate", "TagNumber", "BarCode", "AssetIdentifier"
+ }
+ # fmt: on
+
+ for sheet_name, get_sheet in get_sheets.items():
+ for element in get_sheet(ifc_file):
+ for pset_name, props in ifcopenshell.util.element.get_psets(element).items():
+ pset = ifc_file.by_id(props["id"])
+ pset_created_by = get_created_by(pset) or created_by
+ pset_created_on = get_created_on(pset) or created_on
+ pset_external_system = get_external_system(element) or external_system
+ pset_description = val(pset.Description) or pset_name
+ category = get_category(pset)
+ for name, value in props.items():
+ if value == "default" or not val(value):
+ continue
+ elif name in excluded_names:
+ continue
+ elif name == "id":
+ continue
+
+ unit = None
+ if isinstance(value, (int, float)):
+ unit = get_property_unit(props["id"], name)
+
+ allowed_values = None
+ if isinstance(value, (tuple, list)):
+ allowed_values = get_property_unit(props["id"], name)
+
+ data = {
+ "key": str(val(name)) + str(sheet_name) + str(val(element.Name)),
+ "Name": val(name),
+ "CreatedBy": pset_created_by,
+ "CreatedOn": pset_created_on,
+ "Category": category,
+ "SheetName": sheet_name,
+ "RowName": val(element.Name),
+ "Value": value,
+ "Unit": unit,
+ "ExternalSystem": pset_external_system,
+ "ExternalObject": pset_name,
+ "ExternalIdentifier": pset.GlobalId,
+ "Description": pset_description,
+ "AllowedValues": allowed_values,
+ }
+ results.append(data)
+ return results
+
+
+def get_contact_data(ifc_file, element):
+ email = get_email_from_pao(element)
+
+ history = get_history(ifc_file)
+
+ roles = []
+ for actor in [element, element.ThePerson, element.TheOrganization]:
+ for role in actor.Roles or []:
+ if role.Role == "USERDEFINED":
+ if role.UserDefinedRole:
+ roles.append(role.UserDefinedRole)
+ else:
+ roles.append(role.Role)
+
+ organization = element.TheOrganization
+ person = element.ThePerson
+
+ department = get_pao_address(element, "InternalLocation")
+ if not department:
+ for rel in organization.Relates:
+ for org in rel.RelatedOrganizations:
+ if val(org.Name):
+ department = org.Name
+
+ return {
+ "key": email,
+ "Email": email,
+ "CreatedBy": get_email_from_history(history) if history else None,
+ "CreatedOn": ifcopenshell.util.date.ifc2datetime(history.CreationDate).isoformat() if history else None,
+ "Category": ",".join(roles),
+ "Company": getattr(organization, "Name", None),
+ "Phone": get_pao_address(element, "TelephoneNumbers"),
+ "ExternalSystem": history.OwningApplication.ApplicationFullName if history else None,
+ "ExternalObject": element.is_a(),
+ "ExternalIdentifier": email,
+ "Department": department,
+ "OrganizationCode": getattr(organization, "Id", getattr(organization, "Identification", None))
+ or organization.Name,
+ "GivenName": getattr(person, "GivenName", None),
+ "FamilyName": getattr(person, "FamilyName", None),
+ "Street": get_pao_address(element, "AddressLines"),
+ "PostalBox": get_pao_address(element, "PostalBox"),
+ "Town": get_pao_address(element, "Town"),
+ "StateRegion": get_pao_address(element, "Region"),
+ "PostalCode": get_pao_address(element, "PostalCode"),
+ "Country": get_pao_address(element, "Country"),
+ }
+
+
+def get_facility_data(ifc_file, element):
+ site = get_facility_parent(element, "IfcSite")
+ site_name = None
+ site_description = None
+ if site:
+ site_name = val(site.Name) or val(site.LongName) or site.GlobalId
+ site_description = val(site.Description) or val(site.LongName) or val(site.Name)
+
+ project = None
+ project_name = None
+ project_description = None
+ try:
+ project = ifc_file.by_type("IfcProject")[0]
+ project_name = val(project.Name) or val(project.LongName) or project.GlobalId
+ project_description = val(project.Description) or val(project.LongName) or val(project.Name)
+ except:
+ pass
+
+ name = val(element.Name) or val(element.LongName)
+ if not name:
+ name = val(project.Name) or val(project.LongName)
+ if not name and site:
+ name = val(site.Name) or val(site.LongName)
+
+ return {
+ "key": name,
+ "Name": name,
+ "CreatedBy": get_created_by(element),
+ "CreatedOn": get_created_on(element),
+ "Category": get_category(element),
+ "ProjectName": project_name,
+ "SiteName": site_name,
+ "LinearUnits": get_unit_type_name(ifc_file, "LENGTHUNIT"),
+ "AreaUnits": get_unit_type_name(ifc_file, "AREAUNIT"),
+ "VolumeUnits": get_unit_type_name(ifc_file, "VOLUMEUNIT"),
+ "CurrencyUnit": get_unit_type_name(ifc_file, "IfcMonetaryUnit"),
+ "AreaMeasurement": get_area_measurement(element),
+ "ExternalSystem": get_external_system(element),
+ "ExternalProjectObject": "IfcProject",
+ "ExternalProjectIdentifier": project.GlobalId if project else ifcopenshell.guid.new(),
+ "ExternalSiteObject": "IfcSite",
+ "ExternalSiteIdentifier": site.GlobalId if site else ifcopenshell.guid.new(),
+ "ExternalFacilityObject": "IfcBuilding",
+ "ExternalFacilityIdentifier": element.GlobalId,
+ "Description": val(element.Description) or val(element.LongName) or val(element.Name),
+ "ProjectDescription": project_description,
+ "SiteDescription": site_description,
+ "Phase": val(project.Phase) if project else None,
+ }
+
+
+def get_floor_data(ifc_file, element):
+ external_object = element.is_a()
+ if element.ObjectType and element.ObjectType.lower() in ("site", "ifcsite"):
+ external_object = "IfcSite"
+
+ height_names = {
+ "Height",
+ "NetHeight",
+ "GrossHeight",
+ "Net Height",
+ "Gross Height",
+ "StoreyHeight",
+ "Storey Height",
+ "FloorHeight",
+ "Floor Height",
+ }
+
+ height = None
+ for _, props in ifcopenshell.util.element.get_psets(element).items():
+ if height is not None:
+ break
+ for name, value in props.items():
+ if name in height_names and val(value):
+ height = str(value)
+ break
+
+ elevation = getattr(element, "Elevation", "")
+ elevation = "" if elevation is None else str(elevation)
+
+ return {
+ "key": val(element.Name),
+ "Name": val(element.Name),
+ "CreatedBy": get_created_by(element),
+ "CreatedOn": get_created_on(element),
+ "Category": get_category(element),
+ "ExternalSystem": get_external_system(element),
+ "ExternalObject": external_object,
+ "ExternalIdentifier": element.GlobalId,
+ "Description": val(element.Description) or val(element.LongName) or val(element.Name),
+ "Elevation": val(elevation),
+ "Height": height,
+ }
+
+
+def get_space_data(ifc_file, element):
+ floor_name = None
+ floor = ifcopenshell.util.element.get_aggregate(element)
+ if floor and floor.is_a("IfcBuildingStorey"):
+ floor_name = val(floor.Name)
+
+ room_tag = None
+ room_tag_names = {"RoomTag", "Tag", "Room Tag"}
+ usable_height = None
+ usable_height_names = {"FinishCeilingHeight", "Height", "UsableHeight"}
+ gross_area = None
+ gross_area_names = {"GrossFloorArea", "GSA"}
+ net_area = None
+ net_area_names = {"NetFloorArea", "GSA"}
+ for _, props in ifcopenshell.util.element.get_psets(element).items():
+ for name, value in props.items():
+ if not room_tag and name in room_tag_names and val(value):
+ room_tag = str(value)
+ if not usable_height and name in usable_height_names and val(value):
+ usable_height = str(value)
+ if not gross_area and name in gross_area_names and val(value):
+ gross_area = str(value)
+ if not net_area and name in net_area_names and val(value):
+ net_area = str(value)
+
+ return {
+ "key": val(element.Name),
+ "Name": val(element.Name),
+ "CreatedBy": get_created_by(element),
+ "CreatedOn": get_created_on(element),
+ "Category": get_category(element),
+ "FloorName": floor_name,
+ "Description": val(element.Description) or val(element.LongName) or val(element.Name),
+ "ExternalSystem": get_external_system(element),
+ "ExternalObject": element.is_a(),
+ "ExternalIdentifier": element.GlobalId,
+ "RoomTag": room_tag,
+ "UsableHeight": usable_height,
+ "GrossArea": gross_area,
+ "NetArea": net_area,
+ }
+
+
+def get_zone_data(ifc_file, element):
+ zone, space = element
+
+ if isinstance(zone, tuple):
+ name, category = zone
+ history = get_history(ifc_file)
+ return {
+ "key": "-".join([str(name), str(category), str(space)]),
+ "Name": name,
+ "CreatedBy": get_email_from_history(history) if history else None,
+ "CreatedOn": ifcopenshell.util.date.ifc2datetime(history.CreationDate).isoformat() if history else None,
+ "Category": category,
+ "SpaceNames": space,
+ "ExternalSystem": history.OwningApplication.ApplicationFullName if history else None,
+ "ExternalObject": "IfcPropertySingleValue",
+ "ExternalIdentifier": None,
+ "Description": val(name) or val(category),
+ }
+
+ name = zone.Name
+ parent = ifcopenshell.util.element.get_aggregate(zone)
+ if parent and val(parent.Name):
+ name = parent.Name + "-" + name
+
+ category = get_category(zone)
+ space_name = val(space.Name) if space else None
+
+ return {
+ "key": "-".join([str(name), str(category), str(space_name)]),
+ "Name": name,
+ "CreatedBy": get_created_by(zone),
+ "CreatedOn": get_created_on(zone),
+ "Category": category,
+ "SpaceNames": space_name,
+ "ExternalSystem": get_external_system(zone),
+ "ExternalObject": zone.is_a(),
+ "ExternalIdentifier": zone.GlobalId,
+ "Description": val(zone.Description) or zone.Name,
+ }
+
+
+def get_type_data(ifc_file, element):
+ pset_metadata = {}
+ pset_mapping = {
+ "manufacturer": {"Manufacturer"},
+ "model_number": {"ModelNumber", "ArticleNumber", "ModelReference"},
+ "warranty_guarantor_parts": {"WarrantyGuarantorParts", "PointOfContact"},
+ "warranty_guarantor_labor": {"WarrantyGuarantorLabor", "PointOfContact"},
+ "warranty_description": {"WarrantyDescription", "WarrantyIdentifier"},
+ "replacement_cost": {"ReplacementCost", "Replacement Cost", "Replacement", "Cost"},
+ "nominal_length": {"NominalLength", "OverallLength", "Length"},
+ "nominal_width": {"NominalWidth", "OverallWidth", "Width"},
+ # https://github.com/opensourceBIM/COBie-plugins/blob/master/COBiePlugins/lib/IfcToCobieConfig.xml#L104
+ "nominal_height": {"NominalHeight", "Height"}, # Original has a typo "Heght"
+ "model_reference": {"ModelLabel"}, # I believe this is what the intention was, not "ModelReference".
+ "shape": {"Shape"},
+ "size": {"Size"},
+ "color": {"Color", "Colour"},
+ "finish": {"Finish"},
+ "grade": {"Grade"},
+ "material": {"Material"},
+ "constituents": {"Constituents", "Parts"},
+ "features": {"Features"},
+ "accessibility_performance": {"AccessibilityPerformance", "Access"},
+ "code_performance": {"CodePerformance", "Regulation"},
+ "sustainability_performance": {"SustainabilityPerformance", "Environmental"},
+ }
+ asset_type = None
+ asset_type_names = {"AssetType", "AssetAccountingType"}
+ warranty_duration_parts = None
+ warranty_duration_parts_names = {"WarrantyDurationParts", "WarrantyPeriod"}
+ warranty_duration_labor = None
+ warranty_duration_labor_names = {"WarrantyDurationLabor", "WarrantyPeriod"}
+ warranty_duration_unit = None
+ expected_life = None
+ expected_life_names = {"ExpectedLife", "Expected Life", "ServiceLifeDuration", "Expected"}
+ duration_unit = None
+
+ for pset_name, props in ifcopenshell.util.element.get_psets(element).items():
+ pset_warranty_type = None
+ if pset_name == "Pset_Warranty":
+ if "parts" in (props.get("WarrantyIdentifier", "") or "").lower():
+ pset_warranty_type = "parts"
+ elif "labor" in (props.get("WarrantyIdentifier", "") or "").lower():
+ pset_warranty_type = "labor"
+
+ pset = ifc_file.by_id(props["id"])
+
+ for name, value in props.items():
+ for key, prop_names in pset_mapping.items():
+ if not pset_metadata.get(key, None) and name in prop_names and val(value):
+ pset_metadata[key] = str(value)
+
+ if not asset_type and name in asset_type_names and val(value):
+ value = value.strip().lower()
+ if value in ("moveable", "nonfixed"):
+ asset_type = "Moveable"
+ elif value == "fixed":
+ asset_type = "Fixed"
+ if not warranty_duration_parts and name in warranty_duration_parts_names and val(value):
+ warranty_duration_parts = str(value)
+ if not warranty_duration_unit:
+ warranty_duration_unit = get_property_unit(pset, name)
+ if not warranty_duration_labor and name in warranty_duration_labor_names and val(value):
+ warranty_duration_labor = str(value)
+ if not warranty_duration_unit:
+ warranty_duration_unit = get_property_unit(pset, name)
+ if not expected_life and name in expected_life_names and val(value):
+ expected_life = str(value)
+ if not duration_unit:
+ duration_unit = get_property_unit(pset, name)
+
+ if pset_warranty_type == "parts" and val(value):
+ if name == "PointOfContact":
+ # https://github.com/buildingSMART/IFC4.3.x-development/issues/698
+ warranty_guarantor_parts = str(value)
+ elif name == "WarrantyPeriod":
+ warranty_duration_parts = str(value)
+ unit = get_property_unit(pset, name)
+ warranty_duration_unit = unit or warranty_duration_unit
+ elif pset_warranty_type == "labor" and val(value):
+ if name == "PointOfContact":
+ # https://github.com/buildingSMART/IFC4.3.x-development/issues/698
+ warranty_guarantor_labor = str(value)
+ elif name == "WarrantyPeriod":
+ warranty_duration_labor = str(value)
+ unit = get_property_unit(pset, name)
+ warranty_duration_unit = unit or warranty_duration_unit
+
+ if warranty_duration_parts or warranty_duration_labor:
+ if not warranty_duration_unit:
+ warranty_duration_unit = get_unit_type_name(ifc_file, "TIMEUNIT")
+
+ if not asset_type and element.is_a("IfcFurnitureType"):
+ asset_type = "Moveable"
+
+ return {
+ "key": val(element.Name),
+ "Name": val(element.Name),
+ "CreatedBy": get_created_by(element),
+ "CreatedOn": get_created_on(element),
+ "Category": get_category(element),
+ "Description": val(element.Description) or val(element.Name),
+ "AssetType": asset_type,
+ "Manufacturer": pset_metadata.get("manufacturer", None),
+ "ModelNumber": pset_metadata.get("model_number", None),
+ "WarrantyGuarantorParts": pset_metadata.get("warranty_guarantor_parts", None),
+ "WarrantyDurationParts": warranty_duration_parts,
+ "WarrantyGuarantorLabor": pset_metadata.get("warranty_guarantor_labor", None),
+ "WarrantyDurationLabor": warranty_duration_labor,
+ "WarrantyDurationUnit": warranty_duration_unit,
+ "ExternalSystem": get_external_system(element),
+ "ExternalObject": element.is_a(),
+ "ExternalIdentifier": element.GlobalId,
+ "ReplacementCost": pset_metadata.get("replacement_cost", None),
+ "ExpectedLife": expected_life,
+ "DurationUnit": duration_unit,
+ "WarrantyDescription": pset_metadata.get("warranty_description", None),
+ "NominalLength": pset_metadata.get("nominal_length", None),
+ "NominalWidth": pset_metadata.get("nominal_width", None),
+ "NominalHeight": pset_metadata.get("nominal_height", None),
+ "ModelReference": pset_metadata.get("model_reference", None),
+ "Shape": pset_metadata.get("shape", None),
+ "Size": pset_metadata.get("size", None),
+ "Color": pset_metadata.get("color", None),
+ "Finish": pset_metadata.get("finish", None),
+ "Grade": pset_metadata.get("grade", None),
+ "Material": pset_metadata.get("material", None),
+ "Constituents": pset_metadata.get("constituents", None),
+ "Features": pset_metadata.get("features", None),
+ "AccessibilityPerformance": pset_metadata.get("accessibility_performance", None),
+ "CodePerformance": pset_metadata.get("code_performance", None),
+ "SustainabilityPerformance": pset_metadata.get("sustainability_performance", None),
+ }
+
+
+def get_component_data(ifc_file, element):
+ space = ifcopenshell.util.element.get_container(element)
+ space_name = space.Name if space.is_a("IfcSpace") else None
+ systems = ifcopenshell.util.system.get_element_systems(element)
+ system = systems[0].Name if systems else None
+
+ type_name = None
+ relating_type = ifcopenshell.util.element.get_type(element)
+ if relating_type and val(relating_type.Name):
+ type_name = relating_type.Name
+ else:
+ material = ifcopenshell.util.element.get_material(element, should_skip_usage=True)
+ type_name = getattr(material, "Name", None) or getattr(material, "LayerSetName", None)
+
+ serial_number = None
+ installation_date = None
+ warranty_start_date = None
+ tag_number = None
+ bar_code = None
+ asset_identifier = None
+
+ for _, props in ifcopenshell.util.element.get_psets(element).items():
+ for name, value in props.items():
+ if not serial_number and name == "SerialNumber" and val(value):
+ serial_number = str(value)
+ if not installation_date and name == "InstallationDate" and val(value):
+ installation_date = str(value)
+ if not warranty_start_date and name == "WarrantyStartDate" and val(value):
+ warranty_start_date = str(value)
+ if not tag_number and name == "TagNumber" and val(value):
+ tag_number = str(value)
+ if not bar_code and name == "BarCode" and val(value):
+ bar_code = str(value)
+ if not asset_identifier and name == "AssetIdentifier" and val(value):
+ asset_identifier = str(value)
+
+ return {
+ "key": element.Name,
+ "Name": element.Name,
+ "CreatedBy": get_created_by(element),
+ "CreatedOn": get_created_on(element),
+ "TypeName": type_name,
+ "Space": space_name,
+ "Description": val(element.Description) or val(element.Name),
+ "ExternalSystem": get_external_system(element),
+ "ExternalObject": element.is_a(),
+ "ExternalIdentifier": element.GlobalId,
+ "SerialNumber": serial_number,
+ "InstallationDate": installation_date,
+ "WarrantyStartDate": warranty_start_date,
+ "TagNumber": tag_number,
+ "BarCode": bar_code,
+ "AssetIdentifier": asset_identifier,
+ }
+
+
+def get_system_data(ifc_file, element):
+ system, component = element
+ category = get_category(system)
+ component_name = val(component.Name)
+ return {
+ "key": str(val(system.Name)) + str(category) + str(component_name),
+ "Name": val(system.Name),
+ "CreatedBy": get_created_by(system),
+ "CreatedOn": get_created_on(system),
+ "Category": get_category(system),
+ "ComponentNames": component_name,
+ "ExternalSystem": get_external_system(system),
+ "ExternalObject": system.is_a(),
+ "ExternalIdentifier": system.GlobalId,
+ "Description": val(system.Description) or val(system.Name),
+ }
+
+
+def get_assembly_data(ifc_file, element):
+ rel, relating_object, related_object = element
+
+ if relating_object.is_a("IfcMaterialLayerSet"):
+ name = val(relating_object.LayerSetName)
+ parent_name = name
+ if name:
+ name += " assembly"
+ assembly_type = "Layer"
+ sheet_name = "Type"
+ description = val(relating_object.LayerSetName)
+ else:
+ name = val(relating_object.Name)
+ parent_name = name
+ assembly_type = "Fixed"
+ sheet_name = get_sheet_name(relating_object)
+ description = val(rel.Description) or val(rel.Name)
+
+ child_name = val(related_object.Name)
+ history = get_history(ifc_file)
+
+ return {
+ "key": str(name) + str(sheet_name) + str(parent_name),
+ "Name": name,
+ "CreatedBy": get_email_from_history(history) if history else None,
+ "CreatedOn": ifcopenshell.util.date.ifc2datetime(history.CreationDate).isoformat() if history else None,
+ "SheetName": sheet_name,
+ "ParentName": parent_name,
+ "ChildNames": child_name,
+ "AssemblyType": assembly_type,
+ "ExternalSystem": history.OwningApplication.ApplicationFullName if history else None,
+ "ExternalObject": rel.is_a() if rel else relating_object.is_a(),
+ "ExternalIdentifier": rel.GlobalId if rel else None,
+ "Description": description,
+ }
+
+
+def get_connection_data(ifc_file, element):
+ connection_type = (
+ val(element.RelatingPort.ObjectType)
+ or val(element.RelatedPort.ObjectType)
+ or val(element.Description)
+ or val(element.Name)
+ )
+ name = val(element.Name)
+ row_name1 = val(ifcopenshell.util.system.get_port_element(element.RelatingPort).Name)
+ row_name2 = val(ifcopenshell.util.system.get_port_element(element.RelatedPort).Name)
+ return {
+ "key": str(name) + str(connection_type) + str(row_name1) + str(row_name2),
+ "Name": name,
+ "CreatedBy": get_created_by(element),
+ "CreatedOn": get_created_on(element),
+ "ConnectionType": connection_type,
+ "SheetName": "Component",
+ "RowName1": row_name1,
+ "RowName2": row_name2,
+ "RealizingElement": val(element.RealizingElement.Name) if element.RealizingElement else None,
+ "PortName1": val(element.RelatingPort.Name),
+ "PortName2": val(element.RelatedPort.Name),
+ "ExternalSystem": get_external_system(element),
+ "ExternalObject": element.is_a(),
+ "ExternalIdentifier": element.GlobalId,
+ "Description": val(element.Description) or val(element.Name),
+ }
+
+
+def get_spare_data(ifc_file, element):
+ type_name = None
+ for rel in element.ResourceOf or []:
+ for related_object in rel.RelatedObjects or []:
+ if val(related_object.Name):
+ type_name = val(related_object.Name)
+
+ suppliers = None
+ set_number = None
+ part_number = None
+ for _, props in ifcopenshell.util.element.get_psets(element).items():
+ for name, value in props.items():
+ if name == "Suppliers" and val(value):
+ suppliers = str(value)
+ if name == "SetNumber" and val(value):
+ set_number = str(value)
+ if name == "PartNumber" and val(value):
+ part_number = str(value)
+
+ return {
+ "key": val(element),
+ "Name": val(element.Name),
+ "CreatedBy": get_created_by(element),
+ "CreatedOn": get_created_on(element),
+ "Category": get_category(element),
+ "TypeName": type_name,
+ "Suppliers": suppliers,
+ "ExternalSystem": get_external_system(element),
+ "ExternalObject": element.is_a(),
+ "ExternalIdentifier": element.GlobalId,
+ "Description": val(element.Description) or val(element.Name),
+ "SetNumber": set_number,
+ "PartNumber": part_number,
+ }
+
+
+def get_resource_data(ifc_file, element):
+ return {
+ "key": val(element.Name),
+ "Name": val(element.Name),
+ "CreatedBy": get_created_by(element),
+ "CreatedOn": get_created_on(element),
+ "Category": val(element.ObjectType),
+ "ExternalSystem": get_external_system(element),
+ "ExternalObject": element.is_a(),
+ "ExternalIdentifier": element.GlobalId,
+ "Description": val(element.Description) or val(element.Name),
+ }
+
+
+def get_job_data(ifc_file, element):
+ type_names = []
+ resource_names = []
+ for rel in element.OperatesOn or []:
+ for related_object in rel.RelatedObjects or []:
+ if not val(related_object.Name):
+ continue
+ if related_object.is_a("IfcTypeObject"):
+ type_names.append(related_object.Name)
+ elif related_object.is_a("IfcConstructionEquipmentResource"):
+ resource_names.append(related_object.Name)
+ type_name = ",".join(type_names) if type_names else None
+ resource_names = ",".join(resource_names) if resource_names else None
+
+ duration = None
+ duration_unit = None
+ start = None
+ task_start_unit = None
+ frequency = None
+ frequency_unit = None
+
+ for _, props in ifcopenshell.util.element.get_psets(element).items():
+ pset = ifc_file.by_id(props["id"])
+ for name, value in props.items():
+ if not duration and name == "TaskDuration" and val(value):
+ duration = str(value)
+ if not duration_unit:
+ duration_unit = get_property_unit(pset, name)
+ if not start and name == "TaskStartDate" and val(value):
+ start = str(value)
+ if not task_start_unit:
+ task_start_unit = get_property_unit(pset, name)
+ if not frequency and name == "TaskInterval" and val(value):
+ frequency = str(value)
+ if not frequency_unit:
+ frequency_unit = get_property_unit(pset, name)
+
+ task_number = val(getattr(element, "Id", None)) or val(getattr(element, "Identification", None))
+
+ priors = []
+ for rel in element.IsSuccessorFrom or []:
+ if rel.RelatedProcess.is_a("IfcTask"):
+ prior_task = rel.RelatedProcess
+ prior_id = val(getattr(prior_task, "Id", None)) or val(getattr(prior_task, "Identification", None))
+ if prior_id:
+ priors.append(prior_id)
+ priors = ",".join(priors) if priors else task_number
+
+ return {
+ "key": str(val(element.Name)) + str(type_name) + str(task_number),
+ "Name": val(element.Name),
+ "CreatedBy": get_created_by(element),
+ "CreatedOn": get_created_on(element),
+ "Category": val(element.ObjectType),
+ "Status": val(element.Status),
+ "TypeName": type_name,
+ "Description": val(element.Description) or val(element.Name),
+ "Duration": duration,
+ "DurationUnit": duration_unit,
+ "Start": start,
+ "TaskStartUnit": task_start_unit,
+ "Frequency": frequency,
+ "FrequencyUnit": frequency_unit,
+ "ExternalSystem": get_external_system(element),
+ "ExternalObject": element.is_a(),
+ "ExternalIdentifier": element.GlobalId,
+ "TaskNumber": task_number,
+ "Priors": priors,
+ "ResourceNames": resource_names,
+ }
+
+
+def get_document_data(ifc_file, element):
+ rel, doc, related_object = element
+ directory = getattr(doc, "Location", None)
+ file = None
+ if not directory:
+ references = getattr(doc, "DocumentReferences", []) or getattr(doc, "HasDocumentReferences", [])
+ for reference in references or []:
+ if val(reference.Location):
+ directory = reference.Location
+ identification = getattr(reference, "ItemReference", None) or getattr(reference, "Identification", None)
+ if val(reference.Name) and doc.Name != reference.Name:
+ file = reference.Name
+ elif val(identification):
+ file = identification
+ name = val(doc.Name)
+ stage = val(doc.Scope) or "Requirement"
+ sheet_name = get_sheet_name(related_object)
+ row_name = val(related_object.Name)
+ return {
+ "key": str(name) + str(stage) + str(sheet_name) + str(row_name),
+ "Name": name,
+ "CreatedBy": get_created_by(rel),
+ "CreatedOn": get_created_on(rel),
+ "Category": val(doc.Purpose),
+ "ApprovalBy": val(doc.IntendedUse) or "Information Only",
+ "Stage": stage,
+ "SheetName": sheet_name,
+ "RowName": row_name,
+ "Directory": directory,
+ "File": file,
+ "ExternalSystem": get_external_system(rel),
+ "ExternalObject": rel.is_a(),
+ "ExternalIdentifier": rel.GlobalId,
+ "Description": val(doc.Description),
+ "Reference": val(doc.Description) or val(doc.Name),
+ }
+
+
+def get_attribute_data(ifc_file, element):
+ return element
+
+
+def get_unit_type_name(ifc_file, unit_type):
+ for unit in ifc_file.by_type("IfcUnitAssignment")[0].Units:
+ if unit.is_a("IfcNamedUnit") and unit.UnitType == unit_type:
+ if unit.is_a("IfcSIUnit"):
+ prefix = (unit.Prefix or "").lower()
+ if unit_type == "LENGTHUNIT":
+ return f"{prefix}meters"
+ elif unit_type == "AREAUNIT":
+ return f"square {prefix}meters"
+ elif unit_type == "VOLUMEUNIT":
+ return f"cubic {prefix}meters"
+ else:
+ return val(unit.Name)
+ elif unit.is_a("IfcMonetaryUnit") and unit_type == "IfcMonetaryUnit":
+ return val(unit.Currency)
+
+
+def get_unit_name(ifc_file, unit):
+ if unit.is_a("IfcNamedUnit"):
+ return val(unit.Name)
+
+
+def get_created_by(element):
+ if getattr(element, "OwnerHistory", None):
+ return get_email_from_history(element.OwnerHistory)
+
+
+def get_email_from_history(element):
+ pao = element.OwningUser
+ if pao.is_a("IfcPersonAndOrganization"):
+ return get_email_from_pao(pao)
+
+
+def get_email_from_pao(pao):
+ for address in pao.ThePerson.Addresses or []:
+ if address.is_a("IfcTelecomAddress") and address.ElectronicMailAddresses:
+ return address.ElectronicMailAddresses[0]
+
+ for address in pao.TheOrganization.Addresses or []:
+ if address.is_a("IfcTelecomAddress") and address.ElectronicMailAddresses:
+ return address.ElectronicMailAddresses[0]
+
+ person_id = getattr(pao.ThePerson, "Identification", getattr(pao.ThePerson, "Id", None))
+ if person_id:
+ return person_id
+
+ organization_id = getattr(pao.TheOrganization, "Identification", getattr(pao.TheOrganization, "Id", None))
+ if organization_id:
+ return organization_id
+
+ if pao.ThePerson.GivenName and pao.ThePerson.FamilyName and pao.TheOrganization.Name:
+ return pao.ThePerson.GivenName + pao.ThePerson.FamilyName + "@" + pao.TheOrganization.Name + ".com"
+
+
+def get_owner_name(element):
+ if getattr(element, "OwnerHistory", None):
+ return element.OwnerHistory.OwningUser.TheOrganization.Name
+
+
+def get_created_on(element):
+ if getattr(element, "OwnerHistory", None):
+ return ifcopenshell.util.date.ifc2datetime(element.OwnerHistory.CreationDate).isoformat()
+
+
+def get_external_system(element):
+ if getattr(element, "OwnerHistory", None):
+ return val(element.OwnerHistory.OwningApplication.ApplicationFullName)
+
+
+def get_facility_parent(element, ifc_class):
+ parent = ifcopenshell.util.element.get_aggregate(element)
+ while parent:
+ if parent.is_a(ifc_class):
+ return parent
+ if parent.is_a("IfcProject"):
+ return
+ parent = ifcopenshell.util.element.get_aggregate(parent)
+
+
+def val(x):
+ return x if x not in ("", "n/a") else None
+
+
+def get_area_measurement(element):
+ for relationship in getattr(element, "IsDefinedBy", []) or []:
+ if relationship.is_a("IfcRelDefinesByProperties"):
+ definition = relationship.RelatingPropertyDefinition
+ if definition.is_a("IfcElementQuantity") and val(definition.MethodOfMeasurement):
+ return definition.MethodOfMeasurement
+ for rel in getattr(element, "IsDecomposedBy", []):
+ for related_object in rel.RelatedObjects:
+ result = get_area_measurement(related_object)
+ if result:
+ return result
+
+
+def get_category(element):
+ references = list(ifcopenshell.util.classification.get_references(element))
+ results = []
+ for reference in references:
+ if reference.is_a("IfcClassification"):
+ results.append(reference.Name)
+ elif reference.is_a("IfcClassificationReference"):
+ identification = val(getattr(reference, "Identification", getattr(reference, "ItemReference", None)))
+ if val(reference.Name) and identification and val(reference.Name) != identification:
+ results.append(identification + " : " + val(reference.Name))
+ elif val(reference.Name):
+ results.append(reference.Name)
+ elif identification:
+ results.append(identification)
+ elif reference.ReferencedSource and val(reference.ReferencedSource.Name):
+ results.append(reference.ReferencedSource.Name)
+ elif val(reference.Location):
+ results.append(reference.Location)
+ if results:
+ return ",".join(results)
+
+ category_props = [
+ ("Assembly Code", "Assembly Description"),
+ ("Category Code", "Category Description"),
+ ("Classification Code", "Classification Description"),
+ ("OmniClass Number", "OmniClass Title"),
+ ("Uniclass Code", "Uniclass Description"),
+ ]
+
+ psets = ifcopenshell.util.element.get_psets(element)
+ properties = {}
+ if psets:
+ for _, props in psets.items():
+ properties.update(props)
+
+ for code, description in category_props:
+ code = val(properties.get(code, None))
+ if code:
+ description = val(properties.get(description, None))
+ if code and description:
+ results.append(code + " : " + description)
+ else:
+ results.append(code)
+ if results:
+ return ",".join(results)
+
+ return val(getattr(element, "ObjectType", None))
+
+
+def get_pao_address(element, name):
+ for actor in [element.ThePerson, element.TheOrganization]:
+ for address in actor.Addresses or []:
+ if hasattr(address, name) and getattr(address, name, None):
+ result = getattr(address, name)
+ if isinstance(result, tuple):
+ if name == "AddressLines":
+ return " ".join(result)
+ return result[0]
+ return result
+
+
+def get_property(psets, pset_name, prop_name, decimals=None):
+ if pset_name in psets:
+ result = psets[pset_name].get(prop_name, None)
+ if decimals is None or result is None:
+ return result
+ return round(result, decimals)
+
+
+def get_history(ifc_file):
+ histories = ifc_file.by_type("IfcOwnerHistory")
+ if histories:
+ return sorted(histories, key=lambda x: x.id())[-1]
+
+
+def get_property_unit(pset, prop_name):
+ for prop in getattr(pset, "HasProperties", []) or []:
+ if prop.Name == prop_name:
+ unit = getattr(prop, "Unit", None)
+ if unit:
+ return get_unit_name(unit)
+
+
+def get_allowed_values(pset_id, prop_name):
+ pset = ifc_file.by_id(pset_id)
+ for prop in getattr(pset, "HasProperties", []) or []:
+ if prop.Name == prop_name:
+ if prop.is_a("IfcPropertyEnumeratedValue") and prop.EnumerationValues:
+ return ",".join([v.wrappedValue for v in prop.EnumerationValues])
+
+
+def get_sheet_name(element):
+ if element.is_a("IfcBuilding"):
+ return "Facility"
+ elif element.is_a("IfcBuildingStorey"):
+ return "Floor"
+ elif element.is_a("IfcSpace"):
+ return "Space"
+ elif element.is_a("IfcZone"):
+ return "Zone"
+ elif element.is_a("IfcSystem"):
+ return "System"
+ elif element.is_a("IfcElementType"):
+ return "Type"
+ elif element.is_a("IfcElement"):
+ return "Component"
+ elif element.is_a("IfcTask"):
+ return "Job"
+ elif element.is_a("IfcConstructionProductResource"):
+ return "Spare"
+ elif element.is_a("IfcConstructionEquipmentResource"):
+ return "Resource"
+
+
+config = {
+ "colours": {
+ "h": "c0c0c0", # Header data
+ "r": "ffff99", # Required
+ "i": "ffcc99", # Internal reference
+ "e": "cc99ff", # External reference
+ "o": "ccffcc", # Optionally specified
+ "s": "c0c0c0", # Secondary product data
+ "b": "99ccff", # Bespoke data
+ "x": "000000", # Not in scope
+ },
+ "null": "n/a",
+ "empty": "n/a",
+ "bool_true": "Yes",
+ "bool_false": "No",
+ "categories": {
+ "Contact": {
+ "headers": [
+ "Email",
+ "CreatedBy",
+ "CreatedOn",
+ "Category",
+ "Company",
+ "Phone",
+ "ExternalSystem",
+ "ExternalObject",
+ "ExternalIdentifier",
+ "Department",
+ "OrganizationCode",
+ "GivenName",
+ "FamilyName",
+ "Street",
+ "PostalBox",
+ "Town",
+ "StateRegion",
+ "PostalCode",
+ "Country",
+ ],
+ "colours": "rrrrrreeeoooooooooo",
+ "sort": [{"name": "Email", "order": "ASC"}],
+ "get_category_elements": get_contacts,
+ "get_element_data": get_contact_data,
+ },
+ "Facility": {
+ "headers": [
+ "Name",
+ "CreatedBy",
+ "CreatedOn",
+ "Category",
+ "ProjectName",
+ "SiteName",
+ "LinearUnits",
+ "AreaUnits",
+ "VolumeUnits",
+ "CurrencyUnit",
+ "AreaMeasurement",
+ "ExternalSystem",
+ "ExternalProjectObject",
+ "ExternalProjectIdentifier",
+ "ExternalSiteObject",
+ "ExternalSiteIdentifier",
+ "ExternalFacilityObject",
+ "ExternalFacilityIdentifier",
+ "Description",
+ "ProjectDescription",
+ "SiteDescription",
+ "Phase",
+ ],
+ "colours": "ririrriiiireeeeeeeoooo",
+ "sort": [{"name": "Name", "order": "ASC"}],
+ "get_category_elements": get_facilities,
+ "get_element_data": get_facility_data,
+ },
+ "Floor": {
+ "headers": [
+ "Name",
+ "CreatedBy",
+ "CreatedOn",
+ "Category",
+ "ExternalSystem",
+ "ExternalObject",
+ "ExternalIdentifier",
+ "Description",
+ "Elevation",
+ "Height",
+ ],
+ "colours": "ririeeeooo",
+ "sort": [{"name": "Elevation", "order": "ASC"}, {"name": "Name", "order": "ASC"}],
+ "get_category_elements": get_floors,
+ "get_element_data": get_floor_data,
+ },
+ "Space": {
+ "headers": [
+ "Name",
+ "CreatedBy",
+ "CreatedOn",
+ "Category",
+ "FloorName",
+ "Description",
+ "ExternalSystem",
+ "ExternalObject",
+ "ExternalIdentifier",
+ "RoomTag",
+ "UsableHeight",
+ "GrossArea",
+ "NetArea",
+ ],
+ "colours": "ririrreeeoooo",
+ "sort": [{"name": "FloorName", "order": "ASC"}, {"name": "Name", "order": "ASC"}],
+ "get_category_elements": get_spaces,
+ "get_element_data": get_space_data,
+ },
+ "Zone": {
+ "headers": [
+ "Name",
+ "CreatedBy",
+ "CreatedOn",
+ "Category",
+ "SpaceNames",
+ "ExternalSystem",
+ "ExternalObject",
+ "ExternalIdentifier",
+ "Description",
+ ],
+ "colours": "ririreeeo",
+ "sort": [{"name": "Name", "order": "ASC"}],
+ "get_category_elements": get_zones,
+ "get_element_data": get_zone_data,
+ },
+ "Type": {
+ "headers": [
+ "Name",
+ "CreatedBy",
+ "CreatedOn",
+ "Category",
+ "Description",
+ "AssetType",
+ "Manufacturer",
+ "ModelNumber",
+ "WarrantyGuarantorParts",
+ "WarrantyDurationParts",
+ "WarrantyGuarantorLabor",
+ "WarrantyDurationLabor",
+ "WarrantyDurationUnit",
+ "ExternalSystem",
+ "ExternalObject",
+ "ExternalIdentifier",
+ "ReplacementCost",
+ "ExpectedLife",
+ "DurationUnit",
+ "WarrantyDescription",
+ "NominalLength",
+ "NominalWidth",
+ "NominalHeight",
+ "ModelReference",
+ "Shape",
+ "Size",
+ "Color",
+ "Finish",
+ "Grade",
+ "Material",
+ "Constituents",
+ "Features",
+ "AccessibilityPerformance",
+ "CodePerformance",
+ "SustainabilityPerformance",
+ ],
+ "colours": "riririiriririeeeooiorrroooooooooooo",
+ "sort": [{"name": "ExternalObject", "order": "ASC"}, {"name": "Name", "order": "ASC"}],
+ "get_category_elements": get_types,
+ "get_element_data": get_type_data,
+ },
+ "Component": {
+ "headers": [
+ "Name",
+ "CreatedBy",
+ "CreatedOn",
+ "TypeName",
+ "Space",
+ "Description",
+ "ExternalSystem",
+ "ExternalObject",
+ "ExternalIdentifier",
+ "SerialNumber",
+ "InstallationDate",
+ "WarrantyStartDate",
+ "TagNumber",
+ "BarCode",
+ "AssetIdentifier",
+ ],
+ "colours": "ririireeeoooooo",
+ "sort": [
+ {"name": "ExternalObject", "order": "ASC"},
+ {"name": "TypeName", "order": "ASC"},
+ {"name": "Name", "order": "ASC"},
+ ],
+ "get_category_elements": get_components,
+ "get_element_data": get_component_data,
+ },
+ "System": {
+ "headers": [
+ "Name",
+ "CreatedBy",
+ "CreatedOn",
+ "Category",
+ "ComponentNames",
+ "ExternalSystem",
+ "ExternalObject",
+ "ExternalIdentifier",
+ "Description",
+ ],
+ "colours": "ririieeeo",
+ "sort": [{"name": "Name", "order": "ASC"}],
+ "get_category_elements": get_systems,
+ "get_element_data": get_system_data,
+ },
+ "Assembly": { # Note that this is technically "not required"
+ "headers": [
+ "Name",
+ "CreatedBy",
+ "CreatedOn",
+ "SheetName",
+ "ParentName",
+ "ChildNames",
+ "AssemblyType",
+ "ExternalSystem",
+ "ExternalObject",
+ "ExternalIdentifier",
+ "Description",
+ ],
+ "colours": "ririiiieeeo",
+ "sort": [{"name": "Name", "order": "ASC"}],
+ "get_category_elements": get_assemblies,
+ "get_element_data": get_assembly_data,
+ },
+ "Connection": { # Note that this is technically "not required"
+ "headers": [
+ "Name",
+ "CreatedBy",
+ "CreatedOn",
+ "ConnectionType",
+ "SheetName",
+ "RowName1",
+ "RowName2",
+ "RealizingElement",
+ "PortName1",
+ "PortName2",
+ "ExternalSystem",
+ "ExternalObject",
+ "ExternalIdentifier",
+ "Description",
+ ],
+ "colours": "ririiiiiiieeeo",
+ "sort": [{"name": "Name", "order": "ASC"}],
+ "get_category_elements": get_connections,
+ "get_element_data": get_connection_data,
+ },
+ "Spare": {
+ "headers": [
+ "Name",
+ "CreatedBy",
+ "CreatedOn",
+ "Category",
+ "TypeName",
+ "Suppliers",
+ "ExternalSystem",
+ "ExternalObject",
+ "ExternalIdentifier",
+ "Description",
+ "SetNumber",
+ "PartNumber",
+ ],
+ "colours": "ririiieeeooo",
+ "sort": [{"name": "Name", "order": "ASC"}],
+ "get_category_elements": get_spares,
+ "get_element_data": get_spare_data,
+ },
+ "Resource": {
+ "headers": [
+ "Name",
+ "CreatedBy",
+ "CreatedOn",
+ "Category",
+ "ExternalSystem",
+ "ExternalObject",
+ "ExternalIdentifier",
+ "Description",
+ ],
+ "colours": "ririeeeo",
+ "sort": [{"name": "Name", "order": "ASC"}],
+ "get_category_elements": get_resources,
+ "get_element_data": get_resource_data,
+ },
+ "Job": {
+ "headers": [
+ "Name",
+ "CreatedBy",
+ "CreatedOn",
+ "Category",
+ "Status",
+ "TypeName",
+ "Description",
+ "Duration",
+ "DurationUnit",
+ "Start",
+ "TaskStartUnit",
+ "Frequency",
+ "FrequencyUnit",
+ "ExternalSystem",
+ "ExternalObject",
+ "ExternalIdentifier",
+ "TaskNumber",
+ "Priors",
+ "ResourceNames",
+ ],
+ "colours": "ririiirriririeeeoii",
+ "sort": [{"name": "TypeName", "order": "ASC"}, {"name": "TaskNumber", "order": "ASC"}],
+ "get_category_elements": get_jobs,
+ "get_element_data": get_job_data,
+ },
+ "Document": {
+ "headers": [
+ "Name",
+ "CreatedBy",
+ "CreatedOn",
+ "Category",
+ "ApprovalBy",
+ "Stage",
+ "SheetName",
+ "RowName",
+ "Directory",
+ "File",
+ "ExternalSystem",
+ "ExternalObject",
+ "ExternalIdentifier",
+ "Description",
+ "Reference",
+ ],
+ "colours": "ririiiiirreeeoo",
+ "sort": [{"name": "Name", "order": "ASC"}],
+ "get_category_elements": get_documents,
+ "get_element_data": get_document_data,
+ },
+ "Attribute": {
+ "headers": [
+ "Name",
+ "CreatedBy",
+ "CreatedOn",
+ "Category",
+ "SheetName",
+ "RowName",
+ "Value",
+ "Unit",
+ "ExternalSystem",
+ "ExternalObject",
+ "ExternalIdentifier",
+ "Description",
+ "AllowedValues",
+ ],
+ "colours": "ririiirreeeoo",
+ "sort": [{"name": "Category", "order": "ASC"}, {"name": "Name", "order": "ASC"}],
+ "get_category_elements": get_attributes,
+ "get_element_data": get_attribute_data,
+ },
+ },
+}
diff --git a/src/ifcfm/ifcfm/parser.py b/src/ifcfm/ifcfm/parser.py
deleted file mode 100644
index f74ce4ad78..0000000000
--- a/src/ifcfm/ifcfm/parser.py
+++ /dev/null
@@ -1,658 +0,0 @@
-# IfcFM - IFC for facility management
-# Copyright (C) 2021 Dion Moult
-#
-# This file is part of IfcFM.
-#
-# IfcFM is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# IfcFM 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 Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with IfcFM. If not, see .
-
-import datetime
-import ifcopenshell
-import ifcopenshell.util.fm
-import ifcopenshell.util.selector
-import ifcopenshell.util.date
-import ifcopenshell.util.schema
-import ifcopenshell.util.system
-import ifcopenshell.util.placement
-import ifcopenshell.util.classification
-
-
-class Parser2:
- def __init__(self, preset="BASIC"):
- self.file = None
- self.categories = {}
- self.get_category_elements = {}
- self.get_element_data = {}
- self.get_custom_element_data = {}
- self.duplicate_keys = []
-
- if preset == "BASIC":
- self.get_category_elements = {
- "actors": get_actors,
- "facilities": get_facilities,
- "storeys": get_storeys,
- "spaces": get_spaces,
- "zones": get_zones,
- "types": get_types,
- "elements": get_elements,
- "systems": get_systems,
- }
- self.get_element_data = {
- "actors": get_actor_data,
- "facilities": get_facility_data,
- "storeys": get_storey_data,
- "spaces": get_space_data,
- "zones": get_zone_data,
- "types": get_type_data,
- "elements": get_element_data,
- "systems": get_system_data,
- }
-
- def parse(self, ifc_file):
- for category_name, get_category_elements in self.get_category_elements.items():
- self.categories.setdefault(category_name, {})
- for element in get_category_elements(ifc_file):
- data = self.get_element_data[category_name](ifc_file, element) or {}
- custom_data = (
- self.get_custom_element_data.get(category_name, lambda x, y: None)(ifc_file, element) or {}
- )
- data.update(custom_data)
-
- if data:
- if data["key"] in self.categories[category_name]:
- self.duplicate_keys.append((self.categories[category_name][data["key"]], data))
- self.categories[category_name][data["key"]] = data
-
-
-def get_actors(ifc_file):
- return ifc_file.by_type("IfcActor")
-
-
-def get_facilities(ifc_file):
- return ifc_file.by_type("IfcBuilding")
-
-
-def get_storeys(ifc_file):
- return ifc_file.by_type("IfcBuildingStorey")
-
-
-def get_spaces(ifc_file):
- return ifc_file.by_type("IfcSpace")
-
-
-def get_zones(ifc_file):
- zones = []
- for zone in ifc_file.by_type("IfcZone"):
- for rel in zone.IsGroupedBy:
- zones.extend([(zone, space) for space in rel.RelatedObjects])
- return zones
-
-
-def get_types(ifc_file):
- return ifcopenshell.util.fm.get_fmhem_types(ifc_file)
-
-
-def get_elements(ifc_file):
- elements = set()
- for element_type in ifcopenshell.util.fm.get_fmhem_types(ifc_file):
- elements.update(ifcopenshell.util.element.get_types(element_type))
- return elements
-
-
-def get_systems(ifc_file):
- return ifc_file.by_type("IfcSystem")
-
-
-def get_actor_data(ifc_file, element):
- return {
- "key": element.TheActor.Name,
- "Name": element.TheActor.Name,
- "Category": get_classification(element),
- "Email": get_actor_address(element, "ElectronicMailAddresses"),
- "Phone": get_actor_address(element, "TelephoneNumbers"),
- "CompanyURL": get_actor_address(element, "WWWHomePageURL"),
- "Department": get_actor_address(element, "InternalLocation"),
- "Address1": get_actor_address(element, "AddressLines"),
- "Address2": get_actor_address(element, "Town"),
- "StateRegion": get_actor_address(element, "Region"),
- "PostalCode": get_actor_address(element, "PostalCode"),
- "Country": get_actor_address(element, "Country"),
- }
-
-
-def get_facility_data(ifc_file, element):
- return {
- "key": element.Name,
- "Name": element.Name,
- "AuthorOrganizationName": get_owner_name(element),
- "AuthorDate": get_owner_creation_date(ifc_file.by_type("IfcProject")[0]),
- "Category": get_classification(element),
- "ProjectName": ifc_file.by_type("IfcProject")[0].Name,
- "SiteName": getattr(get_facility_parent(element, "IfcSite"), "Name", None),
- "LinearUnits": "millimeters",
- "AreaUnits": "square meters",
- "AreaMeasurement": "BIM Software",
- "Phase": ifc_file.by_type("IfcProject")[0].Phase,
- "ModelSoftware": get_owner_application(element),
- "ModelProjectID": ifc_file.by_type("IfcProject")[0].GlobalId,
- "ModelSiteID": getattr(get_facility_parent(element, "IfcSite"), "GlobalId", None),
- "ModelBuildingID": element.GlobalId,
- }
-
-
-def get_storey_data(ifc_file, element):
- return {
- "key": element.Name,
- "Name": element.Name,
- "AuthorOrganizationName": get_owner_name(element),
- "AuthorDate": get_owner_creation_date(element),
- "Category": "Level",
- "ModelSoftware": get_owner_application(element),
- "ModelObject": element.is_a(),
- "ModelID": element.GlobalId,
- "Elevation": ifcopenshell.util.placement.get_storey_elevation(element),
- }
-
-
-def get_space_data(ifc_file, element):
- psets = ifcopenshell.util.element.get_psets(element)
- return {
- "key": element.Name,
- "Name": element.Name,
- "AuthorOrganizationName": get_owner_name(element),
- "AuthorDate": get_owner_creation_date(element),
- "Category": get_classification(element),
- "LevelName": getattr(get_facility_parent(element, "IfcBuildingStorey"), "Name", None),
- "Description": element.LongName,
- "ModelSoftware": get_owner_application(element),
- "ModelID": element.GlobalId,
- "AreaGross": get_property(psets, "Qto_SpaceBaseQuantities", "GrossFloorArea", decimals=2),
- "AreaNet": get_property(psets, "Qto_SpaceBaseQuantities", "NetFloorArea", decimals=2),
- }
-
-
-def get_zone_data(ifc_file, element):
- zone, space = element
- return {
- "key": (element.Name or "Unnamed") + (space.Name or "Unnamed"),
- "Name": zone.Name,
- "AuthorOrganizationName": get_owner_name(zone),
- "AuthorDate": get_owner_creation_date(zone),
- "SpaceName": space.Name,
- "ModelSoftware": get_owner_application(zone),
- "ModelID": zone.GlobalId,
- }
-
-
-def get_type_data(ifc_file, element):
- return {
- "key": element.Name,
- "Name": element.Name,
- "AuthorOrganizationName": get_owner_name(element),
- "AuthorDate": get_owner_creation_date(element),
- "Category": get_classification(element),
- "Description": element.Description,
- "ModelSoftware": get_owner_application(element),
- "ModelObject": "{}[{}]".format(element.is_a(), ifcopenshell.util.element.get_predefined_type(element)),
- "ModelTag": element.Tag,
- "ModelID": element.GlobalId,
- }
-
-
-def get_element_data(ifc_file, element):
- space = ifcopenshell.util.element.get_container(element)
- space_name = space.Name if space.is_a("IfcSpace") else None
- systems = ifcopenshell.util.system.get_element_systems(element)
- system = systems[0].Name if systems else None
- return {
- "key": element.Name,
- "Name": element.Name,
- "AuthorOrganizationName": get_owner_name(element),
- "AuthorDate": get_owner_creation_date(element),
- "TypeName": ifcopenshell.util.element.get_type(element).Name,
- "SpaceName": space_name,
- "SystemName": system,
- "ModelSoftware": get_owner_application(element),
- "ModelObject": "{}[{}]".format(element.is_a(), ifcopenshell.util.element.get_predefined_type(element)),
- "ModelID": element.GlobalId,
- }
-
-
-def get_system_data(ifc_file, element):
- return {
- "key": element.Name,
- "Name": element.Name,
- "Description": element.Description,
- "AuthorOrganizationName": get_owner_name(element),
- "AuthorDate": get_owner_creation_date(element),
- "Category": get_classification(element),
- "ModelSoftware": get_owner_application(element),
- "ModelID": element.GlobalId,
- }
-
-
-def get_owner_name(element):
- if not getattr(element, "OwnerHistory", None):
- return
- return element.OwnerHistory.OwningUser.TheOrganization.Name
-
-
-def get_owner_creation_date(element):
- if not getattr(element, "OwnerHistory", None):
- return
- return ifcopenshell.util.date.ifc2datetime(element.OwnerHistory.CreationDate).isoformat()
-
-
-def get_owner_application(element):
- if not getattr(element, "OwnerHistory", None):
- return
- return element.OwnerHistory.OwningApplication.ApplicationFullName
-
-
-def get_facility_parent(element, ifc_class):
- parent = ifcopenshell.util.element.get_aggregate(element)
- while parent:
- if parent.is_a(ifc_class):
- return parent
- if parent.is_a("IfcProject"):
- return
- parent = ifcopenshell.util.element.get_aggregate(parent)
-
-
-def get_classification(element):
- references = list(ifcopenshell.util.classification.get_references(element))
- if references:
- if hasattr(references[0], "Identification"):
- return "{}:{}".format(references[0].Identification, references[0].Name)
- return "{}:{}".format(references[0].ItemReference, references[0].Name)
-
-
-def get_actor_address(element, name):
- for address in element.TheActor.Addresses or []:
- if hasattr(address, name) and getattr(address, name, None):
- result = getattr(address, name)
- if isinstance(result, tuple):
- return result[0]
- return result
-
-
-def get_property(psets, pset_name, prop_name, decimals=None):
- if pset_name in psets:
- result = psets[pset_name].get(prop_name, None)
- if decimals is None or result is None:
- return result
- return round(result, decimals)
-
-
-class Parser:
- def __init__(self, logger):
- self.logger = logger
- self.file = None
- self.categories = {
- "actors": self.get_actors,
- "facilities": self.get_facilities,
- "floors": self.get_floors,
- "spaces": self.get_spaces,
- "zones": self.get_zones,
- "types": self.get_types,
- "components": self.get_components,
- "systems": self.get_systems,
- # "assemblies",
- # "connections",
- # "spares",
- # "resources",
- # "jobs",
- # "impacts",
- "documents": self.get_documents,
- # "attributes",
- # "coordinates",
- # "issues",
- }
- # COBie
- self.custom_parameters = {
- "types": {
- "AssetType": lambda e, p: None,
- "ManufacturerOrganizationName": lambda e, p: None,
- "ModelNumber": lambda e, p: None,
- "WarrantyGuarantorParts": lambda e, p: None,
- "WarrantyDurationParts": lambda e, p: None,
- "WarrantyGuarantorLabour": lambda e, p: None,
- "WarrantyDurationLabour": lambda e, p: None,
- "DurationUnit": lambda e, p: "months",
- "WarrantyDescription": lambda e, p: None,
- "ReplacementCost": lambda e, p: None,
- "ExpectedLife": lambda e, p: None,
- "NominalLength": lambda e, p: None,
- "NominalWidth": lambda e, p: None,
- "NominalHeight": lambda e, p: None,
- },
- "components": {
- "SerialNumber": lambda e, p: None,
- "InstallationDate": lambda e, p: None,
- "WarrantyStartDate": lambda e, p: None,
- "TagNumber": lambda e, p: None,
- "BarCode": lambda e, p: None,
- "AssetIdentifier": lambda e, p: None,
- }
- }
- # AOH-BEM
- self.custom_parameters = {
- "types": {
- "ProcurementMethod": lambda e, p: None,
- "ManufacturerOrganizationName": lambda e, p: None,
- "SupplierOrganizationName": lambda e, p: None,
- "ModelNumber": lambda e, p: None,
- "WarrantyOrganizationName": lambda e, p: None,
- "WarrantyDuration": lambda e, p: None,
- "SpecificationSection": lambda e, p: None,
- "SubmittalID": lambda e, p: None,
- "ProductURL": lambda e, p: None,
- },
- "components": {
- "InstallationDate": lambda e, p: None,
- "WarrantyStartDate": lambda e, p: None,
- "InstalledModelNumber": lambda e, p: None,
- "SerialNumber": lambda e, p: None,
- "BarCode": lambda e, p: None,
- "TagNumber": lambda e, p: None,
- "OwnerAssetID": lambda e, p: None,
- "FluidHotFeedName": lambda e, p: None,
- "FluidColdFeedName": lambda e, p: None,
- "ElectricPanelName": lambda e, p: None,
- "ElectricCircuitName": lambda e, p: None,
- "ControlledByName": lambda e, p: None,
- "InterlockedWithName": lambda e, p: None,
- "PartOfAssemblyName": lambda e, p: None,
- }
- }
- self.picklists = {
- "Category-Role": [],
- "Category-Facility": [],
- "FloorType": [],
- "Category-Space": [],
- "ZoneType": [],
- "Category-Product": [],
- "AssetType": [],
- "DurationUnit": ["day"], # See note about hardcoded day below
- "Category-Element": [],
- "SpareType": [],
- "ApprovalBy": [],
- "StageType": [],
- "objType": [],
- }
- self.default_date = (datetime.datetime(1970, 1, 1) + datetime.timedelta(seconds=-2177452801)).isoformat()
-
- def parse(self, files):
- self.files = files
- for category, get_category in self.categories.items():
- setattr(self, category, {})
- get_category()
-
- def get_actors(self):
- for ifc in self.files.values():
- for element in ifc.by_type("IfcActor"):
- name = element.TheActor.Name
- self.actors[name] = self.get_actor(element)
-
- def get_actor(self, element):
- psets = ifcopenshell.util.element.get_psets(element)
- return {
- "Name": element.TheActor.Name,
- "Category": self.get_classification(element),
- "Email": self.get_actor_address(element, "ElectronicMailAddresses"),
- "Phone": self.get_actor_address(element, "TelephoneNumbers"),
- "CompanyURL": self.get_actor_address(element, "WWWHomePageURL"),
- "Department": self.get_actor_address(element, "InternalLocation"),
- "Address1": self.get_actor_address(element, "AddressLines"),
- "Address2": self.get_actor_address(element, "Town"),
- "StateRegion": self.get_actor_address(element, "Region"),
- "PostalCode": self.get_actor_address(element, "PostalCode"),
- "Country": self.get_actor_address(element, "Country"),
- }
-
- def get_actor_address(self, element, name):
- for address in element.TheActor.Addresses or []:
- if hasattr(address, name) and getattr(address, name, None):
- result = getattr(address, name)
- if isinstance(result, tuple):
- return result[0]
- return result
-
- def get_facilities(self):
- for key, ifc in self.files.items():
- if "arch" not in key:
- continue
- element = ifc.by_type("IfcBuilding")[0]
- self.facilities[element.Name] = {
- "Name": element.Name,
- "AuthorOrganizationName": element.OwnerHistory.OwningUser.TheOrganization.Name,
- "AuthorDate": ifcopenshell.util.date.ifc2datetime(
- ifc.by_type("IfcProject")[0].OwnerHistory.CreationDate
- ).isoformat(),
- "Category": self.get_classification(element),
- "ProjectName": element.Decomposes[0].RelatingObject.Decomposes[0].RelatingObject.Name,
- "SiteName": element.Decomposes[0].RelatingObject.Name,
- "LinearUnits": "millimeters",
- "AreaUnits": "square meters",
- "AreaMeasurement": "Revit",
- "Phase": element.Decomposes[0].RelatingObject.Decomposes[0].RelatingObject.Phase,
- "ModelSoftware": element.OwnerHistory.OwningApplication.ApplicationFullName,
- "ModelProjectID": ifc.by_type("IfcProject")[0].GlobalId,
- "ModelSiteID": element.Decomposes[0].RelatingObject.GlobalId,
- "ModelBuildingID": element.GlobalId,
- }
-
- def get_classification(self, element):
- references = list(ifcopenshell.util.classification.get_references(element))
- if references:
- if hasattr(references[0], "Identification"):
- return "{}:{}".format(references[0].Identification, references[0].Name)
- return "{}:{}".format(references[0].ItemReference, references[0].Name)
-
- def get_floors(self):
- for key, ifc in self.files.items():
- if "arch" not in key:
- continue
- storeys = ifc.by_type("IfcBuildingStorey")
- for element in storeys:
- self.get_floor(element)
-
- def get_floor(self, element):
- name = element.Name
- elevation = element.ObjectPlacement.RelativePlacement.Location.Coordinates[2]
- self.floors[name] = {
- "Name": name,
- "AuthorOrganizationName": element.OwnerHistory.OwningUser.TheOrganization.Name,
- "AuthorDate": ifcopenshell.util.date.ifc2datetime(element.OwnerHistory.CreationDate).isoformat(),
- "Category": "Level",
- "ModelSoftware": element.OwnerHistory.OwningApplication.ApplicationFullName,
- "ModelObject": element.is_a(),
- "ModelID": element.GlobalId,
- "Elevation": elevation,
- }
-
- def get_property(self, psets, pset_name, prop_name, decimals=None):
- if pset_name in psets:
- result = psets[pset_name].get(prop_name, None)
- if decimals is None or result is None:
- return result
- return round(result, decimals)
-
- def get_custom_parameters(self, category, data, element, psets):
- for parameter, get_parameter in self.custom_parameters.get(category, {}).items():
- data[parameter] = get_parameter(element, psets)
-
- def get_spaces(self):
- for key, ifc in self.files.items():
- if "arch" not in key:
- continue
- primary_keys = []
- for element in ifc.by_type("IfcSpace"):
- name = element.Name
- primary_keys.append(name)
- # TODO: not correct mapping
- psets = ifcopenshell.util.element.get_psets(element)
- data = {
- "Name": name,
- "AuthorOrganizationName": element.OwnerHistory.OwningUser.TheOrganization.Name,
- "AuthorDate": ifcopenshell.util.date.ifc2datetime(element.OwnerHistory.CreationDate).isoformat(),
- "Category": self.get_classification(element),
- "LevelName": element.Decomposes[0].RelatingObject.Name,
- "Description": element.LongName,
- "ModelSoftware": element.OwnerHistory.OwningApplication.ApplicationFullName,
- "ModelID": element.GlobalId,
- "BuildingRoomNumber": None,
- "UsableHeight": self.get_property(psets, "Data", "COBie.Space.UsableHeight", decimals=0),
- "AreaGross": self.get_property(psets, "Qto_SpaceBaseQuantities", "GrossFloorArea", decimals=2),
- "AreaNet": self.get_property(psets, "Qto_SpaceBaseQuantities", "NetFloorArea", decimals=2),
- }
- self.get_custom_parameters("spaces", data, element, psets)
- self.spaces[name] = data
-
- def get_zones(self):
- for ifc in self.files.values():
- for element in ifc.by_type("IfcZone"):
- for rel in element.IsGroupedBy:
- for space in rel.RelatedObjects:
- if not space.is_a("IfcSpace"):
- continue
- self.zones[(element.Name or "Unnamed") + (space.Name or "Unnamed")] = {
- "Name": element.Name,
- "AuthorOrganizationName": element.OwnerHistory.OwningUser.TheOrganization.Name,
- "AuthorDate": ifcopenshell.util.date.ifc2datetime(
- element.OwnerHistory.CreationDate
- ).isoformat(),
- "Category": "Occupancy",
- "SpaceName": space.Name,
- "ModelSoftware": element.OwnerHistory.OwningApplication.ApplicationFullName,
- "ModelID": element.GlobalId,
- "ParentZoneName": None,
- }
-
- def get_systems(self):
- for discipline, ifc in self.files.items():
- for element in ifc.by_type("IfcSystem"):
- name = element.Name
- self.systems[name] = {
- "Name": name,
- "AuthorOrganizationName": element.OwnerHistory.OwningUser.TheOrganization.Name,
- "AuthorDate": ifcopenshell.util.date.ifc2datetime(element.OwnerHistory.CreationDate).isoformat(),
- "Category": None,
- "ModelSoftware": element.OwnerHistory.OwningApplication.ApplicationFullName,
- "ModelID": element.GlobalId,
- "ParentSystemName": None,
- }
-
- def get_types(self):
- for discipline, ifc in self.files.items():
- self.get_types_from_file(discipline)
-
- def get_types_from_file(self, ifc_file):
- primary_keys = []
- for element in ifcopenshell.util.fm.get_fmhem_types(self.files[ifc_file]):
- name = element.Name
- primary_keys.append(name)
- psets = ifcopenshell.util.element.get_psets(element)
- data = {
- "Name": name,
- "AuthorOrganizationName": element.OwnerHistory.OwningUser.TheOrganization.Name,
- "AuthorDate": ifcopenshell.util.date.ifc2datetime(element.OwnerHistory.CreationDate).isoformat(),
- "Category": self.get_classification(element),
- "Description": element.Description,
- "ModelSoftware": element.OwnerHistory.OwningApplication.ApplicationFullName,
- "ModelObject": "{}[{}]".format(element.is_a(), ifcopenshell.util.element.get_predefined_type(element)),
- "ModelID": element.GlobalId,
- }
- self.get_custom_parameters("types", data, element, psets)
- self.types[name] = data
-
- def get_components(self):
- for discipline, ifc in self.files.items():
- self.get_components_from_file(discipline)
-
- def get_components_from_file(self, ifc_file):
- for element_type in ifcopenshell.util.fm.get_fmhem_types(self.files[ifc_file]):
- elements = ifcopenshell.util.element.get_types(element_type)
- if not elements:
- self.logger.warning("The type has no occurrences %s", element_type)
- continue
- for element in elements:
- name = element.Name
-
- system = None
- for rel in element.HasAssignments:
- if rel.is_a("IfcRelAssignsToGroup") and rel.RelatingGroup.is_a("IfcSystem"):
- system = rel.RelatingGroup.Name
-
- space = ifcopenshell.util.element.get_container(element)
- space_name = space.Name if space.is_a("IfcSpace") else None
-
- psets = ifcopenshell.util.element.get_psets(element)
-
- data = {
- "Name": name,
- "AuthorOrganizationName": element.OwnerHistory.OwningUser.TheOrganization.Name,
- "AuthorDate": ifcopenshell.util.date.ifc2datetime(element.OwnerHistory.CreationDate).isoformat(),
- "TypeName": element_type.Name,
- "SpaceName": space_name,
- "SystemName": system,
- "ModelSoftware": element.OwnerHistory.OwningApplication.ApplicationFullName,
- "ModelObject": "{}[{}]".format(
- element.is_a(), ifcopenshell.util.element.get_predefined_type(element)
- ),
- "ModelID": element.GlobalId,
- }
- self.get_custom_parameters("components", data, element, psets)
- self.components[name] = data
-
- def get_documents(self):
- for ifc in self.files.values():
- for rel in ifc.by_type("IfcRelAssociatesDocument"):
- element = rel.RelatingDocument
- if element.is_a("IfcDocumentInformation"):
- continue
- for related_object in rel.RelatedObjects:
- worksheet_row = related_object.Name
- if ifc.schema == "IFC2X3":
- referenced_document = element.ReferenceToDocument[0]
- identification = referenced_document.ItemReference
- author_date = None
- if referenced_document.CreationTime:
- author_date = ifcopenshell.util.date.ifc2datetime(
- referenced_document.CreationTime
- ).isoformat()
- else:
- referenced_document = element.ReferencedDocument
- identification = referenced_document.Identification
- author_date = referenced_document.CreationTime
-
- worksheet_name = None
- if related_object.is_a("IfcSpace"):
- worksheet_name = "Space"
- elif related_object.is_a("IfcTypeObject"):
- worksheet_name = "Type"
-
- self.documents[identification + worksheet_name + worksheet_row] = {
- "Name": identification,
- "AuthorOrganizationName": referenced_document.DocumentOwner.Name,
- "AuthorDate": author_date,
- "Category": referenced_document.Purpose,
- "WorksheetName": worksheet_name,
- "WorksheetRow": worksheet_row,
- "Revision": referenced_document.Revision,
- "Location": referenced_document.Location,
- "Description": referenced_document.Name,
- "SpecificationSection": None,
- "SubmittalID": None,
- "SourceURL": None,
- }
diff --git a/src/ifcfm/ifcfm/util.py b/src/ifcfm/ifcfm/util.py
deleted file mode 100644
index 199b280b6e..0000000000
--- a/src/ifcfm/ifcfm/util.py
+++ /dev/null
@@ -1,94 +0,0 @@
-# IfcFM - IFC for facility management
-# Copyright (C) 2021 Dion Moult
-#
-# This file is part of IfcFM.
-#
-# IfcFM is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# IfcFM 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 Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with IfcFM. If not, see .
-
-
-import textwrap
-import ifcopenshell
-import ifcopenshell.util.fm
-import ifcopenshell.util.schema
-import ifcopenshell.util.attribute
-
-
-def print_element(declaration, should_print_subtypes=True):
- if declaration.name() in ifcopenshell.util.fm.fmhem_excluded_classes:
- pass
- elif declaration.is_abstract():
- pass
- else:
- types = []
- for attribute in declaration.all_attributes():
- if attribute.name() == "PredefinedType":
- types = list(ifcopenshell.util.attribute.get_enum_items(attribute))
- if "NOTDEFINED" in types:
- types.remove("NOTDEFINED")
- print("{}".format(declaration.name()))
- if types:
- types = sorted(types)
- for line in textwrap.wrap(", ".join(types), width=70):
- print("\t\t{}".format(line))
- if should_print_subtypes:
- for subtype in declaration.subtypes():
- print_element(subtype)
-
-
-def print_fmhem_documentation(schema="IFC4"):
- if schema == "IFC4":
- classes = ifcopenshell.util.fm.fmhem_classes_ifc4
- elif schema == "IFC2X3":
- classes = ifcopenshell.util.fm.fmhem_classes_ifc2x3
- schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema)
- for ifc_class in classes:
- try:
- declaration = schema.declaration_by_name(ifc_class)
- print_element(declaration)
- except:
- pass
-
-
-def print_all_documentation(schema="IFC4"):
- if schema == "IFC4":
- classes = ifcopenshell.util.fm.fmhem_classes_ifc4
- elif schema == "IFC2X3":
- classes = ifcopenshell.util.fm.fmhem_classes_ifc2x3
- schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema)
- class_queue = [schema.declaration_by_name("IfcElementType")]
- maintainable_declarations = []
- other_declarations = []
- while class_queue:
- declaration = class_queue.pop(0)
- class_queue.extend(declaration.subtypes())
- if declaration.is_abstract():
- continue
- is_maintainable = False
- for ifc_class in classes:
- if ifcopenshell.util.schema.is_a(declaration, ifc_class):
- is_maintainable = True
- break
- if is_maintainable:
- maintainable_declarations.append(declaration)
- else:
- other_declarations.append(declaration)
- print("# Maintainable classes\n")
- for declaration in maintainable_declarations:
- print_element(declaration, should_print_subtypes=False)
- print("\n\n# Other classes\n")
- for declaration in other_declarations:
- print_element(declaration, should_print_subtypes=False)
-
-
-print_all_documentation("IFC2X3")
diff --git a/src/ifcfm/ifcfm/writer.py b/src/ifcfm/ifcfm/writer.py
deleted file mode 100644
index bb102ab63c..0000000000
--- a/src/ifcfm/ifcfm/writer.py
+++ /dev/null
@@ -1,344 +0,0 @@
-# IfcFM - IFC for facility management
-# Copyright (C) 2021 Dion Moult
-#
-# This file is part of IfcFM.
-#
-# IfcFM is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# IfcFM 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 Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with IfcFM. If not, see .
-
-import csv
-
-try:
- from xlsxwriter import Workbook
-except:
- pass # No XLSX support
-
-try:
- from odf.opendocument import OpenDocumentSpreadsheet
- from odf.style import Style, TableCellProperties
- from odf.table import Table, TableRow, TableCell
- from odf.text import P
-except:
- pass # No ODF support
-
-
-# https://stackoverflow.com/questions/1143671/how-to-sort-objects-by-multiple-keys-in-python
-
-from operator import itemgetter as i
-from functools import cmp_to_key
-
-
-def cmp(x, y):
- """
- Replacement for built-in function cmp that was removed in Python 3
-
- Compare the two objects x and y and return an integer according to
- the outcome. The return value is negative if x < y, zero if x == y
- and strictly positive if x > y.
-
- https://portingguide.readthedocs.io/en/latest/comparisons.html#the-cmp-function
- """
-
- try:
- return (x > y) - (x < y)
- except:
- return 0
-
-
-def multikeysort(items, columns):
- comparers = [((i(col[1:].strip()), -1) if col.startswith("-") else (i(col.strip()), 1)) for col in columns]
-
- def comparer(left, right):
- comparer_iter = (cmp(fn(left), fn(right)) * mult for fn, mult in comparers)
- return next((result for result in comparer_iter if result), 0)
-
- return sorted(items, key=cmp_to_key(comparer))
-
-
-class Writer:
- def __init__(self, parser, filename=None):
- self.filename = filename
- self.parser = parser
- self.sheet_data = {}
- self.colours = {
- "r": "fdff8e", # Required
- "i": "fdcd94", # Internal reference
- "e": "cd95ff", # External reference
- "o": "cdffc8", # Optional
- "s": "c0c0c0", # Secondary information
- "p": "9ccaff", # Project specific
- "n": "000000", # Not used
- }
- self.colours = {
- "r": "dc8774", # Required
- "i": "eda786", # Internal reference
- "e": "96c7d0", # External reference
- "o": "ddb873", # Optional or edd889
- "s": "dddddd", # Secondary information
- "p": "b8dd73", # Project specific
- "n": "000000", # Not used
- }
- self.sheets = {
- "Actor": {
- "data": self.parser.actors,
- "fields": [
- "Name",
- "Category",
- "Email",
- "Phone",
- "CompanyURL",
- "Department",
- "Address1",
- "Address2",
- "StateRegion",
- "PostalCode",
- "Country",
- ],
- "colours": "rirrrrrrrrr",
- "order": ["Name"],
- },
- "Facility": {
- "data": self.parser.facilities,
- "fields": [
- "Name",
- "AuthorOrganizationName",
- "AuthorDate",
- "Category",
- "ProjectName",
- "SiteName",
- "LinearUnits",
- "AreaUnits",
- "AreaMeasurement",
- "Phase",
- "ModelSoftware",
- "ModelProjectID",
- "ModelSiteID",
- "ModelBuildingID",
- ],
- "colours": "ririrrrrrreeee",
- "order": ["Name"],
- },
- "Floor": {
- "data": self.parser.floors,
- "fields": [
- "Name",
- "AuthorOrganizationName",
- "AuthorDate",
- "Category",
- "ModelSoftware",
- "ModelObject",
- "ModelID",
- "Elevation",
- ],
- "colours": "ririeeer",
- "order": ["Elevation"],
- },
- "Space": {
- "data": self.parser.spaces,
- "fields": [
- "Name",
- "AuthorOrganizationName",
- "AuthorDate",
- "Category",
- "LevelName",
- "Description",
- "ModelSoftware",
- "ModelID",
- "BuildingRoomNumber",
- "UsableHeight",
- "AreaGross",
- "AreaNet",
- ],
- "colours": "ririireerrrr",
- "order": ["LevelName", "Name"],
- },
- "Zone": {
- "data": self.parser.zones,
- "fields": [
- "Name",
- "AuthorOrganizationName",
- "AuthorDate",
- "Category",
- "SpaceName",
- "ModelSoftware",
- "ModelID",
- "ParentZoneName",
- ],
- "colours": "ririieei",
- "order": ["Name", "SpaceName"],
- },
- "Type": {
- "data": self.parser.types,
- "fields": [
- "Name",
- "AuthorOrganizationName",
- "AuthorDate",
- "Category",
- "Description",
- "ModelSoftware",
- "ModelObject",
- "ModelID",
- ],
- "colours": "ririreee",
- "order": ["ModelObject", "Name"],
- },
- "Component": {
- "data": self.parser.components,
- "fields": [
- "Name",
- "AuthorOrganizationName",
- "AuthorDate",
- "TypeName",
- "SpaceName",
- "SystemName",
- "ModelSoftware",
- "ModelObject",
- "ModelID",
- ],
- "colours": "ririiieee",
- "order": ["ModelObject", "Name"],
- },
- "System": {
- "data": self.parser.systems,
- "fields": [
- "Name",
- "AuthorOrganizationName",
- "AuthorDate",
- "Category",
- "ModelSoftware",
- "ModelID",
- "ParentSystemName",
- ],
- "colours": "ririeei",
- "order": ["Name"],
- },
- "Document": {
- "data": self.parser.documents,
- "fields": [
- "Name",
- "AuthorOrganizationName",
- "AuthorDate",
- "Category",
- "WorksheetName",
- "WorksheetRow",
- "Revision",
- "Location",
- "Description",
- "SpecificationSection",
- "SubmittalID",
- "SourceURL",
- ],
- "colours": "ririiirrrrer",
- "order": ["Name"],
- },
- }
-
- def write(self):
- for category, spec in self.sheets.items():
- self.write_data(category, spec["data"], spec["fields"], spec["colours"], spec["order"])
-
- def write_data(self, sheet, data, fieldnames, colours, sort_fields):
- self.sheet_data[sheet] = {"headers": fieldnames, "colours": colours, "rows": []}
- for row in multikeysort(list(data.values()), sort_fields):
- values = []
- for fieldname in fieldnames:
- values.append(row[fieldname])
- self.sheet_data[sheet]["rows"].append(values)
-
-
-class CsvWriter(Writer):
- def write(self):
- super().write()
- for sheet, data in self.sheet_data.items():
- with open(os.path.join(self.filename, "{}.csv".format(sheet)), "w", newline="", encoding="utf-8") as file:
- writer = csv.writer(file)
- writer.writerow(data["headers"])
- for row in data["rows"]:
- writer.writerow(row)
-
-
-class XlsWriter(Writer):
- def write(self):
- super().write()
- self.workbook = Workbook(self.filename + ".xlsx")
-
- self.cell_formats = {}
- for key, value in self.colours.items():
- self.cell_formats[key] = self.workbook.add_format()
- self.cell_formats[key].set_bg_color(value)
-
- for sheet in self.sheets.keys():
- self.write_worksheet(sheet)
- self.workbook.close()
-
- def write_worksheet(self, name):
- worksheet = self.workbook.add_worksheet(name)
- r = 0
- c = 0
- for header in self.sheet_data[name]["headers"]:
- cell = worksheet.write(r, c, header, self.cell_formats["s"])
- c += 1
- c = 0
- r += 1
- for row in self.sheet_data[name]["rows"]:
- c = 0
- for col in row:
- if c >= len(self.sheet_data[name]["colours"]):
- cell_format = "p"
- else:
- cell_format = self.sheet_data[name]["colours"][c]
- cell = worksheet.write(r, c, col, self.cell_formats[cell_format])
- c += 1
- r += 1
-
-
-class OdsWriter(Writer):
- def write(self):
- super().write()
- self.doc = OpenDocumentSpreadsheet()
-
- self.cell_formats = {}
- for key, value in self.colours.items():
- style = Style(name=key, family="table-cell")
- style.addElement(TableCellProperties(backgroundcolor="#" + value))
- self.doc.automaticstyles.addElement(style)
- self.cell_formats[key] = style
-
- for sheet in self.sheets.keys():
- self.write_table(sheet)
- self.doc.save(self.filename, True)
-
- def write_table(self, name):
- table = Table(name=name)
- tr = TableRow()
- for header in self.sheet_data[name]["headers"]:
- tc = TableCell(valuetype="string", stylename="s")
- tc.addElement(P(text=header))
- tr.addElement(tc)
- table.addElement(tr)
- for row in self.sheet_data[name]["rows"]:
- tr = TableRow()
- c = 0
- for col in row:
- if c >= len(self.sheet_data[name]["colours"]):
- cell_format = "p"
- else:
- cell_format = self.sheet_data[name]["colours"][c]
- tc = TableCell(valuetype="string", stylename=cell_format)
- if col is None:
- col = "NULL"
- tc.addElement(P(text=col))
- tr.addElement(tc)
- c += 1
- table.addElement(tr)
- self.doc.spreadsheet.addElement(table)
diff --git a/src/ifcgeom/IfcGeom.cpp b/src/ifcgeom/IfcGeom.cpp
index d0b5ebd19f..e743461445 100644
--- a/src/ifcgeom/IfcGeom.cpp
+++ b/src/ifcgeom/IfcGeom.cpp
@@ -1084,11 +1084,9 @@ bool IfcGeom::Kernel::convert_layerset(const IfcSchema::IfcProduct* product, std
TopExp_Explorer exp(axis_shape, TopAbs_EDGE);
TopoDS_Edge axis_edge;
- int edge_count = 0;
if (exp.More()) {
axis_edge = TopoDS::Edge(exp.Current());
- ++ edge_count;
} else {
Logger::Message(Logger::LOG_WARNING, "No edge found in axis representation:", product);
return false;
diff --git a/src/ifcgeom_schema_agnostic/IfcGeomRepresentation.h b/src/ifcgeom_schema_agnostic/IfcGeomRepresentation.h
index 0d7df7d7d1..9e5fe20d3f 100644
--- a/src/ifcgeom_schema_agnostic/IfcGeomRepresentation.h
+++ b/src/ifcgeom_schema_agnostic/IfcGeomRepresentation.h
@@ -159,8 +159,8 @@ namespace IfcGeom {
, _normals(normals)
, uvs_(uvs)
, _material_ids(material_ids)
- , styles_(styles)
, _item_ids(item_ids)
+ , styles_(styles)
{
for (auto& s : styles_) {
_materials.push_back(IfcGeom::Material(s));
diff --git a/src/ifcmax/CMakeLists.txt b/src/ifcmax/CMakeLists.txt
index 7db41dc2e1..5ecadc882c 100644
--- a/src/ifcmax/CMakeLists.txt
+++ b/src/ifcmax/CMakeLists.txt
@@ -16,32 +16,72 @@
# along with this program. If not, see . #
# #
################################################################################
+
+# check for 3ds Max SDK
foreach(max_year RANGE 2014 2030)
+ set(max_sdk "$ENV{ADSK_3DSMAX_SDK_${max_year}}")
-set(max_sdk "$ENV{ADSK_3DSMAX_SDK_${max_year}}")
-if (NOT "${max_sdk}" STREQUAL "")
+ if(NOT "${max_sdk}" STREQUAL "")
+ message(STATUS "Autodesk 3ds Max SDK ${max_year} found at ${max_sdk}")
+ list(APPEND FOUND_MAX_YEARS ${max_year})
+ list(APPEND FOUND_MAX_SDKS ${max_sdk})
+ set(HAS_MAX TRUE)
+ endif()
+endforeach()
-INCLUDE_DIRECTORIES(${INCLUDE_DIRECTORIES} ${OCC_INCLUDE_DIR} ${OPENCOLLADA_INCLUDE_DIRS} ${ICU_INCLUDE_DIR}
- ${Boost_INCLUDE_DIRS} ${max_sdk}/include
-)
+if(HAS_MAX)
+ # build libraray for each found 3ds Max SDK
+ foreach(max_year max_sdk IN ZIP_LISTS FOUND_MAX_YEARS FOUND_MAX_SDKS)
-# All recent versions of 3ds Max (2014 and newer) are 64-bit only so assume lib/x64 directory
-LINK_DIRECTORIES(${LINK_DIRECTORIES} ${IfcOpenShell_BINARY_DIR} ${OCC_LIBRARY_DIR} ${OPENCOLLADA_LIBRARY_DIR}
- ${ICU_LIBRARY_DIR} ${Boost_LIBRARY_DIRS} ${max_sdk}/lib/x64/Release
-)
+ message(STATUS "Building IFCMax library for Autodesk 3ds Max SDK ${max_year}")
-ADD_LIBRARY(IfcMax_${max_year} SHARED IfcMax.h IfcMax.cpp)
+ include_directories(${INCLUDE_DIRECTORIES} ${OCC_INCLUDE_DIR} ${OPENCOLLADA_INCLUDE_DIRS} ${ICU_INCLUDE_DIR}
+ ${Boost_INCLUDE_DIRS} ${max_sdk}/include
+ )
-# TODO: find the minimal subset of 3dsmax libraries to reference
-TARGET_LINK_LIBRARIES(IfcMax_${max_year} ${IFCOPENSHELL_LIBRARIES} Comctl32.lib zlibdll.lib bmm.lib core.lib CustDlg.lib edmodel.lib expr.lib
- flt.lib geom.lib gfx.lib gup.lib imageViewers.lib ManipSys.lib maxnet.lib Maxscrpt.lib
- maxutil.lib MenuMan.lib menus.lib mesh.lib MNMath.lib Paramblk2.lib particle.lib Poly.lib RenderUtil.lib
- tessint.lib viewfile.lib ${OPENCASCADE_LIBRARIES}
-)
+ # All recent versions of 3ds Max (2014 and newer) are 64-bit only so assume lib/x64 directory
+ link_directories(${LINK_DIRECTORIES} ${IfcOpenShell_BINARY_DIR} ${OCC_LIBRARY_DIR} ${OPENCOLLADA_LIBRARY_DIR}
+ ${ICU_LIBRARY_DIR} ${Boost_LIBRARY_DIRS} ${max_sdk}/lib/x64/Release
+ )
-SET_TARGET_PROPERTIES(IfcMax_${max_year} PROPERTIES SUFFIX ".dli")
+ add_library(IfcMax_${max_year} SHARED IfcMax.h IfcMax.cpp)
-INSTALL(TARGETS IfcMax_${max_year} RUNTIME DESTINATION ${BINDIR})
+ # TODO: find the minimal subset of 3dsmax libraries to reference
+ target_link_libraries(IfcMax_${max_year} ${IFCOPENSHELL_LIBRARIES}
+ bmm.lib
+ Comctl32.lib
+ core.lib
+ CustDlg.lib
+ edmodel.lib
+ expr.lib
+ flt.lib
+ geom.lib
+ gfx.lib
+ gup.lib
+ imageViewers.lib
+ ManipSys.lib
+ maxnet.lib
+ Maxscrpt.lib
+ maxutil.lib
+ MenuMan.lib
+ menus.lib
+ mesh.lib
+ MNMath.lib
+ Paramblk2.lib
+ particle.lib
+ Poly.lib
+ RenderUtil.lib
+ tessint.lib
+ viewfile.lib
+ zlibdll.lib
+ ${OPENCASCADE_LIBRARIES}
+ )
+ set_target_properties(IfcMax_${max_year} PROPERTIES SUFFIX ".dli")
+
+ install(TARGETS IfcMax_${max_year} RUNTIME DESTINATION ${BINDIR})
+ endforeach()
+else()
+ message(STATUS "Autodesk 3ds Max SDK not found, is required to build IFCMax.")
endif()
-endforeach()
\ No newline at end of file
+
diff --git a/src/ifcopenshell-python/Makefile b/src/ifcopenshell-python/Makefile
index 9c47c7ae68..05442dacbb 100644
--- a/src/ifcopenshell-python/Makefile
+++ b/src/ifcopenshell-python/Makefile
@@ -38,7 +38,7 @@ PYNUMBER:=311
endif
ifeq ($(PLATFORM), linux)
-PLATFORMTAG:=manylinux1_x86_64
+PLATFORMTAG:=manylinux_2_31_x86_64
endif
ifeq ($(PLATFORM), macos)
PLATFORMTAG:=macosx_10_15_x86_64
diff --git a/src/ifcopenshell-python/docs/bcf.rst b/src/ifcopenshell-python/docs/bcf.rst
new file mode 100644
index 0000000000..6e09d19de6
--- /dev/null
+++ b/src/ifcopenshell-python/docs/bcf.rst
@@ -0,0 +1,112 @@
+BCF
+===
+
+**BIM Collaboration Format** (BCF) is a standard by buildingSMART to manage and
+exchange coordination topics between disciplines collaborating on a project.
+For example, when there is an issue during the design, engineering, or
+construction of a project, a topic may be created, assigned, prioritised,
+commented, or linked to objects in a BIM model or camera location.
+
+There are two implementations of BCF:
+
+1. **BCF-XML**: an XML file-based exchange of collaboration topics. This is
+ useful for mass imports, exports, data migration across CDEs, or fully
+ offline implementations.
+2. **BCF-API**: an online RESTful API-based management of collaboration topics.
+ When topics are managed by a CDE, if the CDE follows the OpenCDE
+ specification by buildingSMART, their topics may be accessed and manipulated
+ using BCF.
+
+The upstream documentation by buildingSMART for BCF is available here:
+
+1. `BCF-XML 2.1 upstream documentation
+ `__.
+2. `BCF-XML 3.0 upstream documentation
+ `__.
+3. `BCF-API 3.0 upstream documentation
+ `__.
+
+The IfcOpenShell **BCF** library supports BCF-XML version 2.1 and 3.0, and
+BCF-API 3.0.
+
+BCF-XML
+-------
+
+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.
+
+.. code-block:: 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()
+
+BCF-API
+-------
+
+The ``bcfapi`` module lets you interact with the BCF-API standard.
+
+.. code-block:: 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)
diff --git a/src/ifcopenshell-python/docs/bimserver-plugin.rst b/src/ifcopenshell-python/docs/bimserver-plugin.rst
index 432c499f87..b9d9beae07 100644
--- a/src/ifcopenshell-python/docs/bimserver-plugin.rst
+++ b/src/ifcopenshell-python/docs/bimserver-plugin.rst
@@ -1,16 +1,5 @@
BIMServer-Plugin
================
-This documentation is free software! You are free to contribute and help write
-this document.
-
-.. toctree::
- :maxdepth: 1
- :caption: Contents:
-
-Indices and tables
-------------------
-
-* :ref:`genindex`
-* :ref:`modindex`
-* :ref:`search`
+The BIMServer-Plugin is a plugin to the open source BIMServer CDE to allow you
+to use IfcOpenShell to parse, view, and audit models.
diff --git a/src/ifcopenshell-python/docs/bimtester.rst b/src/ifcopenshell-python/docs/bimtester.rst
index 8640df8bab..bdecbc2131 100644
--- a/src/ifcopenshell-python/docs/bimtester.rst
+++ b/src/ifcopenshell-python/docs/bimtester.rst
@@ -1,16 +1,4 @@
BIMTester
=========
-This documentation is free software! You are free to contribute and help write
-this document.
-
-.. toctree::
- :maxdepth: 1
- :caption: Contents:
-
-Indices and tables
-------------------
-
-* :ref:`genindex`
-* :ref:`modindex`
-* :ref:`search`
+BIMTester is a utility that allows you to write Gherkin-based tests for models.
diff --git a/src/ifcopenshell-python/docs/bsdd.rst b/src/ifcopenshell-python/docs/bsdd.rst
new file mode 100644
index 0000000000..d8f53b4f0f
--- /dev/null
+++ b/src/ifcopenshell-python/docs/bsdd.rst
@@ -0,0 +1,56 @@
+bSDD
+====
+
+The **buildingSMART Data Dictionary** (bSDD) is an online RESTful centralised
+API provided by buildingSMART that allows you to search for standardised
+classifications and properties.
+
+For example, if you want to assign a Uniclass classification system (popular in
+the UK) or an Omniclass classification system (popular in the US) to elements
+in your model, instead of downloading the classification system from their
+website, you can directly search the bSDD. This ensures that you are always up
+to date, and that codes are entered correctly (without spelling mistakes,
+correct formatting, etc).
+
+The bSDD search results may also be filtered based on IFC class. This will make
+it quick to shortlist relevant classification codes and properties to a
+particular object.
+
+The bSDD also stores information on whether or not classification systems
+require additional standard properties to be filled out, and whether they
+should be filled out in a particular way. For example, all countries need to
+fill out a "Fire Rating" property for walls, but they have different ways to
+fill it out. Local governments (or companies) may submit their standard to the
+bSDD so that all bSDD-compatible BIM applications can look up the property and
+fill it out in a standardised way (such as picking for a list of preset
+possible values defined by the local government).
+
+More reading:
+
+1. `Swagger API docs `_
+2. `bSDD Github Repository `_
+
+Examples
+--------
+
+Learning how to use the bSDD is best done by reading the official Swagger API docs.
+
+.. code-block:: python
+
+ client = Client()
+
+ # Get a list of "dictionary domains". For example, Uniclass (by the NBS organisation) might be one domain.
+ print(client.Domain())
+
+ # For example, search the Netherland's Nlsfb2005 classification standard for all codes that apply to an IfcWall.
+ print(client.SearchListOpen("http://identifier.buildingsmart.org/uri/nlsfb/nlsfb2005-2.2", RelatedIfcEntity="IfcWall"))
+
+ # Alternatively, search up a particular classification code.
+ data = client.Classification("http://identifier.buildingsmart.org/uri/nlsfb/nlsfb2005-2.2/class/21.21")
+ print(data)
+
+ # You may also apply default properties (if the classification system on
+ # the bSDD defines them) to your IFC element. For example, if a
+ # classification code is for a load bearing wall, it can automatically set
+ # the "LoadBearing" property to True for you.
+ apply_ifc_classification_properties(ifc_file, element, data["classificationProperties"])
diff --git a/src/ifcopenshell-python/docs/conf.py b/src/ifcopenshell-python/docs/conf.py
index 7335a4c80a..befbaae79b 100644
--- a/src/ifcopenshell-python/docs/conf.py
+++ b/src/ifcopenshell-python/docs/conf.py
@@ -36,7 +36,7 @@ sys.path.insert(0, os.path.abspath('..'))
# -- Project information -----------------------------------------------------
project = "IfcOpenShell"
-copyright = "2020-2022, IfcOpenShell Contributors"
+copyright = "2011-2023, IfcOpenShell Contributors"
author = "IfcOpenShell Contributors"
# The full version, including alpha/beta/rc tags
@@ -54,7 +54,10 @@ release = "0.7.0"
# - No subnav making it really hard to navigate
# - Kinda hacky setup https://stackoverflow.com/questions/2701998/sphinx-autodoc-is-not-automatic-enough
# - I couldn't customise the template to show submodules above members which makes API discovery hard for users
-extensions = ["autoapi.extension"]
+extensions = ["autoapi.extension", "sphinx.ext.autosectionlabel"]
+
+# Auto add document prefixes to help guarantee uniqueness of automatic section references.
+autosectionlabel_prefix_document = True
# We'll add the toctree entry ourselves to distinguish between C++ and Python
autoapi_add_toctree_entry = True
@@ -63,7 +66,8 @@ autoapi_add_toctree_entry = True
autoapi_type = 'python'
# autoapi works by reading source code instead of importing modules
-autoapi_dirs = ['../ifcopenshell', '../../ifcdiff', '../../ifcpatch/ifcpatch']
+autoapi_dirs = ['../ifcopenshell', '../../bcf/src', '../../bsdd', '../../ifccsv', '../../ifcdiff', '../../ifcpatch/ifcpatch', '../../ifctester/ifctester']
+# autoapi_dirs = ['../../bcf/src', '../../bsdd', '../../ifccsv', '../../ifcdiff', '../../ifcpatch/ifcpatch', '../../ifctester/ifctester']
# These are auto-generated based on the IFC schema, so exclude them
autoapi_ignore = ['*ifcopenshell/express/rules*']
diff --git a/src/ifcopenshell-python/docs/ifccobie.rst b/src/ifcopenshell-python/docs/ifccobie.rst
deleted file mode 100644
index 35ba047267..0000000000
--- a/src/ifcopenshell-python/docs/ifccobie.rst
+++ /dev/null
@@ -1,16 +0,0 @@
-IfcCOBie
-========
-
-This documentation is free software! You are free to contribute and help write
-this document.
-
-.. toctree::
- :maxdepth: 1
- :caption: Contents:
-
-Indices and tables
-------------------
-
-* :ref:`genindex`
-* :ref:`modindex`
-* :ref:`search`
diff --git a/src/ifcopenshell-python/docs/ifccsv.rst b/src/ifcopenshell-python/docs/ifccsv.rst
index 0e89bf9622..154a0fcae5 100644
--- a/src/ifcopenshell-python/docs/ifccsv.rst
+++ b/src/ifcopenshell-python/docs/ifccsv.rst
@@ -66,7 +66,8 @@ utility:
::
$ python -m ifccsv -h
- usage: ifccsv.py [-h] -i IFC [-s SPREADSHEET] [-f FORMAT] [-d DELIMITER] [-n NULL] [-q QUERY] [-a ARGUMENTS [ARGUMENTS ...]] [--export] [--import]
+ usage: ifccsv.py [-h] -i IFC [-s SPREADSHEET] [-f FORMAT] [-d DELIMITER] [-n NULL] [-e EMPTY] [--bool_true BOOL_TRUE] [--bool_false BOOL_FALSE] [-q QUERY] [-a ATTRIBUTES [ATTRIBUTES ...]] [--headers HEADERS [HEADERS ...]]
+ [--sort SORT [SORT ...]] [--order ORDER [ORDER ...]] [--export] [--import]
Exports IFC data to and from CSV
@@ -79,13 +80,25 @@ utility:
The format, chosen from csv, ods, or xlsx
-d DELIMITER, --delimiter DELIMITER
The delimiter in CSV. Defaults to a comma.
- -n NULL, --null NULL How to represent null values. Defaults to a hyphen.
+ -n NULL, --null NULL How to represent null values. Defaults to N/A.
+ -e EMPTY, --empty EMPTY
+ How to represent empty strings. Defaults to a hyphen.
+ --bool_true BOOL_TRUE
+ How to represent true values. Defaults to YES.
+ --bool_false BOOL_FALSE
+ How to represent false values. Defaults to NO.
-q QUERY, --query QUERY
Specify a IFC query selector, such as "IfcWall"
- -a ARGUMENTS [ARGUMENTS ...], --arguments ARGUMENTS [ARGUMENTS ...]
- Specify attributes that are part of the extract, using the IfcQuery syntax such as 'type', 'Name' or 'Pset_Foo.Bar'
- --export Export from IFC to CSV
- --import Import from CSV to IFC
+ -a ATTRIBUTES [ATTRIBUTES ...], --attributes ATTRIBUTES [ATTRIBUTES ...]
+ Specify attributes that are part of the extract, using the IfcQuery syntax such as 'class', 'Name' or 'Pset_Foo.Bar'
+ --headers HEADERS [HEADERS ...]
+ Specify human readable headers that correlate to each attribute.
+ --sort SORT [SORT ...]
+ Specify one or more attributes to sort by.
+ --order ORDER [ORDER ...]
+ Choose the sort order from ASC or DESC for each sorted attribute.
+ --export Export from IFC to the desired format.
+ --import Import from the autodetected format to IFC.
$ python -m ifccsv -i model.ifc -s out.csv -f csv -q .IfcProduct -a "Name" "Description" --export
$ cat out.csv
diff --git a/src/ifcopenshell-python/docs/ifcfm.rst b/src/ifcopenshell-python/docs/ifcfm.rst
new file mode 100644
index 0000000000..2decea229a
--- /dev/null
+++ b/src/ifcopenshell-python/docs/ifcfm.rst
@@ -0,0 +1,60 @@
+IfcFM
+=====
+
+**Facility managers** (FM) need to know a lot of information about the building
+in order to maintain the facility and its assets effectively. This includes
+information about spaces, services, and key equipment, such as who to call when
+things break, what the model number is, the warranty period, associated
+certificates, what valve or circuit breaker must be shut off prior to
+maintenance, and a punch list of periodic maintenance tasks.
+
+Traditionally, this information is collected in numerous operations and
+maintenance (O&M) manuals. IFC can collect this information incrementally
+throughout the design development, construction, and commissioning stages of a
+project. IFC's standardised and rich digital relationships can describe the
+information that facility managers need. The most popular requirements
+specification for IFC-based digital FM data is known as **COBie 2.4**.
+
+Despite already requesting and receiving IFC deliverables, many clients still
+request FM data in a spreadsheet format. Unfortunately, many of these
+spreadsheet deliverables are not produced from IFC databases. This defeats the
+purpose of BIM: asset data is no longer richly stored using international
+standards and the BIM model may no longer be trusted.
+
+IfcFM is a highly standards-compliant tool to convert FM data in IFC databases
+to spreadsheets and other machine readable formats, such as ODS, XLSX, CSV,
+Pandas, XML, and JSON. These formats may then be easily read, audited, or
+imported into technologies that cannot work with IFC natively.
+
+Supported data standards
+------------------------
+
+IfcFM assumes that you want to convert IFC data grouped into one or more
+categories of data (e.g. list of assets, list of spare parts, list of documents
+and certificates, etc). Each category of data includes a list or schedule of
+information, and may reference information in other categories. In a
+spreadsheet format, each category correlates to a worksheet, where each
+worksheet focuses on one type of data and has multiple columns (e.g. name of
+manufacturer, point of contact, etc). There are four data standards compatible
+with this data structure that IfcFM supports:
+
+- **COBie 2.4**: the most popular requirements specification for FM data
+ currently in use first published in 2007. It specifies requirements to
+ collect almost 20 categories of data, focusing on maintainable equipment and
+ finishes. It is published by the U.S. Army Corps of Engineers, National
+ Building Information Model (NBIMS-US) standard, version 2, chapter 2.4, and
+ in British Standard BS 1192-4:2014.
+- **COBie 3.0**: an update to **COBie 2.4** published by the National Building
+ Information Model (NBIMS-US) standard. Unlike **COBie 2.4**, it is not a
+ British Standard nor developed by the U.S. Army Corps of Engineers.
+- **AOH-BSEM**: a draft specification by buildingSMART based on the lessons
+ learned from the implementation of **COBie 2.4**, as part of a modular
+ approach to asset data exchange that supports not just buildings, but
+ infrastructure projects too. **AOH-BSEM** is set to supersede **COBIE 3.0**
+ with a focus primarily on equipment maintenance.
+- **Vanilla IFC**: Regardless of any "named" requirement such as above, IFC
+ itself contains a number of standardised property sets and object types
+ related to assets, warranties, manufacturers, and construction /
+ installation. As expected, the majority of these are already referenced in
+ named standards. "Vanilla" IFC offers a "specification agnostic" approach
+ towards facility management data collection.
diff --git a/src/ifcopenshell-python/docs/ifcmax.rst b/src/ifcopenshell-python/docs/ifcmax.rst
new file mode 100644
index 0000000000..a564e15b93
--- /dev/null
+++ b/src/ifcopenshell-python/docs/ifcmax.rst
@@ -0,0 +1,20 @@
+IfcMax
+======
+
+IfcMax is a 3ds Max importer plugin able to import the IFC file format.
+
+Community builds are available for 3ds Max by Josef Wienerroither (also known
+as ``FrogsInSpace`` or ``spacefrog``). Builds are available for IfcOpenShell
+v0.7.0 for 3ds Max version 2020-2024. Older builds are also available for
+IfcOpenShell v0.6.0 for 3ds Max version 2015-2022.
+
+It is recommended to use the latest version of IfcOpenShell and 3ds Max.
+
+- `Visit FrogsInSpace official website for IfcMax `__.
+- `Download IfcMax plugins `__.
+
+.. note::
+
+ This plugin is purely an importer and does not handle native IFC authoring
+ or exporting. For more information for native IFC authoring, we recommend
+ using the :doc:`BlenderBIM Add-on`.
diff --git a/src/ifcopenshell-python/docs/ifcopenshell-python.rst b/src/ifcopenshell-python/docs/ifcopenshell-python.rst
index 0861a2b374..9eebe129d4 100644
--- a/src/ifcopenshell-python/docs/ifcopenshell-python.rst
+++ b/src/ifcopenshell-python/docs/ifcopenshell-python.rst
@@ -2,7 +2,8 @@ IfcOpenShell-Python
===================
IfcOpenShell-Python provides Python bindings to the core IfcOpenShell C++
-system, as well as high level analysis and authoring functions.
+system, as well as high level analysis and authoring functions. All the
+capabilities of the C++ core are available in Python.
.. toctree::
:hidden:
diff --git a/src/ifcopenshell-python/docs/ifcopenshell-python/hello_world.rst b/src/ifcopenshell-python/docs/ifcopenshell-python/hello_world.rst
index e77eab5f70..61931b8794 100644
--- a/src/ifcopenshell-python/docs/ifcopenshell-python/hello_world.rst
+++ b/src/ifcopenshell-python/docs/ifcopenshell-python/hello_world.rst
@@ -1,64 +1,8 @@
Hello, world!
=============
-What's inside an IFC?
----------------------
-
-An IFC model can be considered as a collection of elements with relationships to
-other elements in a graph-like database. Together, these elements and their
-relationships describe the digital built environment.
-
-Each element has a type known as an **IFC Class**. These types define the
-attributes that the element may store. For example, the **IfcWall Class** is
-allowed to store a **Name** and **Description** attribute.
-
-There are many **IFC Classes** available, defined through an **Object Oriented**
-hierarchy. For example, because all **IfcElement** classes can have a
-**GlobalId** attribute, that means that because **IfcWall** is a subtype of
-**IfcElement**, it can also have a **GlobalId** attribute.
-
-This IFC database can be stored in many formats. The most common is the ``.ifc``
-format, which stores data in plain text. If you open a ``.ifc`` file in a text
-editor, you'll see something like this:
-
-::
-
- #1=IFCPROJECT('3Cbhu4euf1hfgM_SHZbeqM',$,'My Project',$,$,$,$,$,#4);
- #2=IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);
- #3=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.);
- #4=IFCUNITASSIGNMENT((#2,#3));
- #5=IFCCARTESIANPOINT((0.,0.,0.));
-
-In this example there are 5 elements in the graph. The element with the ID of
-**#1** has an **IFC Class** of **IfcProject**. This element has 9
-comma-separated attributes.
-
-::
-
- IFC Class Quoted string value Null value ID reference
- ↓ ↓ ↓ ↓
- #1=IFCPROJECT('3Cbhu4euf1hfgM_SHZbeqM',$,'My Project',$,$,$,$,$,#4);
- ↑ ↑
- Element ID Comma-separated list of attributes
-
-By selecting elements by their **IFC Class**, and reading their attributes, you
-can navigate from one element to another. The relationships between elements are
-called **IFC Concepts** and create meaning in our industry. For example, if a
-**IfcWall** element is related to an **IfcBuildingStorey** element in a
-particular way, it might mean that the wall is located in the ground floor of
-the building.
-
-The official IFC documentation describes hundreds of **IFC Classes**, ranging
-from walls, door, to tasks, cost items, parametric materials, and structural
-analysis constraints. There are also hundreds of **IFC Concepts**, which may
-describe how a wall is in a storey, a construction task might occur one after
-another, or how an surface bounds a space for energy analysis.
-
-IfcOpenShell can help you navigate these IFC elements, read their attributes,
-and traverse relationships. Your journey begins here.
-
-Core functionality crash course
--------------------------------
+If you're reading this, we assume you already know IFC and just want to quickly
+get started with IfcOpenShell.
This crash course guides you through basic code snippets that give you a general
idea of the low-level functionality that IfcOpenShell-python provides. You'll
@@ -91,7 +35,7 @@ Let's see what IFC schema we are using:
.. code-block:: python
- print(model.schema) # May return IFC2X3 or IFC4
+ print(model.schema) # May return IFC2X3, IFC4, or IFC4X3.
Let's get the first piece of data in our IFC file:
diff --git a/src/ifcopenshell-python/docs/ifcopenshell-python/installation.rst b/src/ifcopenshell-python/docs/ifcopenshell-python/installation.rst
index 5c0d1b045a..8dc7811f1d 100644
--- a/src/ifcopenshell-python/docs/ifcopenshell-python/installation.rst
+++ b/src/ifcopenshell-python/docs/ifcopenshell-python/installation.rst
@@ -11,9 +11,10 @@ packages**. If you aren't a programmer, go for the **BlenderBIM Add-on**.
4. **Docker** is recommended for developers using Docker.
5. **AWS Lambda** is recommended for developers using AWS Lambda functions.
6. **Google Colab** is recommended for developers using Google Colab.
-7. **Using the BlenderBIM Add-on** is recommended for non-developers wanting a graphical interface.
-8. **From source with precompiled binaries** is recommended for developers actively working with the Python code.
-9. **Compiling from source** is recommended for developers actively working with the C++ core.
+7. **Web Assembly** is recommended for developers experimenting with IfcOpenShell on the web.
+8. **Using the BlenderBIM Add-on** is recommended for non-developers wanting a graphical interface.
+9. **From source with precompiled binaries** is recommended for developers actively working with the Python code.
+10. **Compiling from source** is recommended for developers actively working with the C++ core.
Pre-built packages
------------------
@@ -197,6 +198,17 @@ local system.
`__
to launch a simple notebook.
+Web Assembly
+------------
+
+IfcOpenShell is available as technology preview to be run using WASM. This
+allows you to run IfcOpenShell in a browser using pyodide. This implementation
+is incredibly heavy and will incur a long load time, but once loaded, will give
+you full access to the entire IfcOpenShell API.
+
+`Click here `__ to learn how to
+use WASM.
+
Using the BlenderBIM Add-on
---------------------------
diff --git a/src/ifcopenshell-python/docs/ifcopenshell-python/selector_syntax.rst b/src/ifcopenshell-python/docs/ifcopenshell-python/selector_syntax.rst
index e3775e8d02..f01963a2da 100644
--- a/src/ifcopenshell-python/docs/ifcopenshell-python/selector_syntax.rst
+++ b/src/ifcopenshell-python/docs/ifcopenshell-python/selector_syntax.rst
@@ -184,6 +184,12 @@ Valid keys are:
"``material`` or ``mat``", "Gets the assigned material, which may be a material set."
"``item`` or ``i``", "If the previous key returns a material set, gets the relevant material set items"
"``materials`` or ``mats``", "Gets a list of IfcMaterials assigned directly or indirectly (such as via a material set) to the element"
+ "``x``", "Gets the X coordinate of the element's placement"
+ "``y``", "Gets the Y coordinate of the element's placement"
+ "``z``", "Gets the Z coordinate of the element's placement"
+ "``easting``", "Gets the map easting of the element's placement"
+ "``northing``", "Gets the map northing of the element's placement"
+ "``elevation``", "Gets the map elevation of the element's placement"
"``count``", "If the previous key returns multiple things, count that list. Otherwise, return 1."
"``{{number}}``", "If the previous key returns multiple things, fetch the ``{{number}}`` index (e.g. 0, 1, 2, 3, etc) item in that list."
diff --git a/src/ifcopenshell-python/docs/ifcopenshell.rst b/src/ifcopenshell-python/docs/ifcopenshell.rst
index 38c930ee91..e5bdd50069 100644
--- a/src/ifcopenshell-python/docs/ifcopenshell.rst
+++ b/src/ifcopenshell-python/docs/ifcopenshell.rst
@@ -1,12 +1,19 @@
IfcOpenShell
============
-IfcOpenShell is an open source (LGPL-3.0-or-later) software library for working
-with the Industry Foundation Classes (IFC) file format. Extensive geometric
-support is implemented for the IFC releases IFC2x3 TC1 and IFC4 Add2 TC1.
-Support for parsing is provided for IFC4x1, IFC4x2, and the IFC4x3 release
-candidates. Extending with support for arbitrary IFC schemas is possible at
-compile-time when using C++ and at run-time when using Python.
+IfcOpenShell is an open source (LGPL-3.0-or-later) C++ software library for
+working with the Industry Foundation Classes (IFC) file format.
+
+Extensive geometric support is implemented for the IFC releases IFC2x3 TC1 and
+IFC4 Add2 TC1. Support for parsing is provided for IFC4x1, IFC4x2, and the
+IFC4x3 release candidates. Extending with support for arbitrary IFC schemas is
+possible at compile-time when using C++ and at run-time when using Python.
+
+.. seealso::
+
+ It is not necessary to be a C++ developer to use IfcOpenShell. Please see
+ other sections such as :doc:`ifcopenshell-python`, :doc:`ifcconvert`, and
+ more.
.. toctree::
:hidden:
@@ -17,10 +24,3 @@ compile-time when using C++ and at run-time when using Python.
ifcopenshell/geometry_iterator
ifcopenshell/geometry_settings
ifcopenshell/boolean_process
-
-Indices and tables
-------------------
-
-* :ref:`genindex`
-* :ref:`modindex`
-* :ref:`search`
diff --git a/src/ifcopenshell-python/docs/ifcopenshell/images/intro.png b/src/ifcopenshell-python/docs/ifcopenshell/images/intro.png
new file mode 100644
index 0000000000..04acfcaee4
Binary files /dev/null and b/src/ifcopenshell-python/docs/ifcopenshell/images/intro.png differ
diff --git a/src/ifcopenshell-python/docs/index.rst b/src/ifcopenshell-python/docs/index.rst
index f5bd6086d0..eb9f2d700f 100644
--- a/src/ifcopenshell-python/docs/index.rst
+++ b/src/ifcopenshell-python/docs/index.rst
@@ -1,17 +1,14 @@
-Let's learn IfcOpenShell
-========================
+Let's learn IfcOpenShell!
+=========================
-IfcOpenShell is a suite of developer libraries and utilities to manipulate OpenBIM data.
-
-.. note::
-
- This documentation is incomplete. Would you like to help write more? `Get in touch! `__
+.. image:: ifcopenshell/images/intro.png
.. toctree::
:hidden:
:maxdepth: 1
:caption: Main:
+ introduction
ifcopenshell
ifcopenshell-python
ifcconvert
@@ -22,16 +19,19 @@ IfcOpenShell is a suite of developer libraries and utilities to manipulate OpenB
:maxdepth: 1
:caption: Utilities:
+ bcf
bimserver-plugin
bimtester
+ bsdd
ifc2ca
ifc4d
ifc5d
ifccityjson
ifcclash
- ifccobie
ifccsv
ifcdiff
+ ifcfm
+ ifcmax
ifcpatch
ifcsverchok
ifctester
@@ -44,11 +44,4 @@ IfcOpenShell is a suite of developer libraries and utilities to manipulate OpenB
C++ API Reference
Python API Reference
-
-
-Indices and tables
-==================
-
-* :ref:`genindex`
-* :ref:`modindex`
-* :ref:`search`
+ indices
diff --git a/src/ifcopenshell-python/docs/indices.rst b/src/ifcopenshell-python/docs/indices.rst
new file mode 100644
index 0000000000..d46b839f66
--- /dev/null
+++ b/src/ifcopenshell-python/docs/indices.rst
@@ -0,0 +1,6 @@
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
diff --git a/src/ifcopenshell-python/docs/introduction.rst b/src/ifcopenshell-python/docs/introduction.rst
new file mode 100644
index 0000000000..77c00a4a94
--- /dev/null
+++ b/src/ifcopenshell-python/docs/introduction.rst
@@ -0,0 +1,89 @@
+Introduction
+============
+
+**IfcOpenShell** is an open source software library for software developers and BIM powerusers working with Industry Foundation Classes (`IFC `_).
+
+In addition to a C++ and Python API, **IfcOpenShell** comes with an ecosystem of tools, notably including **IfcConvert** (an application to convert IFC models to other formats), the **BlenderBIM Add-on** (an add-on to Blender providing a graphical IFC authoring platform), and many other libraries, CLI apps, and more. Support is also provided for auxiliary standards such as BCF, bSDD, and IDS.
+
+Things you can do
+-----------------
+
+**IfcOpenShell** is designed to be a complete BIM authoring platform. Its
+capabilities have a similar scope to other BIM libraries, modeling platforms,
+costing programs, scheduling software, CAD packages, and simulation software.
+It is too numerous to list in full, but an example of what is possible include:
+
+- Viewing models, including spaces, properties, and relationships
+- Edit and extract attributes and properties
+- Moving objects, and changing their geometry
+- Create new objects using library elements
+- Manage classification systems, document and library references
+- Generating 2D drawings, schedules, and creating sheets
+- Investigating and editing structural analysis models
+- Connecting and managing distribution systems and ports
+- Creating construction schedules, critical path analysis, and generating sequence animations
+- Creating cost schedules, using formulas, and deriving quantities from model elements
+- Clash detection and managing issues for model coordination
+
+... and much, much more.
+
+What makes IfcOpenShell special?
+--------------------------------
+
+IfcOpenShell has a huge amount of unique features and capabilities not found in any other technology.
+
+- IfcOpenShell is the oldest and most mature open source IFC library available. It's developed since 2011 by a community of hundreds of developers and trusted to deliver many AEC technologies that power our industry. IfcOpenShell is also taught in numerous universities and cited in hundreds of academic publications.
+- Lots of platforms and package management options are available: Windows, Mac, Mac ARM (M1, M2), Linux, Web Assembly (WASM), Docker, AWS Lambda, Google Colab, and more.
+- Develop in C++, Python, or JavaScript via Pyodide.
+- All tools can be used either as a developer library, through a command line interface, or using a rich graphical interface. Whether you're deploy headless server tools for your own pipeline, writing your own apps, or an end-user, there's something for you.
+- Supports IFC2X3, IFC4, and IFC4.3. Custom schemas (such as experimental or draft schemas) may be loaded at run-time instead of having to recompile.
+- Built-in IFC validation is possible from basic syntax validation to more detailed "Where Rule" checks. This is the same validation that powers the official buildingSMART validation engine.
+- Read and write IFC-SPF, IFCJSON, IFCXML, IFCHDF5, MySQL, and SQLite.
+- High level API for hundreds of tasks. Perform complex authoring like copying objects, cost calculation, or 4D simulation with one line of code. Imagine a complete native IFC authoring and editing platform where every function is available to you as a library.
+- Convert parametric geometry into explicit geometry for any CAD system from booleans to complex sweeps. Geometry has been battle-tested over many years to accommodate complex geometric edge cases with an extensive test suite.
+- Geometry may be converted into voxels and analysed through voxels to resolve complex non-manifold geometry and precision issues. This analysis may be used from things like head height calculations, formwork analysis, to egress distances.
+- Generate and annotate 2D drawings from 3D geometry with ease. Preserve drawing semantics and link model data to and from drawing symbols. Drawings may be richly annotated with text, line styles, hatches, symbols, and more and are used to deliver commercial drawings for projects.
+- Clash detection, model comparison, and conversion to over 10 other formats (DAE, GLB, OBJ, SVG, and more). Integrate with technologies like IDS, BCF, and bSDD, and more.
+- Extensive documentation, user guides, academic courses, and a vibrant user community to help you begin your journey.
+
+IfcOpenShell utilities
+----------------------
+
+IfcOpenShell is a modular ecosystem of tools that work together, where each tool focuses on a particular task.
+
+.. csv-table::
+ :header: "Name", "Description"
+
+ "**IfcOpenShell**", "The core library for C++ developers. The library includes the ability to parse schemas, tessellate and process implicit geometry."
+ "**IfcOpenShell-Python**", "Python bindings to the core IfcOpenShell C++ system, as well as high level analysis and authoring functions."
+ "**IfcConvert**", "A command-line application for converting IFC geometry into file formats such as OBJ, DAE, GLB, STP, IGS, XML, SVG, H5, and IFC itself."
+ "**BlenderBIM Add-on**", "A graphical add-on that lets you analyse, author, and modify IFC with Blender."
+ "**BCF**", "BIM Collaboration Format (BCF) is a standard to manage and exchange coordination topics between disciplines collaborating on a project by changing XML files or querying an API."
+ "**BIMServer-Plugin**", "A plugin to the open source BIMServer CDE to allow you to use IfcOpenShell to parse, view, and audit models."
+ "**BIMTester**", "A utility that allows you to write Gherkin-based tests for models."
+ "**bSDD**", "A Python library to query the buildingSMART Data Dictionary API to search for standardised classifications and properties."
+ "**Ifc2CA**", "Converts IFC models to FEM structural analytical models to be used in Code_Aster."
+ "**Ifc4D**", "A series of utilities for converting to and from various 4D software like MS Project, PowerProject, and Oracle P6."
+ "**Ifc5D**", "A collection of utilities of manipulating cost-related data to and from formats, reports, and optimisation engines."
+ "**IfcCityJSON**", "A converter for CityJSON files and IFC. It currently only supports one-way conversion from CityJSON to IFC."
+ "**IfcClash**", "A CLI utility and library that lets you perform clash detection on one or more IFC models. Clashes are defined in terms of clash sets with filters using the IFC query syntax."
+ "**IfcCSV**", "View and edit IFC data using spreadsheets or tabular datasets, such as CSV, ODS, XLSX, Pandas DataFrames, and regular Python lists."
+ "**IfcDiff**", "A CLI utility and library that lets you compare the changes between two IFC models."
+ "**IfcFM**", "A highly standards-compliant tool (e.g. COBie 2.4, COBie 3.0, AOH-BSEM) to convert FM data in IFC databases to spreadsheets and other machine readable formats, such as ODS, XLSX, CSV, Pandas, XML, and JSON."
+ "**IfcMax**", "A 3ds Max importer plugin able to import the IFC file format."
+ "**IfcPatch**", "A CLI utility and library that lets you run and distribute predetermined modifications on an IFC file, known as a patch recipe. Useful in deploying a data pipeline or batch-fixing external models."
+ "**IfcSverchok**", "A node based visual programming add-on for Blender to interact with IFC and Sverchok."
+ "**IfcTester**", "Author and read Information Delivery Specification (IDS) files. You can validate IFC models against IDS and generate reports in multiple formats. It works from the command line, as a web app, or as a library."
+ "**VoxelisationToolkit**", "Converts .ifc geometry into voxels, and lets you perform voxel based geometric analysis."
+
+.. note::
+
+ **IfcOpenShell** and all of its libraries are licensed under LGPL-3.0-or-later. Two exceptions to this are the **BlenderBIM Add-on** and **IfcSverchok**, which are both licensed under GPL-3.0-or-later.
+
+.. toctree::
+ :hidden:
+ :maxdepth: 1
+ :caption: Contents:
+
+ introduction/introduction_to_bim
+ introduction/introduction_to_ifc
diff --git a/src/ifcopenshell-python/docs/introduction/images/ifc-concepts.png b/src/ifcopenshell-python/docs/introduction/images/ifc-concepts.png
new file mode 100644
index 0000000000..4543276782
Binary files /dev/null and b/src/ifcopenshell-python/docs/introduction/images/ifc-concepts.png differ
diff --git a/src/ifcopenshell-python/docs/introduction/images/ifc-concepts.svg b/src/ifcopenshell-python/docs/introduction/images/ifc-concepts.svg
new file mode 100644
index 0000000000..b9ca548c0c
--- /dev/null
+++ b/src/ifcopenshell-python/docs/introduction/images/ifc-concepts.svg
@@ -0,0 +1,340 @@
+
+
+
+
diff --git a/src/ifcopenshell-python/docs/introduction/images/ifc-graph.png b/src/ifcopenshell-python/docs/introduction/images/ifc-graph.png
new file mode 100644
index 0000000000..06e7f88268
Binary files /dev/null and b/src/ifcopenshell-python/docs/introduction/images/ifc-graph.png differ
diff --git a/src/ifcopenshell-python/docs/introduction/images/ifc-graph.svg b/src/ifcopenshell-python/docs/introduction/images/ifc-graph.svg
new file mode 100644
index 0000000000..aea6ba8e0d
--- /dev/null
+++ b/src/ifcopenshell-python/docs/introduction/images/ifc-graph.svg
@@ -0,0 +1,316 @@
+
+
+
+
diff --git a/src/ifcopenshell-python/docs/introduction/images/ifc-tree.png b/src/ifcopenshell-python/docs/introduction/images/ifc-tree.png
new file mode 100644
index 0000000000..5b0a1ec32d
Binary files /dev/null and b/src/ifcopenshell-python/docs/introduction/images/ifc-tree.png differ
diff --git a/src/ifcopenshell-python/docs/introduction/images/ifc-tree.svg b/src/ifcopenshell-python/docs/introduction/images/ifc-tree.svg
new file mode 100644
index 0000000000..f5247069df
--- /dev/null
+++ b/src/ifcopenshell-python/docs/introduction/images/ifc-tree.svg
@@ -0,0 +1,493 @@
+
+
+
+
diff --git a/src/ifcopenshell-python/docs/introduction/introduction_to_bim.rst b/src/ifcopenshell-python/docs/introduction/introduction_to_bim.rst
new file mode 100644
index 0000000000..087ade6b6f
--- /dev/null
+++ b/src/ifcopenshell-python/docs/introduction/introduction_to_bim.rst
@@ -0,0 +1,37 @@
+Introduction to BIM
+===================
+
+**Building Information Modeling**, or **BIM**, is a way of digitally describing
+our built environment to computers. Aspects of our built environment that can be
+described are:
+
+- **Products**, like walls, doors, and windows
+- **Processes**, like construction or maintenance tasks, and procedures
+- **Resources**, like labour, materials, and equipment
+- **Controls**, like permits, orders, costs, or calendar availability
+- **Actors**, like occupants, clients, architects, and liable parties
+- **Groups**, like systems, inventories, or zones
+
+These objects may have lots of data and relationships. Examples of data might be
+classification systems, physical materials, associated documents, simulation
+results, and construction types. The data may be relevant to multiple
+disciplines, such as architecture, engineering, and construction.
+
+.. note::
+
+ BIM data is very different from a regular 3D model. In fact, geometry is
+ optional, and most data is non-geometric. This means that it is not simply a
+ 3D format that you can import or export from and expect meaningful results.
+
+**Industry Foundation Classes**, or **IFC**, is an international standard for
+**BIM**. **IFC** is the most well-established open digital language for our
+built environment. Most software will be able to describe their **BIM** data
+using **IFC**. Most commonly, **IFC** models will be shared as a ``.ifc`` file.
+
+For example, **IFC** will define a wall as an object that can have a name,
+construction type, and quantities. **IFC** will also describe that a wall that
+be associated with a location, like a building storey, or have an associated
+cost item in a schedule.
+
+When you use **IfcOpenShell**, you will be able to view and create **BIM**
+objects and relationships using the **IFC** standard.
diff --git a/src/ifcopenshell-python/docs/introduction/introduction_to_ifc.rst b/src/ifcopenshell-python/docs/introduction/introduction_to_ifc.rst
new file mode 100644
index 0000000000..ca281e517a
--- /dev/null
+++ b/src/ifcopenshell-python/docs/introduction/introduction_to_ifc.rst
@@ -0,0 +1,220 @@
+Introduction to IFC
+===================
+
+An IFC model is a collection of elements (e.g. doors, windows, construction
+tasks, materials, etc) with relationships to other elements in a graph-like
+database. Together, these elements and their relationships describe the digital
+built environment.
+
+.. image:: images/ifc-graph.png
+
+Each element has a type known as an **IFC Class**. These classes define the
+attributes that the element may store. For example, the **IfcWall Class** is
+allowed to store a **Name** and **Description** attribute.
+
+This IFC database can be stored in many formats. The most common is the ``.ifc``
+format, which stores data in plain text. If you open a ``.ifc`` file in a text
+editor, you'll see something like this:
+
+::
+
+ #1=IFCPROJECT('3Cbhu4euf1hfgM_SHZbeqM',$,'My Project',$,$,$,$,$,#4);
+ #2=IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);
+ #3=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.);
+ #4=IFCUNITASSIGNMENT((#2,#3));
+ #5=IFCCARTESIANPOINT((0.,0.,0.));
+
+In this example there are 5 elements in the graph. The element with the ID of
+**#1** has an **IFC Class** of **IfcProject**. This element has 9
+comma-separated attributes. IFC defines how many attributes each **IFC Class**
+is allowed to have, attribute names, the order of attributes, data type,
+optional or mandatory status (i.e. cardinality), and more.
+
+::
+
+ IFC Class Quoted string value Null value ID reference
+ ↓ ↓ ↓ ↓
+ #1=IFCPROJECT('3Cbhu4euf1hfgM_SHZbeqM',$,'My Project',$,$,$,$,$,#4);
+ ↑ ↑
+ Element ID Comma-separated list of attributes
+
+By selecting elements by their **IFC Class**, and reading their attributes, you
+can navigate from one element to another. The relationships between elements are
+called **IFC Concepts** and create meaning in our industry. For example, if a
+**IfcWall** element has an attribute that references an **IfcBuildingStorey**
+element in a particular way, it will mean that the wall is located in the
+ground floor of the building.
+
+.. image:: images/ifc-concepts.png
+
+The official IFC documentation describes hundreds of **IFC Classes**, ranging
+from walls, door, to tasks, cost items, parametric materials, and structural
+analysis constraints. There are also hundreds of **IFC Concepts**, which may
+describe how a wall is in a storey, a construction task might occur one after
+another, or how an surface bounds a space for energy analysis.
+
+It takes time to learn the many **IFC Classes** and **IFC Concepts** available.
+Once you do, you will be able to richly describe our built environment
+digitally. IfcOpenShell can help you navigate these IFC elements, read their
+attributes, and explore relationships. Your journey begins here.
+
+.. seealso::
+
+ If you are already familiar with IFC and just want to learn how to use
+ IfcOpenShell, you can jump to the `Core functionality crash course`_.
+
+Begin learning IFC
+------------------
+
+IFC has three versions published by ISO: **IFC2X3** from 2007, **IFC4** from
+2017, and **IFC4X3** in draft form. Each version improves on the previous
+version, and will have different **IFC Classes** with different attributes and
+different **IFC Concepts**.
+
+You can access the official documentation here:
+
+- `Official IFC2X3 documentation homepage `__
+- `Official IFC4 documentation homepage `__
+- `Official IFC4X3 documentation homepage `__
+- `List of all IFC2X3 classes `__
+- `List of all IFC4 classes `__
+- `List of all IFC4X3 classes `__
+
+.. tip::
+
+ It is recommended to use IFC4. However, the IFC4X3 documentation is a lot
+ more friendly to newcomers.
+
+The official ISO documentation is written for a technical audience and may be
+overwhelming. This guide will take you slowly through the core concepts, and
+leave you with the knowledge you need to discover more.
+
+Before digging into theory, let's explore an existing IFC model. You can
+download this sample IFC for this guide.
+
+.. container:: blockbutton
+
+ `Download sample IFC `__
+
+If you open up the model with a text editor, you will see text similar to this:
+
+::
+
+ ISO-10303-21;
+ HEADER;FILE_DESCRIPTION(('ViewDefinition [, QuantityTakeOffAddOnView, SpaceBoundary2ndLevelAddOnView]'),'2;1');
+ FILE_NAME('AC20-FZK-Haus.ifc','2016-12-21T17:54:06',('Architect'),(''),'','','');
+ FILE_SCHEMA(('IFC4'));
+ ENDSEC;
+
+ DATA;
+ #3= IFCORGANIZATION($,'Nicht definiert',$,$,$);
+ #12= IFCOWNERHISTORY(#7,#11,$,.ADDED.,$,$,$,1482339244);
+ #13= IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
+ #14= IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.);
+
+ ...
+
+ #62= IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.00000000000E-5,#59,#60);
+ #66= IFCPROJECT('0lY6P5Ur90TAQnnnI6wtnb',#12,'Projekt-FZK-Haus','Projekt FZK-House create by KHH Forschuungszentrum Karlsruhe',$,$,$,(#62,#374),#49);
+ #77= IFCPROPERTYSINGLEVALUE('GS_TimeStamp',$,IFCTIMESTAMP(9685146),$);
+ #85= IFCPROPERTYSET('1mnk_H9cG6eU2r9ped0WRu',#12,'GSPset_TimeStamp',$,(#77));
+
+ ...
+
+ #15033= IFCSHAPEREPRESENTATION(#15026,'Axis','Curve2D',(#15031));
+ #15037= IFCPRODUCTDEFINITIONSHAPE($,$,(#15016,#15024,#15033));
+ #15042= IFCWALLSTANDARDCASE('2XPyKWY018sA1ygZKgQPtU',#12,'Wand-Int-ERDG-4',$,$,#14983,#15037,'BC6F0F70-6195-495E-A2-FC-239713029DB1',$);
+ #15046= IFCMATERIAL('Leichtbeton 102890359',$,$);
+
+ ...
+
+ #15231= IFCRELDEFINESBYPROPERTIES('3Q0nMR5elnJFWzAhgkZqe1',#12,$,$,(#15042),#15229);
+ #15234= IFCWALLTYPE('2AEMyYvIjlsz7LRzqYHy64',#12,'Leichtbeton 102890359 240',$,$,$,(#15244,#15248,#15250,#17288,#17290,#17292,#18637,#18639,#18641,#19015,#19017,#19019,#20770,#20772,#20774),'8A396F22-E52B-6FDB-D1D5-6FDD2247C184',$,.NOTDEFINED.);
+ #15237= IFCDIRECTION((1.,0.,0.));
+ #15239= IFCDIRECTION((0.,0.,1.));
+
+ ... etc
+
+The first thing you should notice is the line that defines that this is an
+**IFC4** version. This determines what **IFC Classes** and **IFC Concepts** are
+available.
+
+::
+
+ FILE_SCHEMA(('IFC4'));
+
+You'll notice certain **IFC Class** keywords jump out at you: things like
+**IFCSIUNIT** which defines the length unit of metres, or **IFCPROJECT** which
+defines the project, or **IFCPROPERTYSINGLEVALUE** which defines a property of
+something, or **IFCWALLSTANDARDCASE** which defines a wall, or **IFCMATERIAL**
+which defines a material, and so on.
+
+Let's see how to fetch this data with code. Let's start with loading the model.
+Import the IfcOpenShell module, then use the ``open`` function to load the
+model into a variable called ``model``. The first piece of information we want
+to check is what IFC schema version we are using. We assume the model you are
+learning with is IFC4. We'll then fetch all entities that use the **IfcSlab**
+class.
+
+.. code-block:: python
+
+ import ifcopenshell
+ model = ifcopenshell.open('/path/to/your/model.ifc')
+ print(model.schema) # May return IFC2X3, IFC4, or IFC4X3.
+ print(model.by_type("IfcSlab")) # Will return a list of IFCSLAB entities, like below:
+ # [
+ # #34509=IfcSlab('1pPHnf7cXCpPsNEnQf8_6B',#12,'Bodenplatte',$,$,#34464,#34505,'E4D9CD4B-CA43-4735-94-BD-1FD4376BD455',.BASESLAB.),
+ # #59290=IfcSlab('2RGlQk4xH47RHK93zcTzUL',#12,'Slab-033',$,$,#59253,#59286,'DA0A17AC-B773-47AC-99-C5-D390C73AD5CC',.FLOOR.),
+ # #59553=IfcSlab('07Enbsqm9C7AQC9iyBwfSD',#12,'Dach-1',$,$,#59508,#59549,'E142B455-80E4-4B96-83-EC-E1589CA998DB',.ROOF.),
+ # #59753=IfcSlab('2IxUUNUVPB6Ob$eicCfP2N',#12,'Dach-2',$,$,#59716,#59749,'BD6D9414-37DF-40A8-88-40-301A32A9A5B5',.ROOF.)
+ # ]
+
+.. tip::
+
+ Try changing ``model.by_type("IfcSlab")`` to fetch different types of
+ entities based on their **IFC Class**.
+
+An overview of all IFC classes
+------------------------------
+
+There are hundreds of **IFC Classes**. You don't need to know them all, but
+we'll help describe the general breakdown so you know where to find the
+appropriate class for what you're after.
+
+**IFC Classes** are defined using an **Object Oriented** tree hierarchy. Child
+**IFC Classes** inherit the attributes defined by the parent **IFC Class**.
+This means that **IFC Classes** with common attributes are grouped together in
+the tree.
+
+For example, because all **IfcObject** classes can have a **GlobalId**
+attribute, that means that because **IfcWall** is a subtype of **IfcObject**,
+it can also have a **GlobalId** attribute.
+
+.. image:: images/ifc-tree.png
+
+Important IFC concepts
+----------------------
+
+There are hundreds of **IFC Concepts** that allow you to describe relationships
+between **IFC Classes**. In this guide, we'll focus on the five most common
+**IFC Concepts** to get you started.
+
+Concept 1: the project context
+------------------------------
+
+Concept 2: spatial decomposition
+--------------------------------
+
+Concept 3: object typing
+------------------------
+
+Concept 4: attributes and property sets
+---------------------------------------
+
+Concept 5: material assignment
+------------------------------
+
+Self-learning IFC: how to learn more
+------------------------------------
+
+
diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/calculate_cost_item_resource_value.py b/src/ifcopenshell-python/ifcopenshell/api/cost/calculate_cost_item_resource_value.py
index 2787b0048c..e947654b3a 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/cost/calculate_cost_item_resource_value.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/cost/calculate_cost_item_resource_value.py
@@ -18,6 +18,7 @@
import ifcopenshell.api
import ifcopenshell.util.date
+import ifcopenshell.util.resource
class Usecase:
@@ -105,8 +106,10 @@ class Usecase:
total_cost = 0
for resource in resources:
- cost = self.get_cost(resource)
- quantity = self.get_quantity(resource)
+ cost = ifcopenshell.util.resource.get_cost(resource)
+ quantity = ifcopenshell.util.resource.get_quantity(resource)
+ if not cost:
+ cost = ifcopenshell.util.resource.get_parent_cost(resource) # Concept to standardise - Not defined in schema, but this makes manual scheduling of resources 10x faster and less duplicate data.
if not cost or not quantity:
continue
total_cost += cost * quantity
@@ -114,20 +117,3 @@ class Usecase:
if total_cost:
cost_value = ifcopenshell.api.run("cost.add_cost_value", self.file, parent=self.settings["cost_item"])
cost_value.AppliedValue = self.file.createIfcMonetaryMeasure(total_cost)
-
- def get_cost(self, resource):
- total = 0
- for cost_value in resource.BaseCosts or []:
- total += cost_value.AppliedValue.wrappedValue if cost_value.AppliedValue else 0
- return total
-
- def get_quantity(self, resource):
- total = 0
- if resource.BaseQuantity:
- return resource.BaseQuantity[3]
- if resource.Usage and resource.Usage.ScheduleWork:
- # For now we assume either hourly or daily depending on how duration is stored
- duration = ifcopenshell.util.date.ifc2datetime(resource.Usage.ScheduleWork)
- if duration.days:
- return duration.days
- return duration.seconds / 60 / 60
diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_schedule.py b/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_schedule.py
index 33414bc486..b1ebfbd738 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_schedule.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_schedule.py
@@ -16,6 +16,8 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see .
+import ifcopenshell.api
+
class Usecase:
def __init__(self, file, cost_schedule=None):
@@ -42,4 +44,13 @@ class Usecase:
def execute(self):
# TODO: do a deep purge
+ for inverse in self.file.get_inverse(self.settings["cost_schedule"]):
+ if inverse.is_a("IfcRelAssignsToControl"):
+ [
+ ifcopenshell.api.run(
+ "cost.remove_cost_item", self.file, cost_item=related_object
+ )
+ for related_object in inverse.RelatedObjects
+ if related_object.is_a("IfcCostItem")
+ ]
self.file.remove(self.settings["cost_schedule"])
diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_door_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_door_representation.py
index df01f767db..4da8d5b1be 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_door_representation.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_door_representation.py
@@ -66,9 +66,7 @@ def create_ifc_door_lining(
door_lining = builder.extrude(
door_lining,
size.y,
- position_x_axis=V(1, 0, 0),
- position_z_axis=V(0, -1, 0),
- extrusion_vector=V(0, 0, -1),
+ **builder.extrude_kwargs("Y")
)
builder.translate(door_lining, position)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_railing_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_railing_representation.py
index 33ea7b7b20..b597633678 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_railing_representation.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_railing_representation.py
@@ -111,7 +111,7 @@ class Usecase:
support_disk_circle = builder.circle(radius=support_disk_radius)
angle = V(0, 1).angle_signed(ortho_dir.xy)
- y_extrusion_kwargs = builder.rotate_extrusion_kwargs_by_z(builder.extrude_by_y_kwargs(), angle)
+ y_extrusion_kwargs = builder.rotate_extrusion_kwargs_by_z(builder.extrude_kwargs("Y"), angle)
support_disk = builder.extrude(
support_disk_circle, support_disk_depth, position=support_points[-1], **y_extrusion_kwargs
)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_slab_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_slab_representation.py
index 251c81ad7a..b70b5267d7 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_slab_representation.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_slab_representation.py
@@ -26,7 +26,7 @@ class Usecase:
self.settings = {
"context": None, # IfcGeometricRepresentationContext
"depth": 0.2,
- "x_angle": 0, # Radians
+ "x_angle": 0, # Radians
# Planes are defined as a matrix. The XY plane is the clipping boundary and +Z is removed.
"clippings": [], # A list of planes that define clipping half space solids
}
@@ -55,9 +55,19 @@ class Usecase:
)
else:
extrusion_direction = self.file.createIfcDirection((0.0, 0.0, 1.0))
+
+ position = None
+ # default position for IFC2X3 where .Position is not optional
+ if self.file.schema == "IFC2X3":
+ position = self.file.createIfcAxis2Placement3D(
+ self.file.createIfcCartesianPoint((0.0, 0.0, 0.0)),
+ self.file.createIfcDirection((0.0, 0.0, 1.0)),
+ self.file.createIfcDirection((1.0, 0.0, 0.0)),
+ )
+
extrusion = self.file.createIfcExtrudedAreaSolid(
self.file.createIfcArbitraryClosedProfileDef("AREA", None, curve),
- None,
+ position,
extrusion_direction,
self.convert_si_to_unit(self.settings["depth"]) * 1 / cos(self.settings["x_angle"]),
)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_window_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_window_representation.py
index 47b2f3745b..94b97636aa 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_window_representation.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_window_representation.py
@@ -59,10 +59,8 @@ def create_ifc_window_frame_simple(
return builder.extrude(
profile,
size.y,
- position_x_axis=V(1, 0, 0),
- position_z_axis=V(0, -1, 0),
- extrusion_vector=V(0, 0, -1),
position=position,
+ **builder.extrude_kwargs("Y")
)
# if all lining sides are present then we can just use two rectangles
@@ -212,10 +210,8 @@ def create_ifc_window(
glass = builder.extrude(
glass_rect,
glass_thickness,
- position_x_axis=V(1, 0, 0),
- position_z_axis=V(0, -1, 0),
- extrusion_vector=V(0, 0, -1),
position=glass_position,
+ **builder.extrude_kwargs("Y")
)
output_items = [lining_items, frame_extruded_items, [glass]]
diff --git a/src/ifcopenshell-python/ifcopenshell/api/pset/remove_pset.py b/src/ifcopenshell-python/ifcopenshell/api/pset/remove_pset.py
index 322fb46356..44aa512ab3 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/pset/remove_pset.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/pset/remove_pset.py
@@ -65,8 +65,13 @@ class Usecase:
elif self.settings["pset"].is_a() in ("IfcMaterialProperties", "IfcProfileProperties"):
properties = self.settings["pset"].Properties or []
for prop in properties:
- if self.file.get_total_inverses(prop) == 1:
- self.file.remove(prop)
+ if self.file.get_total_inverses(prop) != 1:
+ continue
+ if prop.is_a("IfcPropertyEnumeratedValue"):
+ enumeration = prop.EnumerationReference
+ if self.file.get_total_inverses(enumeration) == 1:
+ self.file.remove(enumeration)
+ self.file.remove(prop)
self.file.remove(self.settings["pset"])
for element in to_purge:
self.file.remove(element)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/pset_template/add_pset_template.py b/src/ifcopenshell-python/ifcopenshell/api/pset_template/add_pset_template.py
index ae37efa128..4611dfb48e 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/pset_template/add_pset_template.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/pset_template/add_pset_template.py
@@ -21,7 +21,11 @@ import ifcopenshell
class Usecase:
def __init__(
- self, file, name="New_Pset", template_type="PSET_TYPEDRIVENOVERRIDE", applicable_entity="IfcTypeObject"
+ self,
+ file,
+ name="New_Pset",
+ template_type="PSET_TYPEDRIVENOVERRIDE",
+ applicable_entity="IfcObject,IfcTypeObject",
):
"""Adds a new property set template
diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_sequence.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_sequence.py
index a3b4dd2a16..078f5b7f5c 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_sequence.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_sequence.py
@@ -120,7 +120,7 @@ class Usecase:
for rel in self.settings["related_process"].IsSuccessorFrom or []:
if rel.RelatingProcess == self.settings["relating_process"]:
return rel
- return self.file.create_entity(
+ rel = self.file.create_entity(
"IfcRelSequence",
**{
"GlobalId": ifcopenshell.guid.new(),
@@ -132,3 +132,7 @@ class Usecase:
"SequenceType": self.settings["sequence_type"],
}
)
+ ifcopenshell.api.run(
+ "sequence.cascade_schedule", self.file, task=self.settings["relating_process"]
+ )
+ return rel
diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/cascade_schedule.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/cascade_schedule.py
index d8fc4f4384..b541a81670 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/sequence/cascade_schedule.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/cascade_schedule.py
@@ -135,7 +135,9 @@ class Usecase:
finishes = []
starts = []
- for rel in task.IsSuccessorFrom:
+ for rel in ifcopenshell.util.sequence.get_sequence_assignment(
+ task, "predecessor"
+ ):
predecessor = rel.RelatingProcess
predecessor_duration = (
ifcopenshell.util.date.ifc2datetime(
@@ -314,6 +316,12 @@ class Usecase:
for rel in task.IsPredecessorTo:
self.cascade_task(rel.RelatedProcess, task_sequence=task_sequence + [task])
+ for rel in task.IsNestedBy:
+ [
+ self.cascade_task(nested_task, task_sequence=task_sequence + [task])
+ for nested_task in rel.RelatedObjects or []
+ ]
+
def get_lag_time_days(self, lag_time):
return ifcopenshell.util.date.ifc2datetime(lag_time.LagValue.wrappedValue).days
diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/recalculate_schedule.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/recalculate_schedule.py
index c7e911e2d9..4039c53c57 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/sequence/recalculate_schedule.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/recalculate_schedule.py
@@ -148,7 +148,7 @@ class Usecase:
[
(
rel.RelatingProcess.id(),
- rel.RelatedProcess.id(),
+ task.id(),
{
"lag_time": 0
if not rel.TimeLag
@@ -158,11 +158,12 @@ class Usecase:
"type": self.sequence_type_map[rel.SequenceType],
},
)
- for rel in task.IsSuccessorFrom or []
+ for rel in ifcopenshell.util.sequence.get_sequence_assignment(task, sequence="predecessor")
]
)
- predecessor_types = [rel.SequenceType for rel in task.IsSuccessorFrom]
- successor_types = [rel.SequenceType for rel in task.IsPredecessorTo]
+
+ predecessor_types = [rel.SequenceType for rel in ifcopenshell.util.sequence.get_sequence_assignment(task, "predecessor")]
+ successor_types = [rel.SequenceType for rel in ifcopenshell.util.sequence.get_sequence_assignment(task, "successor")]
if not predecessor_types:
self.edges.append(("start", task.id(), {"lag_time": 0, "type": "FS"}))
@@ -170,6 +171,7 @@ class Usecase:
self.start_dates.append(
ifcopenshell.util.date.ifc2datetime(task.TaskTime.ScheduleStart)
)
+ self.g.nodes[task.id()]["early_start"] = ifcopenshell.util.date.ifc2datetime(task.TaskTime.ScheduleStart) # we assume this task is constrained to start on this date
if not successor_types:
self.edges.append((task.id(), "finish", {"lag_time": 0, "type": "FF"}))
@@ -223,6 +225,16 @@ class Usecase:
else:
finishes = []
starts = []
+ if data.get("early_start") is not None:
+ data["early_finish"] = ifcopenshell.util.sequence.get_start_or_finish_date(
+ data["early_start"],
+ datetime.timedelta(days=data["duration"]),
+ data["duration_type"],
+ data["calendar"],
+ date_type="FINISH",
+ )
+ return True # we're done! We assume this task is constrained and finish processing it
+
for predecessor in predecessors:
predecessor_data = self.g.nodes[predecessor]
edge = self.g[predecessor][node]
diff --git a/src/ifcopenshell-python/ifcopenshell/api/system/disconnect_port.py b/src/ifcopenshell-python/ifcopenshell/api/system/disconnect_port.py
index 1ff6609ce1..eddfae5f59 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/system/disconnect_port.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/system/disconnect_port.py
@@ -70,8 +70,8 @@ class Usecase:
}
def execute(self):
- rels = self.settings["port"].ConnectedTo or []
- rels += self.settings["port"].ConnectedFrom or []
+ rels = self.settings["port"].ConnectedTo or ()
+ rels += self.settings["port"].ConnectedFrom or ()
for rel in rels:
rel.RelatingPort.FlowDirection = None
diff --git a/src/ifcopenshell-python/ifcopenshell/util/date.py b/src/ifcopenshell-python/ifcopenshell/util/date.py
index 27e6df4af0..f7f7c102f1 100644
--- a/src/ifcopenshell-python/ifcopenshell/util/date.py
+++ b/src/ifcopenshell-python/ifcopenshell/util/date.py
@@ -35,8 +35,7 @@ def timedelta2duration(timedelta):
}
if components["seconds"]:
components["hours"], components["minutes"], components["seconds"] = [
- int(i)
- for i in str(datetime.timedelta(seconds=components["seconds"])).split(":")
+ int(i) for i in str(datetime.timedelta(seconds=components["seconds"])).split(":")
]
return isodate.Duration(**components)
@@ -122,9 +121,7 @@ def datetime2ifc(dt, ifc_type):
if isinstance(dt, datetime.datetime):
return dt.isoformat()
elif isinstance(dt, datetime.date):
- return datetime.datetime.combine(
- dt, datetime.datetime.min.time()
- ).isoformat()
+ return datetime.datetime.combine(dt, datetime.datetime.min.time()).isoformat()
elif ifc_type == "IfcDate":
if isinstance(dt, datetime.datetime):
return dt.date().isoformat()
@@ -180,9 +177,7 @@ def string_to_duration(duration_string):
match = findall(r"(\d+\.?\d*)s", duration_string)
if match:
seconds = float(match[0])
- return isodate.duration_isoformat(
- datetime.timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds)
- )
+ return isodate.duration_isoformat(datetime.timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds))
def parse_duration(value):
@@ -192,9 +187,11 @@ def parse_duration(value):
if "P" in value:
try:
return isodate.parse_duration(value)
+ except ModuleNotFoundError:
+ print("Duration parsing not supported: isodate module not found")
except:
- print("error parsing ISO string duration")
- return None
+ print("Error parsing ISO string duration")
+ return None
else:
try:
final_string = "P"
@@ -204,11 +201,7 @@ def parse_duration(value):
final_string += char
elif char == "D":
final_string += "D"
- if (
- "H" in value_upper
- or "S" in value_upper
- or "MIN" in value_upper
- ):
+ if "H" in value_upper or "S" in value_upper or "MIN" in value_upper:
final_string += "T"
elif char == "W":
final_string += "W"
@@ -218,9 +211,7 @@ def parse_duration(value):
final_string += "Y"
elif char == "H":
final_string = (
- final_string[:1] + "T" + final_string[1:]
- if "T" not in final_string
- else final_string
+ final_string[:1] + "T" + final_string[1:] if "T" not in final_string else final_string
)
final_string += "H"
elif char == "M":
@@ -229,9 +220,7 @@ def parse_duration(value):
final_string += "M"
elif char == "S":
final_string = (
- final_string[:1] + "T" + final_string[1:]
- if "T" not in final_string
- else final_string
+ final_string[:1] + "T" + final_string[1:] if "T" not in final_string else final_string
)
final_string += "S"
return isodate.parse_duration(final_string)
diff --git a/src/ifcopenshell-python/ifcopenshell/util/fm.py b/src/ifcopenshell-python/ifcopenshell/util/fm.py
index f1c41f65e9..b58d688c99 100644
--- a/src/ifcopenshell-python/ifcopenshell/util/fm.py
+++ b/src/ifcopenshell-python/ifcopenshell/util/fm.py
@@ -19,6 +19,7 @@
import ifcopenshell
import ifcopenshell.util.attribute
+# COBie actually uses an exclusion list, but this inclusion list is equivalent.
cobie_type_classes = [
"IfcDoorStyle",
"IfcBuildingElementProxyType",
diff --git a/src/ifcopenshell-python/ifcopenshell/util/geolocation.py b/src/ifcopenshell-python/ifcopenshell/util/geolocation.py
index ca98fb7a96..d4737ac016 100644
--- a/src/ifcopenshell-python/ifcopenshell/util/geolocation.py
+++ b/src/ifcopenshell-python/ifcopenshell/util/geolocation.py
@@ -75,6 +75,34 @@ def xyz2enh_ifc4x3(
return (eastings, northings, height)
+def auto_xyz2enh(ifc_file, x, y, z):
+ try:
+ conversion = ifc_file.by_type("IfcMapConversion")
+ except:
+ return (x, y, z)
+ if not conversion:
+ return (x, y, z)
+ conversion = conversion[0]
+ e = conversion.Eastings or 0
+ n = conversion.Northings or 0
+ h = conversion.OrthogonalHeight or 0
+ xaa = conversion.XAxisAbscissa or 0
+ xao = conversion.XAxisOrdinate or 0
+ scale = conversion.Scale or 0
+ map_unit = conversion.TargetCRS.MapUnit
+ if map_unit:
+ # Warning! This definition has changed in IFC4X3 such that map_unit no
+ # longer affects unit conversion, only the Scale attribute affects unit
+ # conversion. TODO: consolidate once IFC4X3 confirmed.
+ project_unit = ifcopenshell.util.unit.get_project_unit(ifc_file, "LENGTHUNIT")
+ map_prefix = getattr(map_unit, "Prefix", None)
+ project_prefix = getattr(project_unit, "Prefix", None)
+ e = ifcopenshell.util.unit.convert(e, map_prefix, map_unit.Name, project_prefix, project_unit.Name)
+ n = ifcopenshell.util.unit.convert(n, map_prefix, map_unit.Name, project_prefix, project_unit.Name)
+ h = ifcopenshell.util.unit.convert(h, map_prefix, map_unit.Name, project_prefix, project_unit.Name)
+ return xyz2enh(x, y, z, e, n, h, xaa, xao, scale)
+
+
def auto_z2e(ifc_file, z):
"""Convert a Z coordinate to an elevation using model georeferencing data
diff --git a/src/ifcopenshell-python/ifcopenshell/util/resource.py b/src/ifcopenshell-python/ifcopenshell/util/resource.py
index f1e17dcc42..f3ce2082e0 100644
--- a/src/ifcopenshell-python/ifcopenshell/util/resource.py
+++ b/src/ifcopenshell-python/ifcopenshell/util/resource.py
@@ -113,5 +113,32 @@ def get_resource_required_work(resource):
iso_string = f"P{required_work}D"
return iso_string
+
def get_nested_resources(resource):
return [object for rel in resource.IsNestedBy or [] for object in rel.RelatedObjects]
+
+
+def get_cost(resource):
+ total = 0
+ for cost_value in resource.BaseCosts or []:
+ total += cost_value.AppliedValue.wrappedValue if cost_value.AppliedValue else 0
+ return total
+
+
+def get_quantity(resource):
+ total = 0
+ if resource.BaseQuantity:
+ return resource.BaseQuantity[3]
+ if resource.Usage and resource.Usage.ScheduleWork:
+ # For now we assume either hourly or daily depending on how duration is stored
+ duration = ifcopenshell.util.date.ifc2datetime(resource.Usage.ScheduleWork)
+ if duration.days:
+ return duration.days
+ return duration.seconds / 60 / 60
+
+def get_parent_cost(resource):
+ if not resource.Nests:
+ return
+ else:
+ cost = get_cost(resource.Nests[0].RelatingObject)
+ return cost
diff --git a/src/ifcopenshell-python/ifcopenshell/util/schema/ifc_classes_suggestions.json b/src/ifcopenshell-python/ifcopenshell/util/schema/ifc_classes_suggestions.json
index 562e92fa49..878fef47dd 100644
--- a/src/ifcopenshell-python/ifcopenshell/util/schema/ifc_classes_suggestions.json
+++ b/src/ifcopenshell-python/ifcopenshell/util/schema/ifc_classes_suggestions.json
@@ -55,6 +55,28 @@
"name": "Signage"
}
],
+ "IfcGeographicElement": [
+ {
+ "name": "Tree"
+ },
+ {
+ "name": "Plants"
+ },
+ {
+ "name": "Shrubs"
+ }
+ ],
+ "IfcGeographicElementType": [
+ {
+ "name": "Tree"
+ },
+ {
+ "name": "Plants"
+ },
+ {
+ "name": "Shrubs"
+ }
+ ],
"IfcPlate": [
{
"name": "Glazing"
@@ -114,4 +136,4 @@
"name": "Pane"
}
]
-}
\ No newline at end of file
+}
diff --git a/src/ifcopenshell-python/ifcopenshell/util/selector.py b/src/ifcopenshell-python/ifcopenshell/util/selector.py
index e88dfac47b..0cc3fe537c 100644
--- a/src/ifcopenshell-python/ifcopenshell/util/selector.py
+++ b/src/ifcopenshell-python/ifcopenshell/util/selector.py
@@ -22,7 +22,10 @@ import ifcopenshell.util
import ifcopenshell.util.fm
import ifcopenshell.util.unit
import ifcopenshell.util.element
+import ifcopenshell.util.placement
+import ifcopenshell.util.geolocation
import ifcopenshell.util.classification
+from decimal import Decimal
filter_elements_grammar = lark.Lark(
@@ -51,7 +54,7 @@ filter_elements_grammar = lark.Lark(
ifc_class: /Ifc\\w+/
value: special | quoted_string | regex_string | unquoted_string
- unquoted_string: /[^.=\\s]+/
+ unquoted_string: /[^,.=\\s]+/
regex_string: "/" /[^\\/]+/ "/"
quoted_string: ESCAPED_STRING
@@ -115,16 +118,17 @@ get_element_grammar = lark.Lark(
format_grammar = lark.Lark(
"""start: function
- function: round | format_length | lower | upper | title | concat | ESCAPED_STRING | NUMBER
+ function: round | format_length | lower | upper | title | concat | substr | ESCAPED_STRING | NUMBER
round: "round(" function "," NUMBER ")"
format_length: metric_length | imperial_length
metric_length: "metric_length(" function "," NUMBER "," NUMBER ")"
- imperial_length: "imperial_length(" function "," NUMBER ["," ESCAPED_STRING] ")"
+ imperial_length: "imperial_length(" function "," NUMBER ["," ESCAPED_STRING "," ESCAPED_STRING] ")"
lower: "lower(" function ")"
upper: "upper(" function ")"
title: "title(" function ")"
concat: "concat(" function ("," function)* ")"
+ substr: "substr(" function "," SIGNED_INT ["," SIGNED_INT] ")"
// Embed common.lark for packaging
DIGIT: "0".."9"
@@ -181,8 +185,21 @@ class FormatTransformer(lark.Transformer):
def concat(self, args):
return "".join(args)
+ def substr(self, args):
+ if len(args) == 3:
+ if args[2] is None:
+ return str(args[0])[int(args[1]) :]
+ return str(args[0])[int(args[1]) : int(args[2])]
+ elif len(args) == 2:
+ return str(args[0])[int(args[1]) :]
+
def round(self, args):
- return str(round(float(args[0]) / float(args[1])) * float(args[1]))
+ value = Decimal(args[0] or 0.0)
+ nearest = Decimal(args[1])
+ result = round(value / nearest) * nearest
+ if nearest % 1 == 0:
+ return str(int(result))
+ return str(result)
def format_length(self, args):
return args[0]
@@ -195,17 +212,15 @@ class FormatTransformer(lark.Transformer):
def imperial_length(self, args):
if len(args) == 2:
- imperial_unit = "foot"
+ input_unit = "foot"
value, precision = args
else:
- value, precision, imperial_unit = args
- if imperial_unit == "inch":
- imperial_unit = "inch"
- else:
- imperial_unit = "foot"
+ value, precision, input_unit, output_unit = args
+ input_unit = "inch" if input_unit == "inch" else "foot"
+ output_unit = "inch" if output_unit == "inch" else "foot"
return ifcopenshell.util.unit.format_length(
- float(value), int(precision), unit_system="imperial", imperial_unit=imperial_unit
+ float(value), int(precision), unit_system="imperial", input_unit=input_unit, output_unit=output_unit
)
@@ -257,8 +272,6 @@ def set_element_value(ifc_file, element, query, value):
keys = GetElementTransformer().transform(get_element_grammar.parse(query))
for i, key in enumerate(keys):
- if isinstance(key, str):
- key = key.strip()
if element is None:
return
if key == "type":
@@ -291,6 +304,8 @@ def set_element_value(ifc_file, element, query, value):
return ifcopenshell.util.schema.reassign_class(ifc_file, element, value)
elif key == "id":
return
+ elif key in ("x", "y", "z", "easting", "northing", "elevation") and hasattr(element, "ObjectPlacement"):
+ return
elif isinstance(element, ifcopenshell.entity_instance):
if key == "Name" and element.is_a("IfcMaterialLayerSet"):
key = "LayerSetName" # This oddity in the IFC spec is annoying so we account for it.
@@ -772,8 +787,6 @@ class Selector:
def get_element_value(cls, element, keys):
value = element
for key in keys:
- if isinstance(key, str):
- key = key.strip()
if value is None:
return
if key == "type":
@@ -816,12 +829,23 @@ class Selector:
value = ifcopenshell.util.element.get_predefined_type(value)
elif key == "id":
value = value.id()
+ elif key in ("x", "y", "z", "easting", "northing", "elevation") and hasattr(value, "ObjectPlacement"):
+ if getattr(value, "ObjectPlacement", None):
+ matrix = ifcopenshell.util.placement.get_local_placement(value.ObjectPlacement)
+ xyz = matrix[:, 3][:3]
+ if key in ("x", "y", "z"):
+ value = xyz["xyz".index(key)]
+ else:
+ enh = ifcopenshell.util.geolocation.auto_xyz2enh(element.wrapped_data.file, *xyz)
+ value = enh[("easting", "northing", "elevation").index(key)]
+ else:
+ value = None
elif isinstance(value, ifcopenshell.entity_instance):
if key == "Name" and value.is_a("IfcMaterialLayerSet"):
key = "LayerSetName" # This oddity in the IFC spec is annoying so we account for it.
if isinstance(key, re.Pattern):
- attribute = None # Should we support regex attributes? Probably not for now.
+ attribute = None # Should we support regex attributes? Probably not for now.
else:
attribute = getattr(value, key, None)
diff --git a/src/ifcopenshell-python/ifcopenshell/util/sequence.py b/src/ifcopenshell-python/ifcopenshell/util/sequence.py
index 02ec945a63..b8ad222e8a 100644
--- a/src/ifcopenshell-python/ifcopenshell/util/sequence.py
+++ b/src/ifcopenshell-python/ifcopenshell/util/sequence.py
@@ -48,6 +48,14 @@ def derive_date(task, attribute_name, date=None, is_earliest=False, is_latest=Fa
def derive_calendar(task):
+ calendar = get_calendar(task)
+ if calendar:
+ return calendar
+ for rel in task.Nests or []:
+ return derive_calendar(rel.RelatingObject)
+
+
+def get_calendar(task):
calendar = [
rel.RelatingControl
for rel in task.HasAssignments or []
@@ -56,12 +64,12 @@ def derive_calendar(task):
]
if calendar:
return calendar[0]
- for rel in task.Nests or []:
- return derive_calendar(rel.RelatingObject)
def count_working_days(start, finish, calendar):
result = 0
+ if start == finish:
+ return 0
current_date = datetime.date(start.year, start.month, start.day)
finish_date = datetime.date(finish.year, finish.month, finish.day)
while current_date <= finish_date:
@@ -248,7 +256,7 @@ def get_task_work_schedule(task):
def get_nested_tasks(task):
- return [object for rel in task.IsNestedBy or [] for object in rel.RelatedObjects]
+ return [object for rel in task.IsNestedBy or [] for object in rel.RelatedObjects]
def get_parent_task(task):
@@ -426,3 +434,23 @@ def get_tasks_for_product(product, schedule=None):
]
return inputs, outputs
+
+
+def get_sequence_assignment(task, sequence="successor"):
+ if sequence == "successor":
+ relationship_attr = "IsPredecessorTo"
+ elif sequence == "predecessor":
+ relationship_attr = "IsSuccessorFrom"
+ else:
+ return []
+
+ relationship = getattr(task, relationship_attr, None)
+ if relationship:
+ return relationship
+
+ for rel in task.Nests or []:
+ result = get_sequence_assignment(rel.RelatingObject, sequence)
+ if result:
+ return result
+
+ return []
diff --git a/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py b/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py
index e34aa289c7..6941e37b2f 100644
--- a/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py
+++ b/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py
@@ -1,5 +1,5 @@
# IfcOpenShell - IFC toolkit and geometry engine
-# Copyright (C) 2022 @Andrej730
+# Copyright (C) 2022, 2023 @Andrej730
#
# This file is part of IfcOpenShell.
#
@@ -19,14 +19,22 @@
import collections
import ifcopenshell
import ifcopenshell.api
-from math import cos, sin, pi, tan, radians, degrees, atan, sqrt
+from math import cos, sin, pi, tan, radians, degrees, atan, sqrt, ceil
from mathutils import Vector, Matrix
from itertools import chain
+from typing import List
V = lambda *x: Vector([float(i) for i in x])
sign = lambda x: x and (1, -1)[x < 0]
PRECISION = 1.0e-5
-is_x = lambda value, x: (x + PRECISION) > value > (x - PRECISION)
+
+
+def is_x(value, x, si_conversion=None):
+ if si_conversion:
+ value = value * si_conversion
+ return (x + PRECISION) > value > (x - PRECISION)
+
+
round_to_precision = lambda x, si_conversion: round(x * si_conversion, 5) / si_conversion
round_vector_to_precision = lambda v, si_conversion: Vector([round_to_precision(i, si_conversion) for i in v])
@@ -41,9 +49,25 @@ class ShapeBuilder:
def __init__(self, ifc_file):
self.file = ifc_file
- def polyline(self, points, closed=False, position_offset=None, arc_points=[]):
- # > points - list of points formatted like ( (x0, y0), (x1, y1) )
- # < IfcIndexedPolyCurve
+ def polyline(
+ self, points: List[Vector], closed: bool = False, position_offset: Vector = None, arc_points: List[int] = []
+ ):
+ """
+ Generate an IfcIndexedPolyCurve based on the provided points.
+
+ :param points: List of points formatted as ( (x0, y0), (x1, y1) )
+ :type: List[Vector]
+ :param closed: Whether polyline should be closed. Default is False.
+ :type: bool, optional
+ :param position_offset: Optional offset to be applied to all points.
+ :type: Optional[Vector]
+ :param arc_points: Indices of the middle points for arcs. For creating an arc segment,
+ provide 3 points: `arc_start`, `arc_middle` and `arc_end` and add the `arc_middle`
+ point's index to this list.
+ :type: List[int]
+
+ :return: IfcIndexedPolyCurve
+ """
if arc_points and self.file.schema == "IFC2X3":
raise Exception("Arcs are not supported for IFC2X3.")
@@ -101,7 +125,7 @@ class ShapeBuilder:
if len(segment) == 3:
ifc_segments.append(self.file.createIfcArcIndex(segment))
- # NOTE: IfcIndexPolyCurve support only consequtive segments
+ # NOTE: IfcIndexPolyCurve support only consecutive segments
ifc_curve = self.file.createIfcIndexedPolyCurve(Points=ifc_points, Segments=ifc_segments)
return ifc_curve
@@ -534,6 +558,9 @@ class ShapeBuilder:
Position and position axes are in world space, extrusion vector in placement space defined by
position_x_axis/position_y_axis/position_z_axis
+
+ NOTE: changing position also changes the resulting geometry origin.
+
"""
# > profile_or_curve
# > extrusion vector - as defined in coordinate system position_x_axis+position_z_axis
@@ -575,9 +602,9 @@ class ShapeBuilder:
disk_solid = self.file.createIfcSweptDiskSolid(Directrix=path_curve, Radius=radius)
return disk_solid
- def get_representation(self, context, items, representation_type:str = None):
+ def get_representation(self, context, items, representation_type: str = None):
"""Create IFC representation for the specified context and items.
-
+
:param context: IfcGeometricRepresentationSubContext
:param items: could be a list or single curve/IfcExtrudedAreaSolid
:param representation_type: Explicitly specified RepresentationType, defaults to `None`.
@@ -614,14 +641,36 @@ class ShapeBuilder:
return ifcopenshell.util.element.copy_deep(self.file, element)
# UTILITIES
- def extrude_by_y_kwargs(self):
- """shortcut for `ShapeBuilder.extrude` to extrude by y axis.
- it assumes you have 2d profile in xz plane and trying to extrude it by y axis"""
- return {
- "position_x_axis": Vector((1, 0, 0)),
- "position_z_axis": Vector((0, -1, 0)),
- "extrusion_vector": Vector((0, 0, -1)),
- }
+ def extrude_kwargs(self, axis):
+ """Shortcut to get kwargs for `ShapeBuilder.extrude` to extrude by some axis.
+
+ It assumes you have 2D profile in:
+ XZ plane for Y axis extrusion, \n
+ YZ plane for X axis extrusion, \n
+ XY plane for Z axis extrusion, \n
+
+ Extruding by X/Y using other kwargs might break ValidExtrusionDirection."""
+
+ axis = axis.upper()
+
+ if axis == "Y":
+ return {
+ "position_x_axis": Vector((1, 0, 0)),
+ "position_z_axis": Vector((0, -1, 0)),
+ "extrusion_vector": Vector((0, 0, -1)),
+ }
+ elif axis == "X":
+ return {
+ "position_x_axis": Vector((0, 1, 0)),
+ "position_z_axis": Vector((1, 0, 0)),
+ "extrusion_vector": Vector((0, 0, 1)),
+ }
+ elif axis == "Z":
+ return {
+ "position_x_axis": Vector((1, 0, 0)),
+ "position_z_axis": Vector((0, 0, 1)),
+ "extrusion_vector": Vector((0, 0, 1)),
+ }
def rotate_extrusion_kwargs_by_z(self, kwargs, angle, counter_clockwise=False):
"""shortcut to rotate extrusion kwargs by z axis
@@ -856,8 +905,55 @@ class ShapeBuilder:
return face_set
+ def extrude_face_set(
+ self, points, magnitude: float, extrusion_vector=V(0, 0, 1).freeze(), offset=None, start_cap=True, end_cap=True
+ ):
+ """
+ Method to extrude by creating face sets rather than creating IfcExtrudedAreaSolid.
+
+ Useful if your representation is already using face sets and you need to avoid using SweptSolid
+ to assure CorrectItemsForType.
+
+ :param points: list of points, assuming they form consecutive closed polyline.
+ :param magnitude: extrusion magnitude
+ :param type: float
+ :param extrusion_vector: extrusion direction, by default it's extruding by Z+ axis
+ :param type: Vector, optional
+ :param offset: offset from the points
+ :param type: Vector, optional
+ :param start_cap: if True, create start cap, by default it's True
+ :param type: bool, optional
+ :param end_cap: if True, create end cap, by default it's True
+ :param type: bool, optional
+
+ :return: IfcPolygonalFaceSet
+ """
+
+ # prevent mutating arguments, deepcopy doesn't work
+ start_points = [p.copy() if not offset else (p + offset) for p in points]
+ extrusion_offset = magnitude * extrusion_vector
+ end_points = [p + extrusion_offset for p in start_points]
+
+ points = start_points + end_points
+ faces = []
+ n_verts = len(start_points)
+ last_vert_i = n_verts - 1
+ for i in range(last_vert_i):
+ face = (i, i + 1, n_verts + i + 1, n_verts + i)
+ faces.append(face)
+ faces.append((last_vert_i, 0, n_verts + 0, n_verts + last_vert_i)) # close the loop
+
+ if end_cap:
+ faces.append(tuple(range(n_verts, n_verts * 2)))
+ if start_cap:
+ faces.append(tuple(reversed(range(n_verts))))
+
+ face_set = self.polygonal_face_set(points, faces)
+ return face_set
+
+ # TODO: move MEP to separate shape builder sub module
def mep_transition_shape(
- self, start_segment, end_segment, start_length, end_length, angle=30.0, profile_offset=None
+ self, start_segment, end_segment, start_length, end_length, angle=30.0, profile_offset=V(0, 0).freeze()
):
"""
returns tuple of Model/Body/MODEL_VIEW IfcRepresentation and transition shape data
@@ -899,32 +995,6 @@ class ShapeBuilder:
return V(profile.Radius, profile.Radius, depth)
return None
- def get_profile_faceset(points, length, offset=None):
- # prevent mutating arguments, deepcopy doesn't work
- start_points = [p.copy() if not offset else (p + offset) for p in points]
- end_points = [p.copy() for p in start_points]
- for p in end_points:
- p.z += length
-
- points = start_points + end_points
- faces = []
- n_verts = len(start_points)
- last_vert_i = n_verts - 1
- for i in range(last_vert_i):
- face = (i, i + 1, n_verts + i + 1, n_verts + i)
- faces.append(face)
- faces.append((last_vert_i, 0, n_verts + 0, n_verts + last_vert_i)) # close the loop
-
- # if there is offset we put a cap at the end
- # otherwise at the start
- if offset:
- faces.append(tuple(range(n_verts, n_verts * 2)))
- else:
- faces.append(tuple(reversed(range(n_verts))))
-
- face_set = self.polygonal_face_set(points, faces)
- return face_set
-
start_profile = get_profile(start_segment)
end_profile = get_profile(end_segment)
@@ -945,8 +1015,7 @@ class ShapeBuilder:
faces = []
end_extrusion_offset.z += transition_length
- if profile_offset:
- end_extrusion_offset.xy += profile_offset
+ end_extrusion_offset.xy += profile_offset
if start_profile.is_a("IfcRectangleProfileDef") and end_profile.is_a("IfcRectangleProfileDef"):
# no transitions for exactly the same profiles
@@ -1004,8 +1073,10 @@ class ShapeBuilder:
face = [i, next_i, next_i + n_segments, i + n_segments]
faces.append(face)
- transition_items.append(get_profile_faceset(first_profile_points, start_length))
- transition_items.append(get_profile_faceset(second_profile_points, end_length, end_extrusion_offset))
+ transition_items.append(self.extrude_face_set(first_profile_points, start_length, end_cap=False))
+ transition_items.append(
+ self.extrude_face_set(second_profile_points, end_length, offset=end_extrusion_offset, start_cap=False)
+ )
first_profile_points = [p + start_offset for p in first_profile_points]
second_profile_points = [p + end_extrusion_offset for p in second_profile_points]
@@ -1032,8 +1103,10 @@ class ShapeBuilder:
else:
start_points, end_points = rect_points, circle_points
- transition_items.append(get_profile_faceset(start_points, start_length))
- transition_items.append(get_profile_faceset(end_points, end_length, end_extrusion_offset))
+ transition_items.append(self.extrude_face_set(start_points, start_length, end_cap=False))
+ transition_items.append(
+ self.extrude_face_set(end_points, end_length, offset=end_extrusion_offset, start_cap=False)
+ )
# offset verts
if starting_with_circle:
@@ -1077,10 +1150,12 @@ class ShapeBuilder:
body = ifcopenshell.util.representation.get_context(self.file, "Model", "Body", "MODEL_VIEW")
representation = self.get_representation(body, transition_items, "Tesselation")
+
transition_data = {
"start_length": start_length,
"end_length": end_length,
"angle": angle,
+ "profile_offset": profile_offset,
"transition_length": transition_length,
"full_transition_length": start_length + transition_length + end_length,
}
@@ -1089,7 +1164,7 @@ class ShapeBuilder:
# TODO: move to separate shape_builder method
# so we could check transition length without creating representation
- def mep_transition_length(self, start_half_dim, end_half_dim, angle, profile_offset=None, verbose=True):
+ def mep_transition_length(self, start_half_dim, end_half_dim, angle, profile_offset=V(0, 0).freeze(), verbose=True):
"""get the final transition length for two profiles dimensions, angle and XY offset between them,
the difference from `calculate_transition` - `get_transition_length` is making sure
@@ -1100,7 +1175,7 @@ class ShapeBuilder:
# offsets tend to have bunch of float point garbage
# that can result in errors when we're calculating value for square root below
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(self.file)
- offset = V(0, 0) if profile_offset is None else round_vector_to_precision(profile_offset, si_conversion)
+ offset = round_vector_to_precision(profile_offset, si_conversion)
diff = start_half_dim.xy - end_half_dim.xy
diff = Vector([abs(i) for i in diff])
@@ -1237,3 +1312,151 @@ class ShapeBuilder:
else:
angle = degrees(atan(offset.x / h))
return angle
+
+ def mep_bend_shape(
+ self,
+ segment,
+ start_length: float,
+ end_length: float,
+ angle: float,
+ radius: float,
+ profile_offset: Vector,
+ flip_z_axis: bool,
+ ):
+ """
+
+ :param segment: IfcFlowSegment for a bend.
+ Note that for a bend start and end segments types should match.
+
+ :param angle: bend angle, in radians
+ :param type: float
+ :param radius: bend radius
+ :param type: float
+ :param profile_offset: offset between start and end segments in local space of start segment
+ used mainly to determine the seconn bend axis and it's direction.
+ :param type: Vector
+ :param flip_z_axis: since we cannot determine z axis direction from the profile offset,
+ there is an option to flip it if bend is going by start segment Z- axis.
+ :param type: bool
+
+ :return: tuple of Model/Body/MODEL_VIEW IfcRepresentation and transition shape data
+ """
+
+ def get_profile(element):
+ material = ifcopenshell.util.element.get_material(element, should_skip_usage=True)
+ if material and material.is_a("IfcMaterialProfileSet") and len(material.MaterialProfiles) == 1:
+ return material.MaterialProfiles[0].Profile
+
+ def get_dim(profile, depth):
+ if profile.is_a("IfcRectangleProfileDef"):
+ return V(profile.XDim / 2, profile.YDim / 2, depth)
+ elif profile.is_a("IfcCircleProfileDef"):
+ return V(profile.Radius, profile.Radius, depth)
+ return None
+
+ si_conversion = ifcopenshell.util.unit.calculate_unit_scale(self.file)
+ profile = get_profile(segment)
+ is_circular_profile = profile.is_a("IfcCircleProfileDef")
+ profile_dim = get_dim(profile, start_length)
+
+ rounded_offset = round_vector_to_precision(profile_offset, si_conversion)
+ lateral_axis = next(i for i in range(2) if not is_x(rounded_offset[i], 0))
+ non_lateral_axis = 1 if lateral_axis == 0 else 0
+ lateral_sign = sign(profile_offset[lateral_axis])
+ z_sign = -1 if flip_z_axis else 1
+
+ rep_items = []
+
+ # bend circle center
+ O = V(0, 0, 0)
+ O[lateral_axis] = (radius + profile_dim[lateral_axis]) * lateral_sign
+ theta = angle
+
+ def get_circle_point(angle, radius):
+ point = V(0, 0, 0)
+ angle -= pi / 2
+ # fmt: off
+ point.z = z_sign * cos(angle) * radius
+ point[lateral_axis] = lateral_sign * sin(angle) * radius
+ # fmt: on
+ return point
+
+ def get_circle_tangent(angle):
+ tangent = V(0, 0, 0)
+ tangent.z = cos(angle) * z_sign
+ tangent[lateral_axis] = sin(angle) * lateral_sign
+ return tangent
+
+ def get_bend_representation_item():
+ r = radius
+ theta_segments = [0, theta / 2, theta]
+ if is_circular_profile:
+ r += profile_dim[lateral_axis]
+ points = [get_circle_point(cur_theta, r) for cur_theta in theta_segments]
+ arc_points = (1,)
+ else:
+ outer_r = r + 2 * profile_dim[lateral_axis]
+ outer_points = [get_circle_point(cur_theta, outer_r) for cur_theta in theta_segments[::-1]]
+ if is_x(r, 0):
+ points = [get_circle_point(theta, r)] + outer_points
+ arc_points = (2,)
+ else:
+ inner_points = [get_circle_point(cur_theta, r) for cur_theta in theta_segments]
+ points = inner_points + outer_points
+ arc_points = (1, 4)
+
+ points = [p + O for p in points]
+ offset = V(0, 0, 0)
+ offset.z = z_sign * start_length
+
+ if is_circular_profile:
+ bend_path = self.polyline(points, closed=False, arc_points=arc_points, position_offset=offset)
+ bend = self.create_swept_disk_solid(bend_path, profile_dim[lateral_axis])
+ else:
+ main_axes = lambda v: getattr(v, "xy"[lateral_axis] + "z")
+ offset[non_lateral_axis] = -profile_dim[non_lateral_axis]
+
+ extrusion_kwargs = self.extrude_kwargs("XY"[non_lateral_axis])
+ profile_curve = self.polyline([main_axes(p) for p in points], arc_points=arc_points, closed=True)
+ bend = self.extrude(
+ self.profile(profile_curve), profile_dim[non_lateral_axis] * 2, position=offset, **extrusion_kwargs
+ )
+ return bend
+
+ rep_items.append(get_bend_representation_item())
+ if start_length:
+ rep_items.append(self.extrude(profile, start_length, extrusion_vector=V(0, 0, z_sign)))
+ if end_length:
+ end_position = O + get_circle_point(theta, radius + profile_dim[lateral_axis])
+ end_position.z += start_length * z_sign
+
+ # define extrusion space for the segment after the bend
+ z_axis = get_circle_tangent(theta)
+ extrude_kwargs = {
+ "position_z_axis": z_axis,
+ "extrusion_vector": Vector((0, 0, 1)),
+ }
+ # since we are sure that tangent involves only two axis
+ # it's safe to assume that non lateral axis is untouched
+ if lateral_axis == 0:
+ x_axis = z_axis.cross(Vector((0, 1, 0)))
+ else:
+ x_axis = Vector((1, 0, 0))
+ extrude_kwargs["position_x_axis"] = x_axis
+
+ rep_items.append(self.extrude(profile, end_length, end_position, **extrude_kwargs))
+
+ body = ifcopenshell.util.representation.get_context(self.file, "Model", "Body", "MODEL_VIEW")
+ rep = self.get_representation(body, rep_items)
+
+ bend_data = {
+ "start_length": start_length,
+ "end_length": end_length,
+ "radius": radius,
+ "angle": degrees(theta),
+ "lateral_axis": lateral_axis,
+ "lateral_sign": lateral_sign,
+ "z_axis_sign": -1 if flip_z_axis else 1,
+ "main_profile_dimension": profile_dim[lateral_axis],
+ }
+ return rep, bend_data
diff --git a/src/ifcopenshell-python/ifcopenshell/util/system.py b/src/ifcopenshell-python/ifcopenshell/util/system.py
index 51be9062f5..0c8789a098 100644
--- a/src/ifcopenshell-python/ifcopenshell/util/system.py
+++ b/src/ifcopenshell-python/ifcopenshell/util/system.py
@@ -92,35 +92,37 @@ def get_connected_port(port):
return rel.RelatingPort
+def get_port_element(port):
+ if hasattr(port, "Nests"):
+ for rel in port.Nests:
+ return rel.RelatingObject
+ # IFC2X3 only, deprecated in IFC4
+ elif hasattr(port, "ContainedIn"):
+ for rel in port.ContainedIn:
+ return rel.RelatedElement
+
+
def get_connected_to(element, flow_direction=None):
results = []
for port in ifcopenshell.util.system.get_ports(element, flow_direction=flow_direction):
- for relConnectsPort in port.ConnectedTo:
- for disPort in [relConnectsPort.RelatedPort, relConnectsPort.RelatingPort]:
- if hasattr(disPort, "Nests"):
- for relNest in disPort.Nests:
- if relNest.RelatingObject != element:
- results.append(relNest.RelatingObject)
- # IFC2X3 only, deprecated in IFC4
- elif hasattr(disPort, "ContainedIn"):
- for relConPortToElement in disPort.ContainedIn:
- if relConPortToElement.RelatedElement != element:
- results.append(relConPortToElement.RelatedElement)
+ for rel in port.ConnectedTo:
+ for other_port in [rel.RelatedPort, rel.RelatingPort]:
+ if other_port == port:
+ continue
+ other_element = get_port_element(other_port)
+ if other_element:
+ results.append(other_element)
return results
def get_connected_from(element, flow_direction=None):
results = []
for port in ifcopenshell.util.system.get_ports(element, flow_direction=flow_direction):
- for relConnectsPort in port.ConnectedFrom:
- for disPort in [relConnectsPort.RelatedPort, relConnectsPort.RelatingPort]:
- if hasattr(disPort, "Nests"):
- for relNest in disPort.Nests:
- if relNest.RelatingObject != element:
- results.append(relNest.RelatingObject)
- # IFC2X3 only, deprecated in IFC4
- elif hasattr(disPort, "ContainedIn"):
- for relConPortToElement in disPort.ContainedIn:
- if relConPortToElement.RelatedElement != element:
- results.append(relConPortToElement.RelatedElement)
+ for rel in port.ConnectedFrom:
+ for other_port in [rel.RelatedPort, rel.RelatingPort]:
+ if other_port == port:
+ continue
+ other_element = get_port_element(other_port)
+ if other_element:
+ results.append(other_element)
return results
diff --git a/src/ifcopenshell-python/ifcopenshell/util/unit.py b/src/ifcopenshell-python/ifcopenshell/util/unit.py
index e3dc8aa6d0..a96c424835 100644
--- a/src/ifcopenshell-python/ifcopenshell/util/unit.py
+++ b/src/ifcopenshell-python/ifcopenshell/util/unit.py
@@ -362,12 +362,18 @@ def get_property_unit(prop, ifc_file):
if not unit_assignment:
return
entity = prop.wrapped_data.declaration().as_entity()
+ measure_class = None
if prop.is_a("IfcPhysicalSimpleQuantity"):
measure_class = entity.attribute_by_index(3).type_of_attribute().declared_type().name()
elif prop.is_a("IfcPropertySingleValue") and prop.NominalValue:
measure_class = prop.NominalValue.is_a()
- elif prop.is_a("IfcPropertyEnumeratedValue") and prop.EnumerationValues:
- measure_class = prop.EnumerationValues[0].is_a()
+ elif prop.is_a("IfcPropertyEnumeratedValue"):
+ if prop.EnumerationReference:
+ unit = getattr(prop.EnumerationReference, "Unit", None)
+ if unit:
+ return unit
+ if prop.EnumerationValues:
+ measure_class = prop.EnumerationValues[0].is_a()
elif prop.is_a("IfcPropertyListValue") and prop.ListValues:
measure_class = prop.ListValues[0].is_a()
elif prop.is_a("IfcPropertyBoundedValue"):
@@ -393,6 +399,8 @@ def get_property_unit(prop, ifc_file):
else:
table_units[f"{attribute}Unit"] = None
return table_units
+ if measure_class is None:
+ return
unit_type = get_measure_unit_type(measure_class)
units = [u for u in unit_assignment.Units if getattr(u, "UnitType", None) == unit_type]
if units:
@@ -548,12 +556,18 @@ def calculate_unit_scale(ifc_file):
def format_length(
- value, precision, decimal_places=2, suppress_zero_inches=True, unit_system="imperial", imperial_unit="foot"
+ value,
+ precision,
+ decimal_places=2,
+ suppress_zero_inches=True,
+ unit_system="imperial",
+ input_unit="foot",
+ output_unit="foot",
):
"""Formats a length for readability and imperial formatting
:param value: The value in meters if metric, or either decimal feet or
- inches if imperial depending on imperial_unit.
+ inches if imperial depending on input_unit.
:type value: float
:param precision: How precise the format should be. I.e. round to nearest.
For imperial, it is 1/Nth. E.g. 12 means to the nearest 1/12th of an
@@ -566,18 +580,20 @@ def format_length(
:type suppress_zero_inches: bool
:param unit_system: Choose whether your value is "metric" or "imperial"
:type unit_system: str
- :param imperial_unit: If imperial, specify whether your value is "foot" or
+ :param input_unit: If imperial, specify whether your value is "foot" or
"inch".
- :type imperial_unit: str
+ :type input_unit: str
+ :param output_unit: If imperial, specify whether your value is "foot" to
+ format as both feet and inches, or "inch" if only inches should be
+ shown.
"""
if unit_system == "imperial":
- if imperial_unit == "foot":
+ if input_unit == "foot":
feet = int(value)
inches = (value - feet) * 12
-
- elif imperial_unit == "inch":
- inches = value * 12
-
+ elif input_unit == "inch":
+ inches = value % 12
+ feet = int(round((value - inches) / 12))
# Round to the nearest 1/N
nearest = round(inches * precision)
@@ -587,22 +603,22 @@ def format_length(
# If fraction is a whole number, format it accordingly
if frac.denominator == 1:
- if imperial_unit == "inch":
- return f"{round(inches)}\""
- if suppress_zero_inches:
- if imperial_unit == "foot":
- return f"{round(value)}'"
- elif not suppress_zero_inches:
- if imperial_unit == "foot":
- return f"{round(value)}' - 0\""
- if frac.numerator > frac.denominator and not frac.denominator == 0:
+ if suppress_zero_inches and frac.numerator == 0:
+ if output_unit == "foot":
+ return f"{feet}'"
+ return f'{feet * 12}"'
+ if output_unit == "foot":
+ return f"{feet}' - {frac.numerator}\""
+ return f'{(feet * 12) + frac.numerator}"'
+ if frac.numerator > frac.denominator:
remainder = frac.numerator % frac.denominator
whole = int((frac.numerator - remainder) / frac.denominator)
- if imperial_unit == "foot":
+ if output_unit == "foot":
return f"{feet}' - {whole} {remainder}/{frac.denominator}\""
- elif imperial_unit == "inch":
- return f"{whole} {remainder}/{frac.denominator}\""
-
+ return f'{(feet * 12) + whole} {remainder}/{frac.denominator}"'
+ if output_unit == "foot":
+ return f"{feet}' - {frac.numerator}/{frac.denominator}\""
+ return f'{feet * 12} {frac.numerator}/{frac.denominator}"'
elif unit_system == "metric":
rounded_val = round(value / precision) * precision
return f"{rounded_val:.{decimal_places}f}"
diff --git a/src/ifcopenshell-python/pyproject.toml b/src/ifcopenshell-python/pyproject.toml
index dc8db46e66..cf65b9de30 100644
--- a/src/ifcopenshell-python/pyproject.toml
+++ b/src/ifcopenshell-python/pyproject.toml
@@ -15,6 +15,7 @@ classifiers = [
]
[project.optional-dependencies]
geometry = ["mathutils"]
+date = ["isodate"]
[project.urls]
"Homepage" = "http://ifcopenshell.org"
"Bug Tracker" = "https://github.com/ifcopenshell/ifcopenshell/issues"
diff --git a/src/ifcopenshell-python/test/api/pset/test_remove_pset.py b/src/ifcopenshell-python/test/api/pset/test_remove_pset.py
index 3ce8d0127e..1685f158e8 100644
--- a/src/ifcopenshell-python/test/api/pset/test_remove_pset.py
+++ b/src/ifcopenshell-python/test/api/pset/test_remove_pset.py
@@ -83,3 +83,27 @@ class TestRemovePset(test.bootstrap.IFC4):
pset2.HasProperties = pset.HasProperties
ifcopenshell.api.run("pset.remove_pset", self.file, product=element, pset=pset)
assert pset2.HasProperties
+
+ def test_removing_a_pset_with_enumeration(self):
+ element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="Pset_WallCommon")
+ ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Status": ["NEW"]})
+ ifcopenshell.api.run("pset.remove_pset", self.file, product=element, pset=pset)
+ assert len(self.file.by_type("IfcPropertyEnumeration")) == 0
+
+ def test_removing_a_pset_with_shared_enumeration(self):
+ element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="Pset_WallCommon")
+ ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Status": ["NEW"]})
+
+ element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ pset2 = ifcopenshell.api.run("pset.add_pset", self.file, product=element2, name="Pset_WallCommon")
+ ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset2, properties={"Status": ["NEW"]})
+
+ enumeration1 = pset.HasProperties[0].EnumerationReference
+ enumeration2 = pset2.HasProperties[0].EnumerationReference
+ pset2.HasProperties[0].EnumerationReference = enumeration1
+ self.file.remove(enumeration2)
+
+ ifcopenshell.api.run("pset.remove_pset", self.file, product=element, pset=pset)
+ assert len(self.file.by_type("IfcPropertyEnumeration")) == 1
diff --git a/src/ifcopenshell-python/test/util/test_selector.py b/src/ifcopenshell-python/test/util/test_selector.py
index a63a8fdfb2..c5b69a8724 100644
--- a/src/ifcopenshell-python/test/util/test_selector.py
+++ b/src/ifcopenshell-python/test/util/test_selector.py
@@ -34,10 +34,13 @@ class TestFormat():
assert subject.format('title(\"fOo\")') == "Foo"
assert subject.format('concat(\"fOo\", \"bar\")') == "fOobar"
assert subject.format('upper(concat(\"fOo\", \"bar\"))') == "FOOBAR"
+ assert subject.format('substr(\"foobar\", 3)') == "bar"
+ assert subject.format('substr(\"foobar\", 1, 2)') == "o"
+ assert subject.format('substr(\"foobar\", 1, -1)') == "ooba"
def test_number_formatting(self):
- assert subject.format("round(123, 5)") == "125.0"
- assert subject.format('round(\"123\", 5)') == "125.0"
+ assert subject.format("round(123, 5)") == "125"
+ assert subject.format('round(\"123\", 5)') == "125"
assert subject.format('metric_length(123, 5, 2)') == "125.00"
assert subject.format('metric_length(123.123, 0.1, 2)') == "123.10"
assert subject.format('metric_length(\"123\", 5, 2)') == "125.00"
@@ -45,7 +48,8 @@ class TestFormat():
assert subject.format('imperial_length(3.123, 1)') == "3' - 1\""
assert subject.format('imperial_length(3.123, 2)') == "3' - 1 1/2\""
assert subject.format('imperial_length(\"3.123\", 2)') == "3' - 1 1/2\""
- assert subject.format('imperial_length(\"123.123\", 2, \"inch\")') == "10' - 3\""
+ assert subject.format('imperial_length(\"123.123\", 2, \"inch\", \"foot\")') == "10' - 3\""
+ assert subject.format('imperial_length(\"123.123\", 2, \"inch\", \"inch\")') == "123\""
class TestGetElementValue(test.bootstrap.IFC4):
@@ -147,6 +151,9 @@ class TestFilterElements(test.bootstrap.IFC4):
assert subject.filter_elements(self.file, 'IfcWall, Name="Foo\'s \\"quoted\\" name..."') == {element}
assert subject.filter_elements(self.file, "IfcWall, Name=/Fo.*/") == {element}
assert subject.filter_elements(self.file, "IfcWall, Description=NULL") == {element, element2}
+ element.Name = "Foo"
+ element.Description = "Foobar"
+ assert subject.filter_elements(self.file, "IfcWall, Name=Foo, Description=Foobar") == {element}
def test_selecting_by_type(self):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
diff --git a/src/ifcopenshell-python/test/util/test_unit.py b/src/ifcopenshell-python/test/util/test_unit.py
new file mode 100644
index 0000000000..cdf028a222
--- /dev/null
+++ b/src/ifcopenshell-python/test/util/test_unit.py
@@ -0,0 +1,59 @@
+# IfcOpenShell - IFC toolkit and geometry engine
+# Copyright (C) 2023 Dion Moult
+#
+# This file is part of IfcOpenShell.
+#
+# IfcOpenShell is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# IfcOpenShell 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 Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with IfcOpenShell. If not, see .
+
+import pytest
+import test.bootstrap
+import ifcopenshell.api
+import ifcopenshell.util.unit as subject
+
+
+class TestFormatLength(test.bootstrap.IFC4):
+ def test_run(self):
+ assert subject.format_length(1, 1, decimal_places=0, unit_system="metric") == "1"
+ assert subject.format_length(1, 1, decimal_places=2, unit_system="metric") == "1.00"
+ assert subject.format_length(3, 5, decimal_places=2, unit_system="metric") == "5.00"
+ assert subject.format_length(3.123, 0.01, decimal_places=2, unit_system="metric") == "3.12"
+
+ assert subject.format_length(3, 1, unit_system="imperial", input_unit="foot") == "3'"
+ assert subject.format_length(3.5, 1, unit_system="imperial", input_unit="foot") == "3' - 6\""
+ assert subject.format_length(3.123, 1, unit_system="imperial", input_unit="foot") == "3' - 1\""
+ assert subject.format_length(3.123, 2, unit_system="imperial", input_unit="foot") == "3' - 1 1/2\""
+ assert subject.format_length(3.123, 4, unit_system="imperial", input_unit="foot") == "3' - 1 1/2\""
+ assert subject.format_length(3.123, 32, unit_system="imperial", input_unit="foot") == "3' - 1 15/32\""
+ assert subject.format_length(24, 1, unit_system="imperial", input_unit="inch") == "2'"
+ assert subject.format_length(25.23, 1, unit_system="imperial", input_unit="inch") == "2' - 1\""
+ assert subject.format_length(25.23, 4, unit_system="imperial", input_unit="inch") == "2' - 1 1/4\""
+
+ assert subject.format_length(3, 1, unit_system="imperial", input_unit="foot", output_unit="inch") == '36"'
+ assert subject.format_length(3.5, 1, unit_system="imperial", input_unit="foot", output_unit="inch") == '42"'
+ assert subject.format_length(3.123, 1, unit_system="imperial", input_unit="foot", output_unit="inch") == '37"'
+ assert (
+ subject.format_length(3.123, 2, unit_system="imperial", input_unit="foot", output_unit="inch") == '37 1/2"'
+ )
+ assert (
+ subject.format_length(3.123, 4, unit_system="imperial", input_unit="foot", output_unit="inch") == '37 1/2"'
+ )
+ assert (
+ subject.format_length(3.123, 32, unit_system="imperial", input_unit="foot", output_unit="inch")
+ == '37 15/32"'
+ )
+ assert subject.format_length(24, 1, unit_system="imperial", input_unit="inch", output_unit="inch") == '24"'
+ assert subject.format_length(25.23, 1, unit_system="imperial", input_unit="inch", output_unit="inch") == '25"'
+ assert (
+ subject.format_length(25.23, 4, unit_system="imperial", input_unit="inch", output_unit="inch") == '25 1/4"'
+ )
diff --git a/src/ifcparse/IfcFile.h b/src/ifcparse/IfcFile.h
index eaf5d8848c..88bed3c065 100644
--- a/src/ifcparse/IfcFile.h
+++ b/src/ifcparse/IfcFile.h
@@ -272,7 +272,7 @@ public:
unsigned int getMaxId() const { return MaxId; }
- const IfcParse::declaration* const ifcroot_type() const { return ifcroot_type_; }
+ const IfcParse::declaration* ifcroot_type() const { return ifcroot_type_; }
void recalculate_id_counter();
diff --git a/src/ifcparse/IfcSchema.cpp b/src/ifcparse/IfcSchema.cpp
index 2d26efa0da..a21023a865 100644
--- a/src/ifcparse/IfcSchema.cpp
+++ b/src/ifcparse/IfcSchema.cpp
@@ -13,8 +13,8 @@ bool IfcParse::declaration::is(const std::string& name) const {
if (name_upper_ == *name_ptr) return true;
- if (this->as_entity()) {
- return this->as_entity()->is(name);
+ if (this->as_entity() && this->as_entity()->supertype()) {
+ return this->as_entity()->supertype()->is(name);
} else if (this->as_type_declaration()) {
const IfcParse::named_type* nt = this->as_type_declaration()->declared_type()->as_named_type();
if (nt) return nt->is(name);
@@ -26,8 +26,8 @@ bool IfcParse::declaration::is(const std::string& name) const {
bool IfcParse::declaration::is(const IfcParse::declaration& decl) const {
if (this == &decl) return true;
- if (this->as_entity()) {
- return this->as_entity()->is(decl);
+ if (this->as_entity() && this->as_entity()->supertype()) {
+ return this->as_entity()->supertype()->is(decl);
} else if (this->as_type_declaration()) {
const IfcParse::named_type* nt = this->as_type_declaration()->declared_type()->as_named_type();
if (nt) return nt->is(decl);
diff --git a/src/ifcparse/IfcSchema.h b/src/ifcparse/IfcSchema.h
index 4ec3e93b7d..5661530844 100644
--- a/src/ifcparse/IfcSchema.h
+++ b/src/ifcparse/IfcSchema.h
@@ -293,18 +293,6 @@ namespace IfcParse {
virtual ~entity();
- bool is(const std::string& name) const {
- if (name == name_) return true;
- else if (supertype_) return supertype_->is(name);
- else return false;
- }
-
- bool is(const IfcParse::declaration& decl) const {
- if (this == &decl) return true;
- else if (supertype_) return supertype_->is(decl);
- else return false;
- }
-
bool is_abstract() const { return is_abstract_; }
void set_subtypes(const std::vector& subtypes) {
diff --git a/src/ifcpatch/ifcpatch/recipes/ResetAbsoluteCoordinates.py b/src/ifcpatch/ifcpatch/recipes/ResetAbsoluteCoordinates.py
index a44b41211f..37fd94e2c6 100644
--- a/src/ifcpatch/ifcpatch/recipes/ResetAbsoluteCoordinates.py
+++ b/src/ifcpatch/ifcpatch/recipes/ResetAbsoluteCoordinates.py
@@ -18,7 +18,7 @@
class Patcher:
- def __init__(self, src, file, logger, a=None, b=None, c=None, d=None):
+ def __init__(self, src, file, logger, mode="geometry", a=None, b=None, c=None, d=None):
"""Reset any large coordinates to smaller coordinates based on a threshold
If you find large coordinates in your model, the large coordinates may
@@ -59,6 +59,13 @@ class Patcher:
numbers are treated as the X, Y, Z offset to apply (a, b, c). The fourth
(d) will be treated as the threshold.
+ :param mode: Choose from "geometry", "placement", or "both". Choosing
+ "geometry" will only replace cartesian points used in shape
+ representations. Choosing "placement" will only replace cartesian
+ points used in object placements. Choosing "both" will replace all
+ cartesian points regardless of use (useful if the model has both
+ large placement offsets and large geometry offsets).
+ :type mode: str
:param a: The first parameter
:type a: float,optional
:param b: The second parameter
@@ -87,6 +94,7 @@ class Patcher:
self.src = src
self.file = file
self.logger = logger
+ self.mode = mode
self.args = [x for x in [a, b, c, d] if x is not None]
def patch(self):
@@ -128,8 +136,12 @@ class Patcher:
for point in self.file.by_type("IfcCartesianPoint"):
if len(point.Coordinates) == 2 or not self.is_point_far_away(point):
continue
- if point.id() in placement_coord_ids:
- continue
+ if self.mode == "geometry":
+ if point.id() in placement_coord_ids:
+ continue
+ elif self.mode == "placement":
+ if point.id() not in placement_coord_ids:
+ continue
if not offset_point:
offset_point = (-point.Coordinates[0], -point.Coordinates[1], -point.Coordinates[2])
self.logger.info(f"Resetting absolute coordinates by {point}")
diff --git a/src/ifctester/ifctester/facet.py b/src/ifctester/ifctester/facet.py
index 2e5efdf0ae..d10664fee2 100644
--- a/src/ifctester/ifctester/facet.py
+++ b/src/ifctester/ifctester/facet.py
@@ -21,6 +21,7 @@ import builtins
import ifcopenshell.util.unit
import ifcopenshell.util.element
import ifcopenshell.util.classification
+from functools import lru_cache
from xmlschema.validators import identities
@@ -41,6 +42,16 @@ def cast_to_value(from_value, to_value):
pass
+@lru_cache
+def get_pset(element, pset):
+ return ifcopenshell.util.element.get_pset(element, pset)
+
+
+@lru_cache
+def get_psets(element, pset):
+ return ifcopenshell.util.element.get_psets(element)
+
+
class Facet:
def __init__(self, *parameters):
self.status = None
@@ -286,7 +297,7 @@ class Attribute(Facet):
class Classification(Facet):
- def __init__(self, value=None, system=None, uri=None, minOccurs=None, maxOccurs=None, instructions=None):
+ def __init__(self, value=None, system=None, uri=None, minOccurs=None, maxOccurs="unbounded", instructions=None):
self.parameters = ["value", "system", "@uri", "@minOccurs", "@maxOccurs", "@instructions"]
self.applicability_templates = [
"Data having a {system} reference of {value}",
@@ -347,7 +358,7 @@ class PartOf(Facet):
predefinedType=None,
relation=None,
minOccurs=None,
- maxOccurs=None,
+ maxOccurs="unbounded",
instructions=None,
):
self.parameters = ["entity", "predefinedType", "@relation", "@minOccurs", "@maxOccurs", "@instructions"]
@@ -552,7 +563,7 @@ class Property(Facet):
datatype=None,
uri=None,
minOccurs=None,
- maxOccurs=None,
+ maxOccurs="unbounded",
instructions=None,
):
self.parameters = [
@@ -591,10 +602,10 @@ class Property(Facet):
return PropertyResult(True)
if isinstance(self.propertySet, str):
- pset = ifcopenshell.util.element.get_pset(inst, self.propertySet)
+ pset = get_pset(inst, self.propertySet)
psets = {self.propertySet: pset} if pset else {}
else:
- all_psets = ifcopenshell.util.element.get_psets(inst)
+ all_psets = get_psets(inst)
psets = {k: v for k, v in all_psets.items() if k == self.propertySet}
is_pass = bool(psets)
@@ -637,7 +648,7 @@ class Property(Facet):
elif prop_entity.is_a("IfcPropertySingleValue"):
data_type = prop_entity.NominalValue.is_a()
- if data_type != self.datatype:
+ if data_type.lower() != self.datatype.lower():
is_pass = False
reason = {"type": "DATATYPE", "actual": data_type}
break
@@ -656,7 +667,7 @@ class Property(Facet):
prop_schema = prop_entity.wrapped_data.declaration().as_entity()
data_type = prop_schema.attribute_by_index(3).type_of_attribute().declared_type().name()
- if data_type != self.datatype:
+ if data_type.lower() != self.datatype.lower():
is_pass = False
reason = {"type": "DATATYPE", "actual": data_type}
break
@@ -676,7 +687,7 @@ class Property(Facet):
reason = {"type": "NOVALUE"}
break
data_type = prop_entity.EnumerationValues[0].is_a()
- if data_type != self.datatype:
+ if data_type.lower() != self.datatype.lower():
is_pass = False
reason = {"type": "DATATYPE", "actual": data_type}
break
@@ -686,7 +697,7 @@ class Property(Facet):
reason = {"type": "NOVALUE"}
break
data_type = prop_entity.ListValues[0].is_a()
- if data_type != self.datatype:
+ if data_type.lower() != self.datatype.lower():
is_pass = False
reason = {"type": "DATATYPE", "actual": data_type}
break
@@ -709,7 +720,7 @@ class Property(Facet):
if value is not None:
data_type = value.is_a()
values.append(value.wrappedValue)
- if data_type != self.datatype:
+ if data_type.lower() != self.datatype.lower():
is_pass = False
reason = {"type": "DATATYPE", "actual": data_type}
break
@@ -734,7 +745,7 @@ class Property(Facet):
if not column_values:
continue
data_type = column_values[0].is_a()
- if data_type == self.datatype:
+ if data_type.lower() == self.datatype.lower():
column_values = [v.wrappedValue for v in column_values]
unit = units[f"{attribute}Unit"]
if unit:
@@ -824,7 +835,7 @@ class Property(Facet):
class Material(Facet):
- def __init__(self, value=None, uri=None, minOccurs=None, maxOccurs=None, instructions=None):
+ def __init__(self, value=None, uri=None, minOccurs=None, maxOccurs="unbounded", instructions=None):
self.parameters = ["value", "@uri", "@minOccurs", "@maxOccurs", "@instructions"]
self.applicability_templates = [
"All data with a {value} material",
diff --git a/src/ifctester/ifctester/ids.py b/src/ifctester/ifctester/ids.py
index 38f0fc3f95..e57eca7048 100644
--- a/src/ifctester/ifctester/ids.py
+++ b/src/ifctester/ifctester/ids.py
@@ -21,7 +21,7 @@ import datetime
from xmlschema import XMLSchema
from xmlschema import etree_tostring
from xml.etree import ElementTree as ET
-from .facet import Entity, Attribute, Classification, Property, PartOf, Material, Restriction
+from .facet import Entity, Attribute, Classification, Property, PartOf, Material, Restriction, get_pset, get_psets
cwd = os.path.dirname(os.path.realpath(__file__))
@@ -81,7 +81,7 @@ class Ids:
"@xmlns": "http://standards.buildingsmart.org/IDS",
"@xmlns:xs": "http://www.w3.org/2001/XMLSchema",
"@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
- "@xsi:schemaLocation": "http://standards.buildingsmart.org/IDS/ids_09.xsd",
+ "@xsi:schemaLocation": "http://standards.buildingsmart.org/IDS http://standards.buildingsmart.org/IDS/0.9.6/ids.xsd",
"info": self.info,
"specifications": {"specification": []},
}
@@ -113,6 +113,8 @@ class Ids:
return get_schema().is_valid(filepath)
def validate(self, ifc_file, filter_version=False):
+ get_pset.cache_clear()
+ get_psets.cache_clear()
for specification in self.specifications:
specification.reset_status()
specification.validate(ifc_file, filter_version=filter_version)
@@ -173,8 +175,8 @@ class Specification:
self.minOccurs = ids_dict["@minOccurs"]
self.maxOccurs = ids_dict["@maxOccurs"]
self.ifcVersion = ids_dict["@ifcVersion"]
- self.applicability = self.parse_clause(ids_dict["applicability"]) if "applicability" in ids_dict else []
- self.requirements = self.parse_clause(ids_dict["requirements"]) if "requirements" in ids_dict else []
+ self.applicability = self.parse_clause(ids_dict["applicability"]) if ids_dict.get("applicability") is not None else []
+ self.requirements = self.parse_clause(ids_dict["requirements"]) if ids_dict.get("requirements") is not None else []
return self
def parse_clause(self, clause):
@@ -247,9 +249,7 @@ class Specification:
if self.failed_entities:
self.status = False
elif self.maxOccurs == 0:
- if (len(self.applicable_entities)) > 0 and len(self.requirements) == 0:
- self.status = False
- if (len(self.applicable_entities)) > 0 and (len(self.applicable_entities) - len(self.failed_entities)) > 0:
+ if (len(self.applicable_entities)) > 0:
self.status = False
def get_usage(self):
diff --git a/src/ifctester/ifctester/ids.xsd b/src/ifctester/ifctester/ids.xsd
index 5ac58daf18..0d2a595e72 100644
--- a/src/ifctester/ifctester/ids.xsd
+++ b/src/ifctester/ifctester/ids.xsd
@@ -51,9 +51,9 @@
-
+
-
+
@@ -212,7 +212,7 @@
- Author of the IDS can provide an identifier to the specification. This is intended to be a machine readable identifier. Beware: because of the possibility to combine different 'requirement' elements from several ids files this cannot be enforced/assumed as (global) unique.
+ Author of the IDS can provide an identifier to the specification. This is intended to be a machine readable identifier. Beware: because of the possibility to combine different 'specification' elements from several ids files this cannot be enforced/assumed as (global) unique.
diff --git a/src/ifctester/ifctester/reporter.py b/src/ifctester/ifctester/reporter.py
index c982e1ec9b..e9b139b829 100644
--- a/src/ifctester/ifctester/reporter.py
+++ b/src/ifctester/ifctester/reporter.py
@@ -211,16 +211,19 @@ class Json(Reporter):
requirements = []
for requirement in specification.requirements:
total_fail = len(requirement.failed_entities)
+ total_pass = total_applicable - total_fail
+ percent_pass = math.floor((total_pass / total_applicable) * 100) if total_applicable else "N/A"
total_checks += total_applicable
- total_checks_pass += total_applicable - total_fail
+ total_checks_pass += total_pass
requirements.append(
{
"description": requirement.to_string("requirement"),
"status": requirement.status,
"failed_entities": self.report_failed_entities(requirement),
"total_applicable": total_applicable,
- "total_pass": total_applicable - total_fail,
+ "total_pass": total_pass,
"total_fail": total_fail,
+ "percent_pass": percent_pass,
}
)
total_applicable_pass = total_applicable - len(specification.failed_entities)
@@ -433,13 +436,16 @@ class Bcf(Json):
continue
for failure in requirement["failed_entities"]:
element = failure["element"]
- title_components = [
+ title_components = []
+ for title_component in [
element.is_a(),
- getattr(element, "Name", None) or "Unnamed",
+ getattr(element, "Name", "") or "Unnamed",
failure.get("reason", "No reason"),
getattr(element, "GlobalId", ""),
getattr(element, "Tag", ""),
- ]
+ ]:
+ if title_component:
+ title_components.append(title_component)
title = " - ".join(title_components)
description = f'{specification["name"]} - {requirement["description"]}'
topic = bcfxml.add_topic(title, description, "IfcTester")
diff --git a/src/ifctester/test/ids_doc_generator.py b/src/ifctester/test/ids_doc_generator.py
index e6b4d73532..0b0f593576 100644
--- a/src/ifctester/test/ids_doc_generator.py
+++ b/src/ifctester/test/ids_doc_generator.py
@@ -81,7 +81,9 @@ class FacetDocGenerator:
# Create an IDS with the applicability selecting exactly
# the entity type passed to us in `inst`.
specs = ids.Ids(title=name)
- spec = ids.Specification(name=name, minOccurs=1)
+
+ # todo: to resume IFC2X3 we need to ensure that entities and attributes are consistent with that schema in order to pass audit
+ spec = ids.Specification(name=name, minOccurs=1, ifcVersion=["IFC4"])
spec.applicability.append(ids.Entity(name=inst.is_a().upper()))
spec.requirements.append(facet)
specs.specifications.append(spec)
diff --git a/src/ifctester/test/test_facet.py b/src/ifctester/test/test_facet.py
index 5455a816b3..a495fa2554 100644
--- a/src/ifctester/test/test_facet.py
+++ b/src/ifctester/test/test_facet.py
@@ -667,9 +667,11 @@ class TestAttribute:
class TestClassification:
def test_creating_a_classification_facet(self):
facet = Classification()
- assert facet.asdict() == {}
+ assert facet.asdict() == {
+ "@maxOccurs": "unbounded"
+ }
facet = Classification(value="value", system="system")
- assert facet.asdict() == {"value": {"simpleValue": "value"}, "system": {"simpleValue": "system"}}
+ assert facet.asdict() == {"value": {"simpleValue": "value"}, "system": {"simpleValue": "system"}, "@maxOccurs": "unbounded" }
facet = Classification(
value="value",
system="system",
@@ -834,6 +836,7 @@ class TestProperty:
assert facet.asdict() == {
"propertySet": {"simpleValue": "Property_Set"},
"name": {"simpleValue": "PropertyName"},
+ "@maxOccurs": "unbounded"
}
facet = Property(
propertySet="propertySet",
@@ -861,7 +864,7 @@ class TestProperty:
ifc = self.setup_ifc()
- facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IfcLabel")
+ facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IFCLABEL")
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
run("Elements with no properties always fail", facet=facet, inst=element, expected=False)
pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar")
@@ -873,27 +876,27 @@ class TestProperty:
run("A name check will match any property with any string value", facet=facet, inst=element, expected=True)
ifc = self.setup_ifc()
- facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IfcLabel")
+ facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IFCLABEL")
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar")
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "Bar"})
run("A required facet checks all parameters as normal", facet=facet, inst=element, expected=True)
- facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IfcLabel", minOccurs=0, maxOccurs=0)
+ facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IFCLABEL", minOccurs=0, maxOccurs=0)
run("A prohibited facet returns the opposite of a required facet", facet=facet, inst=element, expected=False)
- facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IfcLabel", minOccurs=0)
+ facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IFCLABEL", minOccurs=0)
run("An optional facet always passes regardless of outcome 1/2", facet=facet, inst=element, expected=True)
- facet = Property(propertySet="Foo_Bar", name="Bar", datatype="IfcLabel", minOccurs=0)
+ facet = Property(propertySet="Foo_Bar", name="Bar", datatype="IFCLABEL", minOccurs=0)
run("An optional facet always passes regardless of outcome 2/2", facet=facet, inst=element, expected=True)
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ""})
- facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IfcLogical")
+ facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IFCLOGICAL")
run("An empty string is considered falsey and will not pass", facet=facet, inst=element, expected=False)
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcLogical("UNKNOWN")})
- facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IfcDuration")
+ facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IFCDURATION")
run("A logical unknown is considered falsey and will not pass", facet=facet, inst=element, expected=False)
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcDuration("P0D")})
run("A zero duration will pass", facet=facet, inst=element, expected=True)
- facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IfcBoolean")
+ facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IFCBOOLEAN")
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcBoolean(True)})
run("A property set to true will pass a name check", facet=facet, inst=element, expected=True)
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": False})
@@ -905,7 +908,7 @@ class TestProperty:
)
ifc = self.setup_ifc()
- facet = Property(propertySet="Foo_Bar", name="Foo", value="Bar", datatype="IfcLabel")
+ facet = Property(propertySet="Foo_Bar", name="Foo", value="Bar", datatype="IFCLABEL")
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar")
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "Bar"})
@@ -915,55 +918,55 @@ class TestProperty:
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "Baz"})
run("Specifying a value fails against different values", facet=facet, inst=element, expected=False)
- facet = Property(propertySet="Foo_Bar", name="Foo", value="♫Don'tÄrgerhôtelЊет", datatype="IfcLabel")
+ facet = Property(propertySet="Foo_Bar", name="Foo", value="♫Don'tÄrgerhôtelЊет", datatype="IFCLABEL")
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "♫Don'tÄrgerhôtelЊет"})
run("Non-ascii characters are treated without encoding", facet=facet, inst=element, expected=True)
identifier = "123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345"
facet = Property(
- propertySet="Foo_Bar", name="Foo", value=identifier + "_extra_characters", datatype="IfcIdentifier"
+ propertySet="Foo_Bar", name="Foo", value=identifier + "_extra_characters", datatype="IFCIDENTIFIER"
)
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcIdentifier(identifier)})
run("IDS does not handle string truncation such as for identifiers", facet=facet, inst=element, expected=False)
ifc = self.setup_ifc()
- facet = Property(propertySet="Foo_Bar", name="Foo", value="1", datatype="IfcLabel")
+ facet = Property(propertySet="Foo_Bar", name="Foo", value="1", datatype="IFCLABEL")
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar")
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "1"})
run("A number specified as a string is treated as a string", facet=facet, inst=element, expected=True)
- facet = Property(propertySet="Foo_Bar", name="Foo", value="42", datatype="IfcInteger")
+ facet = Property(propertySet="Foo_Bar", name="Foo", value="42", datatype="IFCINTEGER")
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcInteger(42)})
run("Integer values are checked using type casting 1/4", facet=facet, inst=element, expected=True)
- facet = Property(propertySet="Foo_Bar", name="Foo", value="42.", datatype="IfcInteger")
+ facet = Property(propertySet="Foo_Bar", name="Foo", value="42.", datatype="IFCINTEGER")
run("Integer values are checked using type casting 2/4", facet=facet, inst=element, expected=True)
- facet = Property(propertySet="Foo_Bar", name="Foo", value="42.0", datatype="IfcInteger")
+ facet = Property(propertySet="Foo_Bar", name="Foo", value="42.0", datatype="IFCINTEGER")
run("Integer values are checked using type casting 3/4", facet=facet, inst=element, expected=True)
- facet = Property(propertySet="Foo_Bar", name="Foo", value="42.3", datatype="IfcInteger")
+ facet = Property(propertySet="Foo_Bar", name="Foo", value="42.3", datatype="IFCINTEGER")
run("Integer values are checked using type casting 4/4", facet=facet, inst=element, expected=False)
- facet = Property(propertySet="Foo_Bar", name="Foo", value="42", datatype="IfcReal")
+ facet = Property(propertySet="Foo_Bar", name="Foo", value="42", datatype="IFCREAL")
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcReal(42.0)})
run("Real values are checked using type casting 1/3", facet=facet, inst=element, expected=True)
- facet = Property(propertySet="Foo_Bar", name="Foo", value="42.0", datatype="IfcReal")
+ facet = Property(propertySet="Foo_Bar", name="Foo", value="42.0", datatype="IFCREAL")
run("Real values are checked using type casting 2/3", facet=facet, inst=element, expected=True)
- facet = Property(propertySet="Foo_Bar", name="Foo", value="42.3", datatype="IfcReal")
+ facet = Property(propertySet="Foo_Bar", name="Foo", value="42.3", datatype="IFCREAL")
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcReal(42.3)})
run("Real values are checked using type casting 3/3", facet=facet, inst=element, expected=True)
- facet = Property(propertySet="Foo_Bar", name="Foo", value="42,3", datatype="IfcReal")
+ facet = Property(propertySet="Foo_Bar", name="Foo", value="42,3", datatype="IFCREAL")
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcReal(42.3)})
run("Only specifically formatted numbers are allowed 1/4", facet=facet, inst=element, expected=False)
- facet = Property(propertySet="Foo_Bar", name="Foo", value="123,4.5", datatype="IfcReal")
+ facet = Property(propertySet="Foo_Bar", name="Foo", value="123,4.5", datatype="IFCREAL")
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcReal(1234.5)})
run("Only specifically formatted numbers are allowed 2/4", facet=facet, inst=element, expected=False)
- facet = Property(propertySet="Foo_Bar", name="Foo", value="1.2345e3", datatype="IfcReal")
+ facet = Property(propertySet="Foo_Bar", name="Foo", value="1.2345e3", datatype="IFCREAL")
run("Only specifically formatted numbers are allowed 3/4", facet=facet, inst=element, expected=True)
- facet = Property(propertySet="Foo_Bar", name="Foo", value="1.2345E3", datatype="IfcReal")
+ facet = Property(propertySet="Foo_Bar", name="Foo", value="1.2345E3", datatype="IFCREAL")
run("Only specifically formatted numbers are allowed 4/4", facet=facet, inst=element, expected=True)
- facet = Property(propertySet="Foo_Bar", name="Foo", value="42.", datatype="IfcReal")
+ facet = Property(propertySet="Foo_Bar", name="Foo", value="42.", datatype="IFCREAL")
ifcopenshell.api.run(
"pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcReal(42.0 * (1.0 + 1e-6))}
)
@@ -981,15 +984,15 @@ class TestProperty:
)
run("Floating point numbers are compared with a 1e-6 tolerance 4/4", facet=facet, inst=element, expected=False)
- facet = Property(propertySet="Foo_Bar", name="Foo", value="TRUE", datatype="IfcBoolean")
+ facet = Property(propertySet="Foo_Bar", name="Foo", value="TRUE", datatype="IFCBOOLEAN")
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcBoolean(False)})
run("Booleans must be specified as uppercase strings 1/3", facet=facet, inst=element, expected=False)
- facet = Property(propertySet="Foo_Bar", name="Foo", value="FALSE", datatype="IfcBoolean")
+ facet = Property(propertySet="Foo_Bar", name="Foo", value="FALSE", datatype="IFCBOOLEAN")
run("Booleans must be specified as uppercase strings 2/3", facet=facet, inst=element, expected=True)
- facet = Property(propertySet="Foo_Bar", name="Foo", value="False", datatype="IfcBoolean")
+ facet = Property(propertySet="Foo_Bar", name="Foo", value="False", datatype="IFCBOOLEAN")
run("Booleans must be specified as uppercase strings 3/3", facet=facet, inst=element, expected=False)
- facet = Property(propertySet="Foo_Bar", name="Foo", value="2022-01-01", datatype="IfcDate")
+ facet = Property(propertySet="Foo_Bar", name="Foo", value="2022-01-01", datatype="IFCDATE")
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcDate("2022-01-01")})
run("Dates are treated as strings 1/2", facet=facet, inst=element, expected=True)
ifcopenshell.api.run(
@@ -997,7 +1000,7 @@ class TestProperty:
)
run("Dates are treated as strings 2/2", facet=facet, inst=element, expected=False)
- facet = Property(propertySet="Foo_Bar", name="Foo", value="PT16H", datatype="IfcDuration")
+ facet = Property(propertySet="Foo_Bar", name="Foo", value="PT16H", datatype="IFCDURATION")
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcDuration("PT16H")})
run("Durations are treated as strings 1/2", facet=facet, inst=element, expected=True)
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcDuration("P2D")})
@@ -1014,11 +1017,11 @@ class TestProperty:
properties={"Status": ["EXISTING", "DEMOLISH"]},
pset_template=pset_template,
)
- facet = Property(propertySet="Pset_WallCommon", name="Status", value="EXISTING", datatype="IfcLabel")
+ facet = Property(propertySet="Pset_WallCommon", name="Status", value="EXISTING", datatype="IFCLABEL")
run("Any matching value in an enumerated property will pass 1/3", facet=facet, inst=element, expected=True)
- facet = Property(propertySet="Pset_WallCommon", name="Status", value="DEMOLISH", datatype="IfcLabel")
+ facet = Property(propertySet="Pset_WallCommon", name="Status", value="DEMOLISH", datatype="IFCLABEL")
run("Any matching value in an enumerated property will pass 2/3", facet=facet, inst=element, expected=True)
- facet = Property(propertySet="Pset_WallCommon", name="Status", value="NEW", datatype="IfcLabel")
+ facet = Property(propertySet="Pset_WallCommon", name="Status", value="NEW", datatype="IFCLABEL")
run("Any matching value in an enumerated property will pass 3/3", facet=facet, inst=element, expected=False)
ifc = self.setup_ifc()
@@ -1028,11 +1031,11 @@ class TestProperty:
Name="Foo", ListValues=[ifc.createIfcLabel("X"), ifc.createIfcLabel("Y")]
)
pset.HasProperties = [list_property]
- facet = Property(propertySet="Foo_Bar", name="Foo", value="X", datatype="IfcLabel")
+ facet = Property(propertySet="Foo_Bar", name="Foo", value="X", datatype="IFCLABEL")
run("Any matching value in a list property will pass 1/3", facet=facet, inst=element, expected=True)
- facet = Property(propertySet="Foo_Bar", name="Foo", value="Y", datatype="IfcLabel")
+ facet = Property(propertySet="Foo_Bar", name="Foo", value="Y", datatype="IFCLABEL")
run("Any matching value in a list property will pass 2/3", facet=facet, inst=element, expected=True)
- facet = Property(propertySet="Foo_Bar", name="Foo", value="Z", datatype="IfcLabel")
+ facet = Property(propertySet="Foo_Bar", name="Foo", value="Z", datatype="IFCLABEL")
run("Any matching value in a list property will pass 3/3", facet=facet, inst=element, expected=False)
ifc = self.setup_ifc()
@@ -1045,13 +1048,13 @@ class TestProperty:
SetPointValue=ifc.createIfcLengthMeasure(3000),
)
pset.HasProperties = [bounded_property]
- facet = Property(propertySet="Foo_Bar", name="Foo", value="1", datatype="IfcLengthMeasure")
+ facet = Property(propertySet="Foo_Bar", name="Foo", value="1", datatype="IFCLENGTHMEASURE")
run("Any matching value in a bounded property will pass 1/4", facet=facet, inst=element, expected=True)
- facet = Property(propertySet="Foo_Bar", name="Foo", value="5", datatype="IfcLengthMeasure")
+ facet = Property(propertySet="Foo_Bar", name="Foo", value="5", datatype="IFCLENGTHMEASURE")
run("Any matching value in a bounded property will pass 2/4", facet=facet, inst=element, expected=True)
- facet = Property(propertySet="Foo_Bar", name="Foo", value="3", datatype="IfcLengthMeasure")
+ facet = Property(propertySet="Foo_Bar", name="Foo", value="3", datatype="IFCLENGTHMEASURE")
run("Any matching value in a bounded property will pass 3/4", facet=facet, inst=element, expected=True)
- facet = Property(propertySet="Foo_Bar", name="Foo", value="2", datatype="IfcLengthMeasure")
+ facet = Property(propertySet="Foo_Bar", name="Foo", value="2", datatype="IFCLENGTHMEASURE")
run("Any matching value in a bounded property will pass 4/4", facet=facet, inst=element, expected=False)
ifc = self.setup_ifc()
@@ -1061,18 +1064,18 @@ class TestProperty:
Name="Foo", DefiningValues=[ifc.createIfcLabel("X")], DefinedValues=[ifc.createIfcLengthMeasure(1000)]
)
pset.HasProperties = [table_property]
- facet = Property(propertySet="Foo_Bar", name="Foo", value="X", datatype="IfcLabel")
+ facet = Property(propertySet="Foo_Bar", name="Foo", value="X", datatype="IFCLABEL")
run("Any matching value in a table property will pass 1/3", facet=facet, inst=element, expected=True)
- facet = Property(propertySet="Foo_Bar", name="Foo", value="1", datatype="IfcLengthMeasure")
+ facet = Property(propertySet="Foo_Bar", name="Foo", value="1", datatype="IFCLENGTHMEASURE")
run("Any matching value in a table property will pass 2/3", facet=facet, inst=element, expected=True)
- facet = Property(propertySet="Foo_Bar", name="Foo", value="Y", datatype="IfcLabel")
+ facet = Property(propertySet="Foo_Bar", name="Foo", value="Y", datatype="IFCLABEL")
run("Any matching value in a table property will pass 3/3", facet=facet, inst=element, expected=False)
ifc = self.setup_ifc()
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar")
pset.HasProperties = [ifc.createIfcPropertyReferenceValue(Name="Foo")]
- facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IfcLabel")
+ facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IFCLABEL")
run("Reference properties are treated as objects and not supported", facet=facet, inst=element, expected=False)
ifc = self.setup_ifc()
@@ -1091,21 +1094,21 @@ class TestProperty:
RelatingPropertyDefinition=pset,
)
facet = Property(
- propertySet="Foo_Bar", name="PanelOperation", value="SWINGING", datatype="IfcDoorPanelOperationEnum"
+ propertySet="Foo_Bar", name="PanelOperation", value="SWINGING", datatype="IFCDOORPANELOPERATIONENUM"
)
run("Predefined properties are supported but discouraged 1/2", facet=facet, inst=element, expected=True)
facet = Property(
- propertySet="Foo_Bar", name="PanelOperation", value="SWONGING", datatype="IfcDoorPanelOperationEnum"
+ propertySet="Foo_Bar", name="PanelOperation", value="SWONGING", datatype="IFCDOORPANELOPERATIONENUM"
)
run("Predefined properties are supported but discouraged 2/2", facet=facet, inst=element, expected=False)
ifc = self.setup_ifc()
- facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IfcLengthMeasure")
+ facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IFCLENGTHMEASURE")
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
qto = ifcopenshell.api.run("pset.add_qto", ifc, product=element, name="Foo_Bar")
ifcopenshell.api.run("pset.edit_qto", ifc, qto=qto, properties={"Foo": ifc.createIfcLengthMeasure(42)})
run("A name check will match any quantity with any value", facet=facet, inst=element, expected=True)
- facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IfcAreaMeasure")
+ facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IFCAREAMEASURE")
run("Quantities must also match the appropriate measure", facet=facet, inst=element, expected=False)
ifc = self.setup_ifc()
@@ -1114,9 +1117,9 @@ class TestProperty:
complex_property = ifc.createIfcComplexProperty(Name="Foo", UsageName="RabbitAgilityTraining")
ifcopenshell.api.run("pset.edit_pset", ifc, pset=complex_property, properties={"Rabbits": "Awesome"})
pset.HasProperties = [complex_property]
- facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IfcLabel")
+ facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IFCLABEL")
run("Complex properties are not supported 1/2", facet=facet, inst=element, expected=False)
- facet = Property(propertySet="Foo", name="Rabbits", datatype="IfcLabel")
+ facet = Property(propertySet="Foo", name="Rabbits", datatype="IFCLABEL")
run("Complex properties are not supported 2/2", facet=facet, inst=element, expected=False)
ifc = self.setup_ifc()
@@ -1127,14 +1130,14 @@ class TestProperty:
"pset.edit_qto", ifc, qto=complex_quantity, properties={"MyLength": ifc.createIfcLengthMeasure(42)}
)
qto.Quantities = [complex_quantity]
- facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IfcLengthMeasure")
+ facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IFCLENGTHMEASURE")
run("Complex properties are not supported 1/2", facet=facet, inst=element, expected=False)
- facet = Property(propertySet="Foo", name="MyLength", datatype="IfcLengthMeasure")
+ facet = Property(propertySet="Foo", name="MyLength", datatype="IFCLENGTHMEASURE")
run("Complex properties are not supported 2/2", facet=facet, inst=element, expected=False)
ifc = self.setup_ifc()
restriction = Restriction(options={"pattern": "Foo_.*"})
- facet = Property(propertySet=restriction, name="Foo", datatype="IfcLabel")
+ facet = Property(propertySet=restriction, name="Foo", datatype="IFCLABEL")
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar")
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "Bar"})
@@ -1147,7 +1150,7 @@ class TestProperty:
ifc = self.setup_ifc()
restriction = Restriction(options={"pattern": "Foo.*"})
- facet = Property(propertySet="Foo_Bar", name=restriction, value="x", datatype="IfcLabel")
+ facet = Property(propertySet="Foo_Bar", name=restriction, value="x", datatype="IFCLABEL")
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar")
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foobar": "x"})
@@ -1160,7 +1163,7 @@ class TestProperty:
ifc = self.setup_ifc()
restriction1 = Restriction(options={"pattern": "Foo.*"})
restriction2 = Restriction(options={"enumeration": ["x", "y"]})
- facet = Property(propertySet="Foo_Bar", name=restriction1, value=restriction2, datatype="IfcLabel")
+ facet = Property(propertySet="Foo_Bar", name=restriction1, value=restriction2, datatype="IFCLABEL")
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar")
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foobar": "x", "Foobaz": "y"})
@@ -1179,7 +1182,7 @@ class TestProperty:
)
ifc = self.setup_ifc()
- facet = Property(propertySet="Foo_Bar", name="Foo", value="2", datatype="IfcTimeMeasure")
+ facet = Property(propertySet="Foo_Bar", name="Foo", value="2", datatype="IFCTIMEMEASURE")
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar")
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcMassMeasure(2)})
@@ -1188,7 +1191,7 @@ class TestProperty:
run("Measures are used to specify an IFC data type 2/2", facet=facet, inst=element, expected=True)
ifc = self.setup_ifc()
- facet = Property(propertySet="Foo_Bar", name="Foo", value="2", datatype="IfcLengthMeasure")
+ facet = Property(propertySet="Foo_Bar", name="Foo", value="2", datatype="IFCLENGTHMEASURE")
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar")
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": ifc.createIfcLengthMeasure(2)})
@@ -1212,7 +1215,7 @@ class TestProperty:
ifcopenshell.api.run("type.assign_type", ifc, related_object=wall, relating_type=wall_type)
pset = ifcopenshell.api.run("pset.add_pset", ifc, product=wall_type, name="Foo_Bar")
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "Bar"})
- facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IfcLabel")
+ facet = Property(propertySet="Foo_Bar", name="Foo", datatype="IFCLABEL")
run("Properties can be inherited from the type 1/2", facet=facet, inst=wall, expected=True)
run("Properties can be inherited from the type 2/2", facet=facet, inst=wall_type, expected=True)
@@ -1224,7 +1227,7 @@ class TestProperty:
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "Baz"})
pset = ifcopenshell.api.run("pset.add_pset", ifc, product=wall, name="Foo_Bar")
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"Foo": "Bar"})
- facet = Property(propertySet="Foo_Bar", name="Foo", value="Bar", datatype="IfcLabel")
+ facet = Property(propertySet="Foo_Bar", name="Foo", value="Bar", datatype="IFCLABEL")
run("Properties can be overriden by an occurrence 1/2", facet=facet, inst=wall, expected=True)
run("Properties can be overriden by an occurrence 2/2", facet=facet, inst=wall_type, expected=False)
@@ -1243,7 +1246,7 @@ class TestProperty:
class TestMaterial:
def test_creating_a_material_facet(self):
facet = Material()
- assert facet.asdict() == {}
+ assert facet.asdict() == {"@maxOccurs": "unbounded"}
facet = Material(
value="value", uri="https://test.com", minOccurs="0", maxOccurs="unbounded", instructions="instructions"
)
@@ -1401,9 +1404,9 @@ class TestMaterial:
class TestPartOf:
def test_creating_a_partof_facet(self):
facet = PartOf()
- assert facet.asdict() == {"entity": {"name": {"simpleValue": "IFCWALL"}}}
+ assert facet.asdict() == {"entity": {"name": {"simpleValue": "IFCWALL"}}, "@maxOccurs": "unbounded" }
facet = PartOf(
- entity="IfcGroup",
+ entity="IFCGROUP",
predefinedType="predefinedType",
relation="IFCRELASSIGNSTOGROUP",
minOccurs="0",
@@ -1412,7 +1415,7 @@ class TestPartOf:
)
assert facet.asdict() == {
"entity": {
- "name": {"simpleValue": "IfcGroup"},
+ "name": {"simpleValue": "IFCGROUP"},
"predefinedType": {"simpleValue": "predefinedType"},
},
"@relation": "IFCRELASSIGNSTOGROUP",
diff --git a/src/ifctester/test/test_ids.py b/src/ifctester/test/test_ids.py
index bf392ed8af..ea2b63dd55 100644
--- a/src/ifctester/test/test_ids.py
+++ b/src/ifctester/test/test_ids.py
@@ -48,11 +48,12 @@ class TestIds:
def test_create_an_ids_with_minimal_information(self):
specs = ids.Ids()
+ print('AAA', specs.asdict())
assert specs.asdict() == {
"@xmlns": "http://standards.buildingsmart.org/IDS",
"@xmlns:xs": "http://www.w3.org/2001/XMLSchema",
"@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
- "@xsi:schemaLocation": "http://standards.buildingsmart.org/IDS/ids_09.xsd",
+ "@xsi:schemaLocation": "http://standards.buildingsmart.org/IDS http://standards.buildingsmart.org/IDS/0.9.6/ids.xsd",
"info": {"title": "Untitled"},
"specifications": {"specification": []},
}
@@ -72,7 +73,7 @@ class TestIds:
"@xmlns": "http://standards.buildingsmart.org/IDS",
"@xmlns:xs": "http://www.w3.org/2001/XMLSchema",
"@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
- "@xsi:schemaLocation": "http://standards.buildingsmart.org/IDS/ids_09.xsd",
+ "@xsi:schemaLocation": "http://standards.buildingsmart.org/IDS http://standards.buildingsmart.org/IDS/0.9.6/ids.xsd",
"info": {
"title": "title",
"copyright": "copyright",
@@ -92,7 +93,7 @@ class TestIds:
"@xmlns": "http://standards.buildingsmart.org/IDS",
"@xmlns:xs": "http://www.w3.org/2001/XMLSchema",
"@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
- "@xsi:schemaLocation": "http://standards.buildingsmart.org/IDS/ids_09.xsd",
+ "@xsi:schemaLocation": "http://standards.buildingsmart.org/IDS http://standards.buildingsmart.org/IDS/0.9.6/ids.xsd",
"info": {"title": "Untitled"},
"specifications": {"specification": []},
}
@@ -158,7 +159,7 @@ class TestIds:
run("Prohibited specifications fail if at least one entity passes all requirements 1/3", specs, model, True)
model = ifcopenshell.file()
wall = model.createIfcWall(Name="Wally")
- run("Prohibited specifications fail if at least one entity passes all requirements 2/3", specs, model, True, [wall], [wall])
+ run("Prohibited specifications fail if at least one entity passes all requirements 2/3", specs, model, False, [wall], [wall])
model = ifcopenshell.file()
wall = model.createIfcWall(Name="Waldo")
run("Prohibited specifications fail if at least one entity passes all requirements 3/3", specs, model, False, [wall])